Home | History | Annotate | Download | only in shadows
      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
     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
    111   protected void setLayoutParams(LayoutParams params) {
    112     ReflectionHelpers.setField(realWebView, "mLayoutParams", params);
    113   }
    114 
    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 
    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
    137   protected void loadUrl(String url) {
    138     loadUrl(url, null);
    139   }
    140 
    141   @Implementation
    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
    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
    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 */
    171   public String getLastLoadedUrl() {
    172     return lastUrl;
    173   }
    174 
    175   @Implementation
    176   protected String getOriginalUrl() {
    177     return originalUrl;
    178   }
    179 
    180   @Implementation
    181   protected String getUrl() {
    182     return originalUrl;
    183   }
    184 
    185   /** @return the additional Http headers that in the same request with last loaded url */
    186   public Map<String, String> getLastAdditionalHttpHeaders() {
    187     return lastAdditionalHttpHeaders;
    188   }
    189 
    190   @Implementation
    191   protected WebSettings getSettings() {
    192     return webSettings;
    193   }
    194 
    195   @Implementation
    196   protected void setWebViewClient(WebViewClient client) {
    197     webViewClient = client;
    198   }
    199 
    200   @Implementation
    201   protected void setWebChromeClient(WebChromeClient client) {
    202     webChromeClient = client;
    203   }
    204 
    205   public WebViewClient getWebViewClient() {
    206     return webViewClient;
    207   }
    208 
    209   @Implementation
    210   protected void addJavascriptInterface(Object obj, String interfaceName) {
    211     javascriptInterfaces.put(interfaceName, obj);
    212   }
    213 
    214   public Object getJavascriptInterface(String interfaceName) {
    215     return javascriptInterfaces.get(interfaceName);
    216   }
    217 
    218   @Implementation
    219   protected void clearCache(boolean includeDiskFiles) {
    220     clearCacheCalled = true;
    221     clearCacheIncludeDiskFiles = includeDiskFiles;
    222   }
    223 
    224   public boolean wasClearCacheCalled() {
    225     return clearCacheCalled;
    226   }
    227 
    228   public boolean didClearCacheIncludeDiskFiles() {
    229     return clearCacheIncludeDiskFiles;
    230   }
    231 
    232   @Implementation
    233   protected void clearFormData() {
    234     clearFormDataCalled = true;
    235   }
    236 
    237   public boolean wasClearFormDataCalled() {
    238     return clearFormDataCalled;
    239   }
    240 
    241   @Implementation
    242   protected void clearHistory() {
    243     clearHistoryCalled = true;
    244     history.clear();
    245   }
    246 
    247   public boolean wasClearHistoryCalled() {
    248     return clearHistoryCalled;
    249   }
    250 
    251   @Implementation
    252   protected void clearView() {
    253     clearViewCalled = true;
    254   }
    255 
    256   public boolean wasClearViewCalled() {
    257     return clearViewCalled;
    258   }
    259 
    260   @Implementation
    261   protected void onPause() {
    262     onPauseCalled = true;
    263   }
    264 
    265   public boolean wasOnPauseCalled() {
    266     return onPauseCalled;
    267   }
    268 
    269   @Implementation
    270   protected void onResume() {
    271     onResumeCalled = true;
    272   }
    273 
    274   public boolean wasOnResumeCalled() {
    275     return onResumeCalled;
    276   }
    277 
    278   @Implementation
    279   protected void destroy() {
    280     destroyCalled = true;
    281   }
    282 
    283   public boolean wasDestroyCalled() {
    284     return destroyCalled;
    285   }
    286 
    287   /** @return webChromeClient */
    288   public WebChromeClient getWebChromeClient() {
    289     return webChromeClient;
    290   }
    291 
    292   @Implementation
    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
    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
    318   protected WebBackForwardList copyBackForwardList() {
    319     return new BackForwardList(history);
    320   }
    321 
    322   @Implementation
    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)
    333   protected static PackageInfo getCurrentWebViewPackage() {
    334     return packageInfo;
    335   }
    336 
    337   /** Sets the value to return from {@code #getCurrentWebviewPackage()}. */
    338   public static void setCurrentWebViewPackage(PackageInfo webViewPackageInfo) {
    339     packageInfo = webViewPackageInfo;
    340   }
    341 
    342   @Implementation(minSdk = Build.VERSION_CODES.KITKAT)
    343   protected void evaluateJavascript(String script, ValueCallback<String> callback) {
    344     this.lastEvaluatedJavascript = script;
    345   }
    346 
    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
    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    */
    368   public int getGoBackInvocations() {
    369     return goBackInvocations;
    370   }
    371 
    372   public LoadData getLastLoadData() {
    373     return lastLoadData;
    374   }
    375 
    376   public LoadDataWithBaseURL getLastLoadDataWithBaseURL() {
    377     return lastLoadDataWithBaseURL;
    378   }
    379 
    380   @Implementation
    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
    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
    400   public static void reset() {
    401      packageInfo = null;
    402   }
    403 
    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 
    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 
    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 
    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
    445     public int getCurrentIndex() {
    446       return history.size() - 1;
    447     }
    448 
    449     @Override
    450     public int getSize() {
    451       return history.size();
    452     }
    453 
    454     @Override
    455     public HistoryItem getCurrentItem() {
    456       if (history.isEmpty()) {
    457         return null;
    458       }
    459 
    460       return new HistoryItem(history.get(getCurrentIndex()));
    461     }
    462 
    463     @Override
    464     public HistoryItem getItemAtIndex(int index) {
    465       return new HistoryItem(history.get(index));
    466     }
    467 
    468     @Override
    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 
    477     public HistoryItem(String url) {
    478       this.url = url;
    479     }
    480 
    481     @Override
    482     public int getId() {
    483       return url.hashCode();
    484     }
    485 
    486     @Override
    487     public Bitmap getFavicon() {
    488       return null;
    489     }
    490 
    491     @Override
    492     public String getOriginalUrl() {
    493       return url;
    494     }
    495 
    496     @Override
    497     public String getTitle() {
    498       return url;
    499     }
    500 
    501     @Override
    502     public String getUrl() {
    503       return url;
    504     }
    505 
    506     @Override
    507     protected HistoryItem clone() {
    508       return new HistoryItem(url);
    509     }
    510   }
    511 }
    512