• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.robolectric.shadows;
2 
3 import android.content.pm.PackageInfo;
4 import android.graphics.Bitmap;
5 import android.os.Build;
6 import android.os.Bundle;
7 import android.view.ViewGroup.LayoutParams;
8 import android.webkit.ValueCallback;
9 import android.webkit.WebBackForwardList;
10 import android.webkit.WebChromeClient;
11 import android.webkit.WebHistoryItem;
12 import android.webkit.WebSettings;
13 import android.webkit.WebView;
14 import android.webkit.WebViewClient;
15 import java.lang.reflect.Field;
16 import java.lang.reflect.InvocationHandler;
17 import java.lang.reflect.Method;
18 import java.lang.reflect.Proxy;
19 import java.util.ArrayList;
20 import java.util.Collections;
21 import java.util.HashMap;
22 import java.util.Map;
23 import org.robolectric.annotation.HiddenApi;
24 import org.robolectric.annotation.Implementation;
25 import org.robolectric.annotation.Implements;
26 import org.robolectric.annotation.RealObject;
27 import org.robolectric.annotation.Resetter;
28 import org.robolectric.fakes.RoboWebSettings;
29 import org.robolectric.util.ReflectionHelpers;
30 
31 @SuppressWarnings({"UnusedDeclaration"})
32 @Implements(value = WebView.class)
33 public class ShadowWebView extends ShadowViewGroup {
34   @RealObject private WebView realWebView;
35 
36   private static final String HISTORY_KEY = "ShadowWebView.History";
37 
38   private static PackageInfo packageInfo = null;
39 
40   private String lastUrl;
41   private Map<String, String> lastAdditionalHttpHeaders;
42   private HashMap<String, Object> javascriptInterfaces = new HashMap<>();
43   private WebSettings webSettings = new RoboWebSettings();
44   private WebViewClient webViewClient = null;
45   private boolean clearCacheCalled = false;
46   private boolean clearCacheIncludeDiskFiles = false;
47   private boolean clearFormDataCalled = false;
48   private boolean clearHistoryCalled = false;
49   private boolean clearViewCalled = false;
50   private boolean destroyCalled = false;
51   private boolean onPauseCalled = false;
52   private boolean onResumeCalled = false;
53   private WebChromeClient webChromeClient;
54   private boolean canGoBack;
55   private int goBackInvocations = 0;
56   private LoadData lastLoadData;
57   private LoadDataWithBaseURL lastLoadDataWithBaseURL;
58   private String originalUrl;
59   private ArrayList<String> history = new ArrayList<>();
60   private String lastEvaluatedJavascript;
61   // TODO: Delete this when setCanGoBack is deleted. This is only used to determine which "path" we
62   // use when canGoBack or goBack is called.
63   private boolean canGoBackIsSet;
64 
65   @HiddenApi
66   @Implementation
ensureProviderCreated()67   public void ensureProviderCreated() {
68     final ClassLoader classLoader = getClass().getClassLoader();
69     Class<?> webViewProviderClass = getClassNamed("android.webkit.WebViewProvider");
70     Field mProvider;
71     try {
72       mProvider = WebView.class.getDeclaredField("mProvider");
73       mProvider.setAccessible(true);
74       if (mProvider.get(realView) == null) {
75         Object provider =
76             Proxy.newProxyInstance(
77                 classLoader,
78                 new Class[] {webViewProviderClass},
79                 new InvocationHandler() {
80                   @Override
81                   public Object invoke(Object proxy, Method method, Object[] args)
82                       throws Throwable {
83                     if (method.getName().equals("getViewDelegate")
84                         || method.getName().equals("getScrollDelegate")) {
85                       return Proxy.newProxyInstance(
86                           classLoader,
87                           new Class[] {
88                             getClassNamed("android.webkit.WebViewProvider$ViewDelegate"),
89                             getClassNamed("android.webkit.WebViewProvider$ScrollDelegate")
90                           },
91                           new InvocationHandler() {
92                             @Override
93                             public Object invoke(Object proxy, Method method, Object[] args)
94                                 throws Throwable {
95                               return nullish(method);
96                             }
97                           });
98                     }
99 
100                     return nullish(method);
101                   }
102                 });
103         mProvider.set(realView, provider);
104       }
105     } catch (NoSuchFieldException | IllegalAccessException e) {
106       throw new RuntimeException(e);
107     }
108   }
109 
110   @Implementation
setLayoutParams(LayoutParams params)111   protected void setLayoutParams(LayoutParams params) {
112     ReflectionHelpers.setField(realWebView, "mLayoutParams", params);
113   }
114 
nullish(Method method)115   private Object nullish(Method method) {
116     Class<?> returnType = method.getReturnType();
117     if (returnType.equals(long.class)
118         || returnType.equals(double.class)
119         || returnType.equals(int.class)
120         || returnType.equals(float.class)
121         || returnType.equals(short.class)
122         || returnType.equals(byte.class)) return 0;
123     if (returnType.equals(char.class)) return '\0';
124     if (returnType.equals(boolean.class)) return false;
125     return null;
126   }
127 
getClassNamed(String className)128   private Class<?> getClassNamed(String className) {
129     try {
130       return getClass().getClassLoader().loadClass(className);
131     } catch (ClassNotFoundException e) {
132       throw new RuntimeException(e);
133     }
134   }
135 
136   @Implementation
loadUrl(String url)137   protected void loadUrl(String url) {
138     loadUrl(url, null);
139   }
140 
141   @Implementation
loadUrl(String url, Map<String, String> additionalHttpHeaders)142   protected void loadUrl(String url, Map<String, String> additionalHttpHeaders) {
143     history.add(0, url);
144     originalUrl = url;
145     lastUrl = url;
146 
147     if (additionalHttpHeaders != null) {
148       this.lastAdditionalHttpHeaders = Collections.unmodifiableMap(additionalHttpHeaders);
149     } else {
150       this.lastAdditionalHttpHeaders = null;
151     }
152   }
153 
154   @Implementation
loadDataWithBaseURL( String baseUrl, String data, String mimeType, String encoding, String historyUrl)155   protected void loadDataWithBaseURL(
156       String baseUrl, String data, String mimeType, String encoding, String historyUrl) {
157     if (historyUrl != null) {
158       originalUrl = historyUrl;
159       history.add(0, historyUrl);
160     }
161     lastLoadDataWithBaseURL =
162         new LoadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl);
163   }
164 
165   @Implementation
loadData(String data, String mimeType, String encoding)166   protected void loadData(String data, String mimeType, String encoding) {
167     lastLoadData = new LoadData(data, mimeType, encoding);
168   }
169 
170   /** @return the last loaded url */
getLastLoadedUrl()171   public String getLastLoadedUrl() {
172     return lastUrl;
173   }
174 
175   @Implementation
getOriginalUrl()176   protected String getOriginalUrl() {
177     return originalUrl;
178   }
179 
180   @Implementation
getUrl()181   protected String getUrl() {
182     return originalUrl;
183   }
184 
185   /** @return the additional Http headers that in the same request with last loaded url */
getLastAdditionalHttpHeaders()186   public Map<String, String> getLastAdditionalHttpHeaders() {
187     return lastAdditionalHttpHeaders;
188   }
189 
190   @Implementation
getSettings()191   protected WebSettings getSettings() {
192     return webSettings;
193   }
194 
195   @Implementation
setWebViewClient(WebViewClient client)196   protected void setWebViewClient(WebViewClient client) {
197     webViewClient = client;
198   }
199 
200   @Implementation
setWebChromeClient(WebChromeClient client)201   protected void setWebChromeClient(WebChromeClient client) {
202     webChromeClient = client;
203   }
204 
getWebViewClient()205   public WebViewClient getWebViewClient() {
206     return webViewClient;
207   }
208 
209   @Implementation
addJavascriptInterface(Object obj, String interfaceName)210   protected void addJavascriptInterface(Object obj, String interfaceName) {
211     javascriptInterfaces.put(interfaceName, obj);
212   }
213 
getJavascriptInterface(String interfaceName)214   public Object getJavascriptInterface(String interfaceName) {
215     return javascriptInterfaces.get(interfaceName);
216   }
217 
218   @Implementation
clearCache(boolean includeDiskFiles)219   protected void clearCache(boolean includeDiskFiles) {
220     clearCacheCalled = true;
221     clearCacheIncludeDiskFiles = includeDiskFiles;
222   }
223 
wasClearCacheCalled()224   public boolean wasClearCacheCalled() {
225     return clearCacheCalled;
226   }
227 
didClearCacheIncludeDiskFiles()228   public boolean didClearCacheIncludeDiskFiles() {
229     return clearCacheIncludeDiskFiles;
230   }
231 
232   @Implementation
clearFormData()233   protected void clearFormData() {
234     clearFormDataCalled = true;
235   }
236 
wasClearFormDataCalled()237   public boolean wasClearFormDataCalled() {
238     return clearFormDataCalled;
239   }
240 
241   @Implementation
clearHistory()242   protected void clearHistory() {
243     clearHistoryCalled = true;
244     history.clear();
245   }
246 
wasClearHistoryCalled()247   public boolean wasClearHistoryCalled() {
248     return clearHistoryCalled;
249   }
250 
251   @Implementation
clearView()252   protected void clearView() {
253     clearViewCalled = true;
254   }
255 
wasClearViewCalled()256   public boolean wasClearViewCalled() {
257     return clearViewCalled;
258   }
259 
260   @Implementation
onPause()261   protected void onPause() {
262     onPauseCalled = true;
263   }
264 
wasOnPauseCalled()265   public boolean wasOnPauseCalled() {
266     return onPauseCalled;
267   }
268 
269   @Implementation
onResume()270   protected void onResume() {
271     onResumeCalled = true;
272   }
273 
wasOnResumeCalled()274   public boolean wasOnResumeCalled() {
275     return onResumeCalled;
276   }
277 
278   @Implementation
destroy()279   protected void destroy() {
280     destroyCalled = true;
281   }
282 
wasDestroyCalled()283   public boolean wasDestroyCalled() {
284     return destroyCalled;
285   }
286 
287   /** @return webChromeClient */
getWebChromeClient()288   public WebChromeClient getWebChromeClient() {
289     return webChromeClient;
290   }
291 
292   @Implementation
canGoBack()293   protected boolean canGoBack() {
294     // TODO: Remove the canGoBack check when setCanGoBack is deleted.
295     if (canGoBackIsSet) {
296       return canGoBack;
297     }
298     return history.size() > 1;
299   }
300 
301   @Implementation
goBack()302   protected void goBack() {
303     if (canGoBack()) {
304       goBackInvocations++;
305       // TODO: Delete this when setCanGoBack is deleted, since this creates two different behavior
306       // paths.
307       if (canGoBackIsSet) {
308         return;
309       }
310       history.remove(0);
311       if (!history.isEmpty()) {
312         originalUrl = history.get(0);
313       }
314     }
315   }
316 
317   @Implementation
copyBackForwardList()318   protected WebBackForwardList copyBackForwardList() {
319     return new BackForwardList(history);
320   }
321 
322   @Implementation
findAddress(String addr)323   protected static String findAddress(String addr) {
324     return null;
325   }
326 
327   /**
328    * Overrides the system implementation for getting the WebView package.
329    *
330    * <p>Returns null by default, but this can be changed with {@code #setCurrentWebviewPackage()}.
331    */
332   @Implementation(minSdk = Build.VERSION_CODES.O)
getCurrentWebViewPackage()333   protected static PackageInfo getCurrentWebViewPackage() {
334     return packageInfo;
335   }
336 
337   /** Sets the value to return from {@code #getCurrentWebviewPackage()}. */
setCurrentWebViewPackage(PackageInfo webViewPackageInfo)338   public static void setCurrentWebViewPackage(PackageInfo webViewPackageInfo) {
339     packageInfo = webViewPackageInfo;
340   }
341 
342   @Implementation(minSdk = Build.VERSION_CODES.KITKAT)
evaluateJavascript(String script, ValueCallback<String> callback)343   protected void evaluateJavascript(String script, ValueCallback<String> callback) {
344     this.lastEvaluatedJavascript = script;
345   }
346 
getLastEvaluatedJavascript()347   public String getLastEvaluatedJavascript() {
348     return lastEvaluatedJavascript;
349   }
350 
351   /**
352    * Sets the value to return from {@code android.webkit.WebView#canGoBack()}
353    *
354    * @param canGoBack Value to return from {@code android.webkit.WebView#canGoBack()}
355    * @deprecated Do not depend on this method as it will be removed in a future update. The
356    *     preferered method is to populate a fake web history to use for going back.
357    */
358   @Deprecated
setCanGoBack(boolean canGoBack)359   public void setCanGoBack(boolean canGoBack) {
360     canGoBackIsSet = true;
361     this.canGoBack = canGoBack;
362   }
363 
364   /**
365    * @return goBackInvocations the number of times {@code android.webkit.WebView#goBack()} was
366    *     invoked
367    */
getGoBackInvocations()368   public int getGoBackInvocations() {
369     return goBackInvocations;
370   }
371 
getLastLoadData()372   public LoadData getLastLoadData() {
373     return lastLoadData;
374   }
375 
getLastLoadDataWithBaseURL()376   public LoadDataWithBaseURL getLastLoadDataWithBaseURL() {
377     return lastLoadDataWithBaseURL;
378   }
379 
380   @Implementation
saveState(Bundle outState)381   protected WebBackForwardList saveState(Bundle outState) {
382     if (history.size() > 0) {
383       outState.putStringArrayList(HISTORY_KEY, history);
384     }
385     return new BackForwardList(history);
386   }
387 
388   @Implementation
restoreState(Bundle inState)389   protected WebBackForwardList restoreState(Bundle inState) {
390     history = inState.getStringArrayList(HISTORY_KEY);
391     if (history != null && history.size() > 0) {
392       originalUrl = history.get(0);
393       lastUrl = history.get(0);
394       return new BackForwardList(history);
395     }
396     return null;
397   }
398 
399   @Resetter
reset()400   public static void reset() {
401      packageInfo = null;
402   }
403 
setWebContentsDebuggingEnabled(boolean enabled)404   public static void setWebContentsDebuggingEnabled(boolean enabled) {}
405 
406   public static class LoadDataWithBaseURL {
407     public final String baseUrl;
408     public final String data;
409     public final String mimeType;
410     public final String encoding;
411     public final String historyUrl;
412 
LoadDataWithBaseURL( String baseUrl, String data, String mimeType, String encoding, String historyUrl)413     public LoadDataWithBaseURL(
414         String baseUrl, String data, String mimeType, String encoding, String historyUrl) {
415       this.baseUrl = baseUrl;
416       this.data = data;
417       this.mimeType = mimeType;
418       this.encoding = encoding;
419       this.historyUrl = historyUrl;
420     }
421   }
422 
423   public static class LoadData {
424     public final String data;
425     public final String mimeType;
426     public final String encoding;
427 
LoadData(String data, String mimeType, String encoding)428     public LoadData(String data, String mimeType, String encoding) {
429       this.data = data;
430       this.mimeType = mimeType;
431       this.encoding = encoding;
432     }
433   }
434 
435   private static class BackForwardList extends WebBackForwardList {
436     private final ArrayList<String> history;
437 
BackForwardList(ArrayList<String> history)438     public BackForwardList(ArrayList<String> history) {
439       this.history = (ArrayList<String>) history.clone();
440       // WebView expects the most recently visited item to be at the end of the list.
441       Collections.reverse(this.history);
442     }
443 
444     @Override
getCurrentIndex()445     public int getCurrentIndex() {
446       return history.size() - 1;
447     }
448 
449     @Override
getSize()450     public int getSize() {
451       return history.size();
452     }
453 
454     @Override
getCurrentItem()455     public HistoryItem getCurrentItem() {
456       if (history.isEmpty()) {
457         return null;
458       }
459 
460       return new HistoryItem(history.get(getCurrentIndex()));
461     }
462 
463     @Override
getItemAtIndex(int index)464     public HistoryItem getItemAtIndex(int index) {
465       return new HistoryItem(history.get(index));
466     }
467 
468     @Override
clone()469     protected WebBackForwardList clone() {
470       return new BackForwardList(history);
471     }
472   }
473 
474   private static class HistoryItem extends WebHistoryItem {
475     private final String url;
476 
HistoryItem(String url)477     public HistoryItem(String url) {
478       this.url = url;
479     }
480 
481     @Override
getId()482     public int getId() {
483       return url.hashCode();
484     }
485 
486     @Override
getFavicon()487     public Bitmap getFavicon() {
488       return null;
489     }
490 
491     @Override
getOriginalUrl()492     public String getOriginalUrl() {
493       return url;
494     }
495 
496     @Override
getTitle()497     public String getTitle() {
498       return url;
499     }
500 
501     @Override
getUrl()502     public String getUrl() {
503       return url;
504     }
505 
506     @Override
clone()507     protected HistoryItem clone() {
508       return new HistoryItem(url);
509     }
510   }
511 }
512