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