Home | History | Annotate | Download | only in shadows
      1 package org.robolectric.shadows;
      2 
      3 import android.os.Build;
      4 import android.view.ViewGroup.LayoutParams;
      5 import android.webkit.ValueCallback;
      6 import android.webkit.WebChromeClient;
      7 import android.webkit.WebSettings;
      8 import android.webkit.WebView;
      9 import android.webkit.WebViewClient;
     10 import java.lang.reflect.Field;
     11 import java.lang.reflect.InvocationHandler;
     12 import java.lang.reflect.Method;
     13 import java.lang.reflect.Proxy;
     14 import java.util.ArrayList;
     15 import java.util.Collections;
     16 import java.util.HashMap;
     17 import java.util.List;
     18 import java.util.Map;
     19 import org.robolectric.annotation.HiddenApi;
     20 import org.robolectric.annotation.Implementation;
     21 import org.robolectric.annotation.Implements;
     22 import org.robolectric.annotation.RealObject;
     23 import org.robolectric.fakes.RoboWebSettings;
     24 import org.robolectric.util.ReflectionHelpers;
     25 
     26 @SuppressWarnings({"UnusedDeclaration"})
     27 @Implements(value = WebView.class, inheritImplementationMethods = true)
     28 public class ShadowWebView extends ShadowViewGroup {
     29   @RealObject
     30   private WebView realWebView;
     31 
     32   private String lastUrl;
     33   private Map<String, String> lastAdditionalHttpHeaders;
     34   private HashMap<String, Object> javascriptInterfaces = new HashMap<>();
     35   private WebSettings webSettings = new RoboWebSettings();
     36   private WebViewClient webViewClient = null;
     37   private boolean runFlag = false;
     38   private boolean clearCacheCalled = false;
     39   private boolean clearCacheIncludeDiskFiles = false;
     40   private boolean clearFormDataCalled = false;
     41   private boolean clearHistoryCalled = false;
     42   private boolean clearViewCalled = false;
     43   private boolean destroyCalled = false;
     44   private boolean onPauseCalled = false;
     45   private boolean onResumeCalled = false;
     46   private WebChromeClient webChromeClient;
     47   private boolean canGoBack;
     48   private int goBackInvocations = 0;
     49   private LoadData lastLoadData;
     50   private LoadDataWithBaseURL lastLoadDataWithBaseURL;
     51   private String originalUrl;
     52   private List<String> history = new ArrayList<>();
     53   private String lastEvaluatedJavascript;
     54   // TODO: Delete this when setCanGoBack is deleted. This is only used to determine which "path" we
     55   // use when canGoBack or goBack is called.
     56   private boolean canGoBackIsSet;
     57 
     58   @HiddenApi @Implementation
     59   public void ensureProviderCreated() {
     60     final ClassLoader classLoader = getClass().getClassLoader();
     61     Class<?> webViewProviderClass = getClassNamed("android.webkit.WebViewProvider");
     62     Field mProvider;
     63     try {
     64       mProvider = WebView.class.getDeclaredField("mProvider");
     65       mProvider.setAccessible(true);
     66       if (mProvider.get(realView) == null) {
     67         Object provider = Proxy.newProxyInstance(classLoader, new Class[]{webViewProviderClass}, new InvocationHandler() {
     68           @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
     69             if (method.getName().equals("getViewDelegate") || method.getName().equals("getScrollDelegate")) {
     70               return Proxy.newProxyInstance(classLoader, new Class[]{
     71                   getClassNamed("android.webkit.WebViewProvider$ViewDelegate"),
     72                   getClassNamed("android.webkit.WebViewProvider$ScrollDelegate")
     73               }, new InvocationHandler() {
     74                 @Override
     75                 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
     76                   return nullish(method);
     77                 }
     78               });
     79             }
     80 
     81             return nullish(method);
     82           }
     83         });
     84         mProvider.set(realView, provider);
     85       }
     86     } catch (NoSuchFieldException | IllegalAccessException e) {
     87       throw new RuntimeException(e);
     88     }
     89   }
     90 
     91   @Implementation
     92   public void setLayoutParams(LayoutParams params) {
     93     ReflectionHelpers.setField(realWebView, "mLayoutParams", params);
     94   }
     95 
     96   private Object nullish(Method method) {
     97     Class<?> returnType = method.getReturnType();
     98     if (returnType.equals(long.class)
     99         || returnType.equals(double.class)
    100         || returnType.equals(int.class)
    101         || returnType.equals(float.class)
    102         || returnType.equals(short.class)
    103         || returnType.equals(byte.class)
    104         ) return 0;
    105     if (returnType.equals(char.class)) return '\0';
    106     if (returnType.equals(boolean.class)) return false;
    107     return null;
    108   }
    109 
    110   private Class<?> getClassNamed(String className) {
    111     try {
    112       return getClass().getClassLoader().loadClass(className);
    113     } catch (ClassNotFoundException e) {
    114       throw new RuntimeException(e);
    115     }
    116   }
    117 
    118   @Implementation
    119   public void loadUrl(String url) {
    120     loadUrl(url, null);
    121   }
    122 
    123   @Implementation
    124   public void loadUrl(String url, Map<String, String> additionalHttpHeaders) {
    125     history.add(0, url);
    126     originalUrl = url;
    127     lastUrl = url;
    128 
    129     if (additionalHttpHeaders != null) {
    130       this.lastAdditionalHttpHeaders = Collections.unmodifiableMap(additionalHttpHeaders);
    131     } else {
    132       this.lastAdditionalHttpHeaders = null;
    133     }
    134   }
    135 
    136   @Implementation
    137   public void loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String historyUrl) {
    138     if (historyUrl != null) {
    139       originalUrl = historyUrl;
    140       history.add(0, historyUrl);
    141     }
    142     lastLoadDataWithBaseURL = new LoadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl);
    143   }
    144 
    145   @Implementation
    146   public void loadData(String data, String mimeType, String encoding) {
    147     lastLoadData = new LoadData(data, mimeType, encoding);
    148   }
    149 
    150   /**
    151    * @return the last loaded url
    152    */
    153   public String getLastLoadedUrl() {
    154     return lastUrl;
    155   }
    156 
    157   @Implementation
    158   public String getOriginalUrl() {
    159     return originalUrl;
    160   }
    161 
    162   @Implementation
    163   public String getUrl() {
    164     return originalUrl;
    165   }
    166 
    167   /**
    168    * @return the additional Http headers that in the same request with last loaded url
    169    */
    170   public Map<String, String> getLastAdditionalHttpHeaders() {
    171     return lastAdditionalHttpHeaders;
    172   }
    173 
    174   @Implementation
    175   public WebSettings getSettings() {
    176     return webSettings;
    177   }
    178 
    179   @Implementation
    180   public void setWebViewClient(WebViewClient client) {
    181     webViewClient = client;
    182   }
    183 
    184   @Implementation
    185   public void setWebChromeClient(WebChromeClient client) {
    186     webChromeClient = client;
    187   }
    188 
    189   public WebViewClient getWebViewClient() {
    190     return webViewClient;
    191   }
    192 
    193   @Implementation
    194   public void addJavascriptInterface(Object obj, String interfaceName) {
    195     javascriptInterfaces.put(interfaceName, obj);
    196   }
    197 
    198   public Object getJavascriptInterface(String interfaceName) {
    199     return javascriptInterfaces.get(interfaceName);
    200   }
    201 
    202   @Implementation
    203   public void clearCache(boolean includeDiskFiles) {
    204     clearCacheCalled = true;
    205     clearCacheIncludeDiskFiles = includeDiskFiles;
    206   }
    207 
    208   public boolean wasClearCacheCalled() {
    209     return clearCacheCalled;
    210   }
    211 
    212   public boolean didClearCacheIncludeDiskFiles() {
    213     return clearCacheIncludeDiskFiles;
    214   }
    215 
    216   @Implementation
    217   public void clearFormData() {
    218     clearFormDataCalled = true;
    219   }
    220 
    221   public boolean wasClearFormDataCalled() {
    222     return clearFormDataCalled;
    223   }
    224 
    225   @Implementation
    226   public void clearHistory() {
    227     clearHistoryCalled = true;
    228     history.clear();
    229   }
    230 
    231   public boolean wasClearHistoryCalled() {
    232     return clearHistoryCalled;
    233   }
    234 
    235   @Implementation
    236   public void clearView() {
    237     clearViewCalled = true;
    238   }
    239 
    240   public boolean wasClearViewCalled() {
    241     return clearViewCalled;
    242   }
    243 
    244   @Implementation
    245   public void onPause(){
    246     onPauseCalled = true;
    247   }
    248 
    249   public boolean wasOnPauseCalled() {
    250     return onPauseCalled;
    251   }
    252 
    253   @Implementation
    254   public void onResume() {
    255     onResumeCalled = true;
    256   }
    257 
    258   public boolean wasOnResumeCalled() {
    259     return onResumeCalled;
    260   }
    261 
    262   @Implementation
    263   public void destroy() {
    264     destroyCalled = true;
    265   }
    266 
    267   public boolean wasDestroyCalled() {
    268     return destroyCalled;
    269   }
    270 
    271   @Override @Implementation
    272   public void post(Runnable action) {
    273     action.run();
    274     runFlag = true;
    275   }
    276 
    277   public boolean getRunFlag() {
    278     return runFlag;
    279   }
    280 
    281 
    282   /**
    283    * @return webChromeClient
    284    */
    285   public WebChromeClient getWebChromeClient() {
    286     return webChromeClient;
    287   }
    288 
    289   @Implementation
    290   public boolean canGoBack() {
    291     // TODO: Remove the canGoBack check when setCanGoBack is deleted.
    292     if (canGoBackIsSet) {
    293       return canGoBack;
    294     }
    295     return history.size() > 1;
    296   }
    297 
    298   @Implementation
    299   public void goBack() {
    300     if (canGoBack()) {
    301       goBackInvocations++;
    302       // TODO: Delete this when setCanGoBack is deleted, since this creates two different behavior
    303       // paths.
    304       if (canGoBackIsSet) {
    305         return;
    306       }
    307       history.remove(0);
    308       if (!history.isEmpty()) {
    309         originalUrl = history.get(0);
    310       }
    311     }
    312   }
    313 
    314   @Implementation
    315   public static String findAddress(String addr) {
    316     return null;
    317   }
    318 
    319   @Implementation(minSdk = Build.VERSION_CODES.KITKAT)
    320   public void evaluateJavascript(String script, ValueCallback<String> callback) {
    321     this.lastEvaluatedJavascript = script;
    322   }
    323 
    324   public String getLastEvaluatedJavascript() {
    325     return lastEvaluatedJavascript;
    326   }
    327 
    328   /**
    329    * Sets the value to return from {@code android.webkit.WebView#canGoBack()}
    330    *
    331    * @param canGoBack Value to return from {@code android.webkit.WebView#canGoBack()}
    332    * @deprecated Do not depend on this method as it will be removed in a future update. The
    333    *     preferered method is to populate a fake web history to use for going back.
    334    */
    335   @Deprecated
    336   public void setCanGoBack(boolean canGoBack) {
    337     canGoBackIsSet = true;
    338     this.canGoBack = canGoBack;
    339   }
    340 
    341   /**
    342    * @return goBackInvocations the number of times {@code android.webkit.WebView#goBack()} was
    343    *     invoked
    344    */
    345   public int getGoBackInvocations() {
    346     return goBackInvocations;
    347   }
    348 
    349   public LoadData getLastLoadData() {
    350     return lastLoadData;
    351   }
    352 
    353   public LoadDataWithBaseURL getLastLoadDataWithBaseURL() {
    354     return lastLoadDataWithBaseURL;
    355   }
    356 
    357   public static void setWebContentsDebuggingEnabled(boolean enabled) { }
    358 
    359   public static class LoadDataWithBaseURL {
    360     public final String baseUrl;
    361     public final String data;
    362     public final String mimeType;
    363     public final String encoding;
    364     public final String historyUrl;
    365 
    366     public LoadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String historyUrl) {
    367       this.baseUrl = baseUrl;
    368       this.data = data;
    369       this.mimeType = mimeType;
    370       this.encoding = encoding;
    371       this.historyUrl = historyUrl;
    372     }
    373   }
    374 
    375   public static class LoadData {
    376     public final String data;
    377     public final String mimeType;
    378     public final String encoding;
    379 
    380     public LoadData(String data, String mimeType, String encoding) {
    381       this.data = data;
    382       this.mimeType = mimeType;
    383       this.encoding = encoding;
    384     }
    385   }
    386 }
    387