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