• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.robolectric.shadows;
2 
3 import android.annotation.ColorInt;
4 import android.content.pm.PackageInfo;
5 import android.graphics.Bitmap;
6 import android.os.Build;
7 import android.os.Build.VERSION;
8 import android.os.Build.VERSION_CODES;
9 import android.os.Bundle;
10 import android.os.Handler;
11 import android.os.Looper;
12 import android.view.ViewGroup.LayoutParams;
13 import android.webkit.DownloadListener;
14 import android.webkit.ValueCallback;
15 import android.webkit.WebBackForwardList;
16 import android.webkit.WebChromeClient;
17 import android.webkit.WebHistoryItem;
18 import android.webkit.WebMessagePort;
19 import android.webkit.WebSettings;
20 import android.webkit.WebView;
21 import android.webkit.WebView.HitTestResult;
22 import android.webkit.WebViewClient;
23 import android.webkit.WebViewFactoryProvider;
24 import com.google.common.collect.ImmutableList;
25 import java.lang.reflect.Field;
26 import java.lang.reflect.InvocationHandler;
27 import java.lang.reflect.Method;
28 import java.lang.reflect.Proxy;
29 import java.util.ArrayList;
30 import java.util.Collections;
31 import java.util.HashMap;
32 import java.util.List;
33 import java.util.Map;
34 import org.robolectric.annotation.HiddenApi;
35 import org.robolectric.annotation.Implementation;
36 import org.robolectric.annotation.Implements;
37 import org.robolectric.annotation.RealObject;
38 import org.robolectric.annotation.Resetter;
39 import org.robolectric.fakes.RoboWebMessagePort;
40 import org.robolectric.fakes.RoboWebSettings;
41 import org.robolectric.util.ReflectionHelpers;
42 
43 @SuppressWarnings({"UnusedDeclaration"})
44 @Implements(value = WebView.class)
45 public class ShadowWebView extends ShadowViewGroup {
46   @RealObject private WebView realWebView;
47 
48   private static final String HISTORY_KEY = "ShadowWebView.History";
49   private static final String HISTORY_INDEX_KEY = "ShadowWebView.HistoryIndex";
50 
51   private static PackageInfo packageInfo = null;
52 
53   private List<RoboWebMessagePort[]> allCreatedPorts = new ArrayList<>();
54   private String lastUrl;
55   private Map<String, String> lastAdditionalHttpHeaders;
56   private HashMap<String, Object> javascriptInterfaces = new HashMap<>();
57   private WebSettings webSettings = new RoboWebSettings();
58   private WebViewClient webViewClient = null;
59   private boolean clearCacheCalled = false;
60   private boolean clearCacheIncludeDiskFiles = false;
61   private boolean clearFormDataCalled = false;
62   private boolean clearHistoryCalled = false;
63   private boolean clearViewCalled = false;
64   private boolean destroyCalled = false;
65   private boolean onPauseCalled = false;
66   private boolean onResumeCalled = false;
67   private WebChromeClient webChromeClient;
68   private boolean canGoBack;
69   private Bitmap currentFavicon = null;
70   private int goBackInvocations = 0;
71   private int goForwardInvocations = 0;
72   private int reloadInvocations = 0;
73   private LoadData lastLoadData;
74   private LoadDataWithBaseURL lastLoadDataWithBaseURL;
75   private String originalUrl;
76   private int historyIndex = -1;
77   private ArrayList<String> history = new ArrayList<>();
78   private String lastEvaluatedJavascript;
79   private ValueCallback<String> lastEvaluatedJavascriptCallback;
80   // TODO: Delete this when setCanGoBack is deleted. This is only used to determine which "path" we
81   // use when canGoBack or goBack is called.
82   private boolean canGoBackIsSet;
83   private PageLoadType pageLoadType = PageLoadType.UNDEFINED;
84   private HitTestResult hitTestResult = new HitTestResult();
85   private int backgroundColor = 0;
86   private DownloadListener downloadListener;
87   private static WebViewFactoryProvider webViewFactoryProvider;
88 
89   @HiddenApi
90   @Implementation
getFactory()91   protected static WebViewFactoryProvider getFactory() {
92     if (webViewFactoryProvider == null) {
93       webViewFactoryProvider = ReflectionHelpers.createDeepProxy(WebViewFactoryProvider.class);
94     }
95     return webViewFactoryProvider;
96   }
97 
98   @HiddenApi
99   @Implementation
ensureProviderCreated()100   public void ensureProviderCreated() {
101     final ClassLoader classLoader = getClass().getClassLoader();
102     Class<?> webViewProviderClass = getClassNamed("android.webkit.WebViewProvider");
103     Field mProvider;
104     try {
105       mProvider = WebView.class.getDeclaredField("mProvider");
106       mProvider.setAccessible(true);
107       if (mProvider.get(realView) == null) {
108         Object provider =
109             Proxy.newProxyInstance(
110                 classLoader,
111                 new Class[] {webViewProviderClass},
112                 new InvocationHandler() {
113                   @Override
114                   public Object invoke(Object proxy, Method method, Object[] args)
115                       throws Throwable {
116                     if (method.getName().equals("getViewDelegate")
117                         || method.getName().equals("getScrollDelegate")) {
118                       return Proxy.newProxyInstance(
119                           classLoader,
120                           new Class[] {
121                             getClassNamed("android.webkit.WebViewProvider$ViewDelegate"),
122                             getClassNamed("android.webkit.WebViewProvider$ScrollDelegate")
123                           },
124                           new InvocationHandler() {
125                             @Override
126                             public Object invoke(Object proxy, Method method, Object[] args)
127                                 throws Throwable {
128                               return nullish(method);
129                             }
130                           });
131                     }
132 
133                     return nullish(method);
134                   }
135                 });
136         mProvider.set(realView, provider);
137       }
138     } catch (NoSuchFieldException | IllegalAccessException e) {
139       throw new RuntimeException(e);
140     }
141   }
142 
143   @Implementation
setLayoutParams(LayoutParams params)144   protected void setLayoutParams(LayoutParams params) {
145     ReflectionHelpers.setField(realWebView, "mLayoutParams", params);
146   }
147 
nullish(Method method)148   private Object nullish(Method method) {
149     Class<?> returnType = method.getReturnType();
150     if (returnType.equals(long.class)
151         || returnType.equals(double.class)
152         || returnType.equals(int.class)
153         || returnType.equals(float.class)
154         || returnType.equals(short.class)
155         || returnType.equals(byte.class)) return 0;
156     if (returnType.equals(char.class)) return '\0';
157     if (returnType.equals(boolean.class)) return false;
158     return null;
159   }
160 
getClassNamed(String className)161   private Class<?> getClassNamed(String className) {
162     try {
163       return getClass().getClassLoader().loadClass(className);
164     } catch (ClassNotFoundException e) {
165       throw new RuntimeException(e);
166     }
167   }
168 
169   @Implementation
loadUrl(String url)170   protected void loadUrl(String url) {
171     loadUrl(url, null);
172   }
173 
174   /**
175    * Fires a request to load the given {@code url} in WebView.
176    *
177    * <p>The {@code url} is is not added to the history until {@link #pushEntryToHistory(String)} is
178    * called. If you want to simulate a redirect you can pass the redirect URL to {@link
179    * #pushEntryToHistory(String)}.
180    */
181   @Implementation
loadUrl(String url, Map<String, String> additionalHttpHeaders)182   protected void loadUrl(String url, Map<String, String> additionalHttpHeaders) {
183     originalUrl = url;
184     lastUrl = url;
185 
186     if (additionalHttpHeaders != null) {
187       this.lastAdditionalHttpHeaders = Collections.unmodifiableMap(additionalHttpHeaders);
188     } else {
189       this.lastAdditionalHttpHeaders = null;
190     }
191 
192     performPageLoadType(url);
193   }
194 
195   @Implementation
loadDataWithBaseURL( String baseUrl, String data, String mimeType, String encoding, String historyUrl)196   protected void loadDataWithBaseURL(
197       String baseUrl, String data, String mimeType, String encoding, String historyUrl) {
198     if (historyUrl != null) {
199       originalUrl = historyUrl;
200     }
201     lastLoadDataWithBaseURL =
202         new LoadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl);
203 
204     performPageLoadType(baseUrl);
205   }
206 
207   @Implementation
loadData(String data, String mimeType, String encoding)208   protected void loadData(String data, String mimeType, String encoding) {
209     lastLoadData = new LoadData(data, mimeType, encoding);
210 
211     performPageLoadType(data);
212   }
213 
214   /**
215    * Pushes an entry to the history with the given {@code url}.
216    *
217    * <p>This method can be used after a {@link #loadUrl(String)} call to push that navigation into
218    * the history. This matches the prod behaviour of WebView, a navigation is never committed to
219    * history inline and can take an arbitrary amount of time depending on the network connection.
220    * Notice that the given {@code url} does not need to match that of the {@link #loadUrl(String)}
221    * as URL can be changed e.g. through server-side redirects without WebView being notified by the
222    * time it is committed.
223    *
224    * <p>This method can also be used to simulate navigations started by user interaction, as these
225    * would still add an entry to the history themselves.
226    *
227    * <p>If there are any entries ahead of the current index (for forward navigation) these are
228    * removed.
229    */
pushEntryToHistory(String url)230   public void pushEntryToHistory(String url) {
231     history.subList(historyIndex + 1, history.size()).clear();
232 
233     history.add(url);
234     historyIndex++;
235 
236     originalUrl = url;
237   }
238 
239   /**
240    * Performs no callbacks on {@link WebViewClient} and {@link WebChromeClient} when any of {@link
241    * #loadUrl}, {@link loadData} or {@link #loadDataWithBaseURL} is called.
242    */
performNoPageLoadClientCallbacks()243   public void performNoPageLoadClientCallbacks() {
244     this.pageLoadType = PageLoadType.UNDEFINED;
245   }
246 
247   /**
248    * Performs callbacks on {@link WebViewClient} and {@link WebChromeClient} that simulates a
249    * successful page load when any of {@link #loadUrl}, {@link loadData} or {@link
250    * #loadDataWithBaseURL} is called.
251    */
performSuccessfulPageLoadClientCallbacks()252   public void performSuccessfulPageLoadClientCallbacks() {
253     this.pageLoadType = PageLoadType.SUCCESS;
254   }
255 
performPageLoadType(String url)256   private void performPageLoadType(String url) {
257     switch (pageLoadType) {
258       case SUCCESS:
259         performSuccessfulPageLoad(url);
260         break;
261       case UNDEFINED:
262         break;
263     }
264   }
265 
performSuccessfulPageLoad(String url)266   private void performSuccessfulPageLoad(String url) {
267     new Handler(Looper.getMainLooper())
268         .post(
269             () -> {
270               if (webChromeClient != null) {
271                 webChromeClient.onProgressChanged(realWebView, 10);
272               }
273               if (webViewClient != null) {
274                 webViewClient.onPageStarted(realWebView, url, /* favicon= */ null);
275               }
276               if (webChromeClient != null) {
277                 webChromeClient.onProgressChanged(realWebView, 40);
278                 webChromeClient.onProgressChanged(realWebView, 80);
279               }
280               if (webViewClient != null && VERSION.SDK_INT >= 23) {
281                 webViewClient.onPageCommitVisible(realWebView, url);
282               }
283               if (webChromeClient != null) {
284                 webChromeClient.onReceivedTitle(realWebView, url);
285                 webChromeClient.onProgressChanged(realWebView, 100);
286               }
287               if (webViewClient != null) {
288                 webViewClient.onPageFinished(realWebView, url);
289               }
290             });
291   }
292 
293   /**
294    * @return the last loaded url
295    */
getLastLoadedUrl()296   public String getLastLoadedUrl() {
297     return lastUrl;
298   }
299 
300   @Implementation
getOriginalUrl()301   protected String getOriginalUrl() {
302     return originalUrl;
303   }
304 
305   @Implementation
getUrl()306   protected String getUrl() {
307     return originalUrl;
308   }
309 
310   @Implementation
getTitle()311   protected String getTitle() {
312     return originalUrl;
313   }
314 
315   /**
316    * @return the additional Http headers that in the same request with last loaded url
317    */
getLastAdditionalHttpHeaders()318   public Map<String, String> getLastAdditionalHttpHeaders() {
319     return lastAdditionalHttpHeaders;
320   }
321 
322   @Implementation
getSettings()323   protected WebSettings getSettings() {
324     return webSettings;
325   }
326 
327   @Implementation
setWebViewClient(WebViewClient client)328   protected void setWebViewClient(WebViewClient client) {
329     webViewClient = client;
330   }
331 
332   @Implementation
setWebChromeClient(WebChromeClient client)333   protected void setWebChromeClient(WebChromeClient client) {
334     webChromeClient = client;
335   }
336 
337   @Implementation(minSdk = VERSION_CODES.O)
getWebViewClient()338   public WebViewClient getWebViewClient() {
339     return webViewClient;
340   }
341 
342   @Implementation
addJavascriptInterface(Object obj, String interfaceName)343   protected void addJavascriptInterface(Object obj, String interfaceName) {
344     javascriptInterfaces.put(interfaceName, obj);
345   }
346 
getJavascriptInterface(String interfaceName)347   public Object getJavascriptInterface(String interfaceName) {
348     return javascriptInterfaces.get(interfaceName);
349   }
350 
351   @Implementation
removeJavascriptInterface(String name)352   protected void removeJavascriptInterface(String name) {
353     javascriptInterfaces.remove(name);
354   }
355 
356   @Implementation(minSdk = Build.VERSION_CODES.M)
createWebMessageChannel()357   protected WebMessagePort[] createWebMessageChannel() {
358     RoboWebMessagePort[] ports = RoboWebMessagePort.createPair();
359     allCreatedPorts.add(ports);
360     return ports;
361   }
362 
getCreatedPorts()363   public List<RoboWebMessagePort[]> getCreatedPorts() {
364     return ImmutableList.copyOf(allCreatedPorts);
365   }
366 
367   @Implementation
clearCache(boolean includeDiskFiles)368   protected void clearCache(boolean includeDiskFiles) {
369     clearCacheCalled = true;
370     clearCacheIncludeDiskFiles = includeDiskFiles;
371   }
372 
wasClearCacheCalled()373   public boolean wasClearCacheCalled() {
374     return clearCacheCalled;
375   }
376 
didClearCacheIncludeDiskFiles()377   public boolean didClearCacheIncludeDiskFiles() {
378     return clearCacheIncludeDiskFiles;
379   }
380 
381   @Implementation
clearFormData()382   protected void clearFormData() {
383     clearFormDataCalled = true;
384   }
385 
wasClearFormDataCalled()386   public boolean wasClearFormDataCalled() {
387     return clearFormDataCalled;
388   }
389 
390   @Implementation
clearHistory()391   protected void clearHistory() {
392     clearHistoryCalled = true;
393     history.clear();
394     historyIndex = -1;
395   }
396 
wasClearHistoryCalled()397   public boolean wasClearHistoryCalled() {
398     return clearHistoryCalled;
399   }
400 
401   @Implementation
reload()402   protected void reload() {
403     reloadInvocations++;
404   }
405 
406   /** Returns the number of times {@code android.webkit.WebView#reload()} was invoked */
getReloadInvocations()407   public int getReloadInvocations() {
408     return reloadInvocations;
409   }
410 
411   @Implementation
clearView()412   protected void clearView() {
413     clearViewCalled = true;
414   }
415 
wasClearViewCalled()416   public boolean wasClearViewCalled() {
417     return clearViewCalled;
418   }
419 
420   @Implementation
onPause()421   protected void onPause() {
422     onPauseCalled = true;
423   }
424 
wasOnPauseCalled()425   public boolean wasOnPauseCalled() {
426     return onPauseCalled;
427   }
428 
429   @Implementation
onResume()430   protected void onResume() {
431     onResumeCalled = true;
432   }
433 
wasOnResumeCalled()434   public boolean wasOnResumeCalled() {
435     return onResumeCalled;
436   }
437 
438   @Implementation
destroy()439   protected void destroy() {
440     destroyCalled = true;
441   }
442 
wasDestroyCalled()443   public boolean wasDestroyCalled() {
444     return destroyCalled;
445   }
446 
447   /**
448    * @return webChromeClient
449    */
450   @Implementation(minSdk = VERSION_CODES.O)
getWebChromeClient()451   public WebChromeClient getWebChromeClient() {
452     return webChromeClient;
453   }
454 
455   @Implementation
canGoBack()456   protected boolean canGoBack() {
457     // TODO: Remove the canGoBack check when setCanGoBack is deleted.
458     if (canGoBackIsSet) {
459       return canGoBack;
460     }
461     return historyIndex > 0;
462   }
463 
464   @Implementation
canGoForward()465   protected boolean canGoForward() {
466     return historyIndex < history.size() - 1;
467   }
468 
469   @Implementation
goBack()470   protected void goBack() {
471     if (canGoBack()) {
472       goBackInvocations++;
473       // TODO: Delete this when setCanGoBack is deleted, since this creates two different behavior
474       // paths.
475       if (canGoBackIsSet) {
476         return;
477       }
478       historyIndex--;
479       originalUrl = history.get(historyIndex);
480     }
481   }
482 
483   @Implementation
goForward()484   protected void goForward() {
485     if (canGoForward()) {
486       goForwardInvocations++;
487       historyIndex++;
488       originalUrl = history.get(historyIndex);
489     }
490   }
491 
492   @Implementation
goBackOrForward(int steps)493   protected void goBackOrForward(int steps) {
494     if (steps == 0) {
495       return;
496     }
497     if (steps > 0) {
498       while (steps-- > 0) {
499         goForward();
500       }
501       return;
502     }
503 
504     while (steps++ < 0) {
505       goBack();
506     }
507   }
508 
509   @Implementation
copyBackForwardList()510   protected WebBackForwardList copyBackForwardList() {
511     return new BackForwardList(history, historyIndex);
512   }
513 
514   @Implementation
findAddress(String addr)515   protected static String findAddress(String addr) {
516     return null;
517   }
518 
519   /**
520    * Overrides the system implementation for getting the WebView package.
521    *
522    * <p>Returns null by default, but this can be changed with {@code #setCurrentWebviewPackage()}.
523    */
524   @Implementation(minSdk = Build.VERSION_CODES.O)
getCurrentWebViewPackage()525   protected static PackageInfo getCurrentWebViewPackage() {
526     return packageInfo;
527   }
528 
529   /** Sets the value to return from {@code #getCurrentWebviewPackage()}. */
setCurrentWebViewPackage(PackageInfo webViewPackageInfo)530   public static void setCurrentWebViewPackage(PackageInfo webViewPackageInfo) {
531     packageInfo = webViewPackageInfo;
532   }
533 
534   /** Gets the favicon for the current page set by {@link #setFavicon}. */
535   @Implementation
getFavicon()536   protected Bitmap getFavicon() {
537     return currentFavicon;
538   }
539 
540   /** Sets the favicon to return from {@link #getFavicon}. */
setFavicon(Bitmap favicon)541   public void setFavicon(Bitmap favicon) {
542     currentFavicon = favicon;
543   }
544 
545   @Implementation
evaluateJavascript(String script, ValueCallback<String> callback)546   protected void evaluateJavascript(String script, ValueCallback<String> callback) {
547     this.lastEvaluatedJavascript = script;
548     this.lastEvaluatedJavascriptCallback = callback;
549   }
550 
551   /**
552    * Returns the last evaluated Javascript value provided to {@link #evaluateJavascript(String,
553    * ValueCallback)} or null if the method has not been called.
554    */
getLastEvaluatedJavascript()555   public String getLastEvaluatedJavascript() {
556     return lastEvaluatedJavascript;
557   }
558 
559   /**
560    * Returns the last callback value provided to {@link #evaluateJavascript(String, ValueCallback)}
561    * or null if the method has not been called.
562    */
getLastEvaluatedJavascriptCallback()563   public ValueCallback<String> getLastEvaluatedJavascriptCallback() {
564     return lastEvaluatedJavascriptCallback;
565   }
566 
567   /**
568    * Sets the value to return from {@code android.webkit.WebView#canGoBack()}
569    *
570    * @param canGoBack Value to return from {@code android.webkit.WebView#canGoBack()}
571    * @deprecated Do not depend on this method as it will be removed in a future update. The
572    *     preferred method is to populate a fake web history to use for going back.
573    */
574   @Deprecated
setCanGoBack(boolean canGoBack)575   public void setCanGoBack(boolean canGoBack) {
576     canGoBackIsSet = true;
577     this.canGoBack = canGoBack;
578   }
579 
580   /** Returns the number of times {@code android.webkit.WebView#goBack()} was invoked. */
getGoBackInvocations()581   public int getGoBackInvocations() {
582     return goBackInvocations;
583   }
584 
585   /** Returns the number of times {@code android.webkit.WebView#goForward()} was invoked. */
getGoForwardInvocations()586   public int getGoForwardInvocations() {
587     return goForwardInvocations;
588   }
589 
getLastLoadData()590   public LoadData getLastLoadData() {
591     return lastLoadData;
592   }
593 
getLastLoadDataWithBaseURL()594   public LoadDataWithBaseURL getLastLoadDataWithBaseURL() {
595     return lastLoadDataWithBaseURL;
596   }
597 
598   @Implementation
saveState(Bundle outState)599   protected WebBackForwardList saveState(Bundle outState) {
600     if (history.size() > 0) {
601       outState.putStringArrayList(HISTORY_KEY, history);
602       outState.putInt(HISTORY_INDEX_KEY, historyIndex);
603     }
604     return new BackForwardList(history, historyIndex);
605   }
606 
607   @Implementation
restoreState(Bundle inState)608   protected WebBackForwardList restoreState(Bundle inState) {
609     history = inState.getStringArrayList(HISTORY_KEY);
610     if (history == null) {
611       history = new ArrayList<>();
612       historyIndex = -1;
613     } else {
614       historyIndex = inState.getInt(HISTORY_INDEX_KEY);
615     }
616 
617     if (history.size() > 0) {
618       originalUrl = history.get(historyIndex);
619       lastUrl = history.get(historyIndex);
620       return new BackForwardList(history, historyIndex);
621     }
622     return null;
623   }
624 
625   @Implementation
getHitTestResult()626   protected HitTestResult getHitTestResult() {
627     return hitTestResult;
628   }
629 
630   /** Creates an instance of {@link HitTestResult}. */
createHitTestResult(int type, String extra)631   public static HitTestResult createHitTestResult(int type, String extra) {
632     HitTestResult hitTestResult = new HitTestResult();
633     hitTestResult.setType(type);
634     hitTestResult.setExtra(extra);
635     return hitTestResult;
636   }
637 
638   /** Sets the {@link HitTestResult} that should be returned from {@link #getHitTestResult()}. */
setHitTestResult(HitTestResult hitTestResult)639   public void setHitTestResult(HitTestResult hitTestResult) {
640     this.hitTestResult = hitTestResult;
641   }
642 
643   @Resetter
reset()644   public static void reset() {
645     packageInfo = null;
646   }
647 
648   @Implementation
setWebContentsDebuggingEnabled(boolean enabled)649   public static void setWebContentsDebuggingEnabled(boolean enabled) {}
650 
651   /**
652    * Sets the {@link android.graphics.Color} int that should be returned from {@link
653    * #getBackgroundColor}.
654    *
655    * <p>WebView uses the background color set by the {@link
656    * android.webkit.WebView#setBackgroundColor} method to internally tint the background color of
657    * web pages until they are drawn. The way this API works is completely independent of the {@link
658    * android.view.View#setBackgroundColor} method and it interacts directly with WebView renderers.
659    * Tests can access the set background color using the {@link #getBackgroundColor} method.
660    */
661   @Implementation
setBackgroundColor(@olorInt int backgroundColor)662   protected void setBackgroundColor(@ColorInt int backgroundColor) {
663     this.backgroundColor = backgroundColor;
664   }
665 
666   /**
667    * Returns the {@link android.graphics.Color} int that has been set by {@link
668    * #setBackgroundColor}.
669    */
getBackgroundColor()670   public int getBackgroundColor() {
671     return backgroundColor;
672   }
673 
674   @Implementation
setDownloadListener(DownloadListener downloadListener)675   protected void setDownloadListener(DownloadListener downloadListener) {
676     this.downloadListener = downloadListener;
677   }
678 
679   /** Returns the {@link DownloadListener} set with {@link #setDownloadListener}, if any. */
getDownloadListener()680   public DownloadListener getDownloadListener() {
681     return this.downloadListener;
682   }
683 
684   public static class LoadDataWithBaseURL {
685     public final String baseUrl;
686     public final String data;
687     public final String mimeType;
688     public final String encoding;
689     public final String historyUrl;
690 
LoadDataWithBaseURL( String baseUrl, String data, String mimeType, String encoding, String historyUrl)691     public LoadDataWithBaseURL(
692         String baseUrl, String data, String mimeType, String encoding, String historyUrl) {
693       this.baseUrl = baseUrl;
694       this.data = data;
695       this.mimeType = mimeType;
696       this.encoding = encoding;
697       this.historyUrl = historyUrl;
698     }
699   }
700 
701   public static class LoadData {
702     public final String data;
703     public final String mimeType;
704     public final String encoding;
705 
LoadData(String data, String mimeType, String encoding)706     public LoadData(String data, String mimeType, String encoding) {
707       this.data = data;
708       this.mimeType = mimeType;
709       this.encoding = encoding;
710     }
711   }
712 
713   /**
714    * Defines a type of page load which is associated with a certain order of {@link WebViewClient}
715    * and {@link WebChromeClient} callbacks.
716    *
717    * <p>A page load is triggered either using {@link #loadUrl}, {@link loadData} or {@link
718    * loadDataWithBaseURL}.
719    */
720   private enum PageLoadType {
721     /** Default type, triggers no {@link WebViewClient} or {@link WebChromeClient} callbacks. */
722     UNDEFINED,
723     /**
724      * Represents a successful page load, which triggers all the associated {@link WebViewClient} or
725      * {@link WebChromeClient} callbacks from {@code onPageStarted} until {@code onPageFinished}
726      * without any error.
727      */
728     SUCCESS
729   }
730 
731   private static class BackForwardList extends WebBackForwardList {
732     private final ArrayList<String> history;
733     private final int index;
734 
BackForwardList(ArrayList<String> history, int index)735     public BackForwardList(ArrayList<String> history, int index) {
736       this.history = (ArrayList<String>) history.clone();
737       this.index = index;
738     }
739 
740     @Override
getCurrentIndex()741     public int getCurrentIndex() {
742       return index;
743     }
744 
745     @Override
getSize()746     public int getSize() {
747       return history.size();
748     }
749 
750     @Override
getCurrentItem()751     public HistoryItem getCurrentItem() {
752       if (history.isEmpty()) {
753         return null;
754       }
755 
756       return new HistoryItem(history.get(getCurrentIndex()));
757     }
758 
759     @Override
getItemAtIndex(int index)760     public HistoryItem getItemAtIndex(int index) {
761       return new HistoryItem(history.get(index));
762     }
763 
764     @Override
clone()765     protected WebBackForwardList clone() {
766       return new BackForwardList(history, index);
767     }
768   }
769 
770   private static class HistoryItem extends WebHistoryItem {
771     private final String url;
772 
HistoryItem(String url)773     public HistoryItem(String url) {
774       this.url = url;
775     }
776 
777     @Override
getId()778     public int getId() {
779       return url.hashCode();
780     }
781 
782     @Override
getFavicon()783     public Bitmap getFavicon() {
784       return null;
785     }
786 
787     @Override
getOriginalUrl()788     public String getOriginalUrl() {
789       return url;
790     }
791 
792     @Override
getTitle()793     public String getTitle() {
794       return url;
795     }
796 
797     @Override
getUrl()798     public String getUrl() {
799       return url;
800     }
801 
802     @Override
clone()803     protected HistoryItem clone() {
804       return new HistoryItem(url);
805     }
806   }
807 }
808