1 package org.robolectric.shadows; 2 3 import android.annotation.ColorInt; 4 import android.content.pm.PackageInfo; 5 import android.graphics.Bitmap; 6 import android.os.Build; 7 import android.os.Build.VERSION; 8 import android.os.Build.VERSION_CODES; 9 import android.os.Bundle; 10 import android.os.Handler; 11 import android.os.Looper; 12 import android.view.ViewGroup.LayoutParams; 13 import android.webkit.DownloadListener; 14 import android.webkit.ValueCallback; 15 import android.webkit.WebBackForwardList; 16 import android.webkit.WebChromeClient; 17 import android.webkit.WebHistoryItem; 18 import android.webkit.WebMessagePort; 19 import android.webkit.WebSettings; 20 import android.webkit.WebView; 21 import android.webkit.WebView.HitTestResult; 22 import android.webkit.WebViewClient; 23 import android.webkit.WebViewFactoryProvider; 24 import com.google.common.collect.ImmutableList; 25 import java.lang.reflect.Field; 26 import java.lang.reflect.InvocationHandler; 27 import java.lang.reflect.Method; 28 import java.lang.reflect.Proxy; 29 import java.util.ArrayList; 30 import java.util.Collections; 31 import java.util.HashMap; 32 import java.util.List; 33 import java.util.Map; 34 import org.robolectric.annotation.HiddenApi; 35 import org.robolectric.annotation.Implementation; 36 import org.robolectric.annotation.Implements; 37 import org.robolectric.annotation.RealObject; 38 import org.robolectric.annotation.Resetter; 39 import org.robolectric.fakes.RoboWebMessagePort; 40 import org.robolectric.fakes.RoboWebSettings; 41 import org.robolectric.util.ReflectionHelpers; 42 43 @SuppressWarnings({"UnusedDeclaration"}) 44 @Implements(value = WebView.class) 45 public class ShadowWebView extends ShadowViewGroup { 46 @RealObject private WebView realWebView; 47 48 private static final String HISTORY_KEY = "ShadowWebView.History"; 49 private static final String HISTORY_INDEX_KEY = "ShadowWebView.HistoryIndex"; 50 51 private static PackageInfo packageInfo = null; 52 53 private List<RoboWebMessagePort[]> allCreatedPorts = new ArrayList<>(); 54 private String lastUrl; 55 private Map<String, String> lastAdditionalHttpHeaders; 56 private HashMap<String, Object> javascriptInterfaces = new HashMap<>(); 57 private WebSettings webSettings = new RoboWebSettings(); 58 private WebViewClient webViewClient = null; 59 private boolean clearCacheCalled = false; 60 private boolean clearCacheIncludeDiskFiles = false; 61 private boolean clearFormDataCalled = false; 62 private boolean clearHistoryCalled = false; 63 private boolean clearViewCalled = false; 64 private boolean destroyCalled = false; 65 private boolean onPauseCalled = false; 66 private boolean onResumeCalled = false; 67 private WebChromeClient webChromeClient; 68 private boolean canGoBack; 69 private Bitmap currentFavicon = null; 70 private int goBackInvocations = 0; 71 private int goForwardInvocations = 0; 72 private int reloadInvocations = 0; 73 private LoadData lastLoadData; 74 private LoadDataWithBaseURL lastLoadDataWithBaseURL; 75 private String originalUrl; 76 private int historyIndex = -1; 77 private ArrayList<String> history = new ArrayList<>(); 78 private String lastEvaluatedJavascript; 79 private ValueCallback<String> lastEvaluatedJavascriptCallback; 80 // TODO: Delete this when setCanGoBack is deleted. This is only used to determine which "path" we 81 // use when canGoBack or goBack is called. 82 private boolean canGoBackIsSet; 83 private PageLoadType pageLoadType = PageLoadType.UNDEFINED; 84 private HitTestResult hitTestResult = new HitTestResult(); 85 private int backgroundColor = 0; 86 private DownloadListener downloadListener; 87 private static WebViewFactoryProvider webViewFactoryProvider; 88 89 @HiddenApi 90 @Implementation getFactory()91 protected static WebViewFactoryProvider getFactory() { 92 if (webViewFactoryProvider == null) { 93 webViewFactoryProvider = ReflectionHelpers.createDeepProxy(WebViewFactoryProvider.class); 94 } 95 return webViewFactoryProvider; 96 } 97 98 @HiddenApi 99 @Implementation ensureProviderCreated()100 public void ensureProviderCreated() { 101 final ClassLoader classLoader = getClass().getClassLoader(); 102 Class<?> webViewProviderClass = getClassNamed("android.webkit.WebViewProvider"); 103 Field mProvider; 104 try { 105 mProvider = WebView.class.getDeclaredField("mProvider"); 106 mProvider.setAccessible(true); 107 if (mProvider.get(realView) == null) { 108 Object provider = 109 Proxy.newProxyInstance( 110 classLoader, 111 new Class[] {webViewProviderClass}, 112 new InvocationHandler() { 113 @Override 114 public Object invoke(Object proxy, Method method, Object[] args) 115 throws Throwable { 116 if (method.getName().equals("getViewDelegate") 117 || method.getName().equals("getScrollDelegate")) { 118 return Proxy.newProxyInstance( 119 classLoader, 120 new Class[] { 121 getClassNamed("android.webkit.WebViewProvider$ViewDelegate"), 122 getClassNamed("android.webkit.WebViewProvider$ScrollDelegate") 123 }, 124 new InvocationHandler() { 125 @Override 126 public Object invoke(Object proxy, Method method, Object[] args) 127 throws Throwable { 128 return nullish(method); 129 } 130 }); 131 } 132 133 return nullish(method); 134 } 135 }); 136 mProvider.set(realView, provider); 137 } 138 } catch (NoSuchFieldException | IllegalAccessException e) { 139 throw new RuntimeException(e); 140 } 141 } 142 143 @Implementation setLayoutParams(LayoutParams params)144 protected void setLayoutParams(LayoutParams params) { 145 ReflectionHelpers.setField(realWebView, "mLayoutParams", params); 146 } 147 nullish(Method method)148 private Object nullish(Method method) { 149 Class<?> returnType = method.getReturnType(); 150 if (returnType.equals(long.class) 151 || returnType.equals(double.class) 152 || returnType.equals(int.class) 153 || returnType.equals(float.class) 154 || returnType.equals(short.class) 155 || returnType.equals(byte.class)) return 0; 156 if (returnType.equals(char.class)) return '\0'; 157 if (returnType.equals(boolean.class)) return false; 158 return null; 159 } 160 getClassNamed(String className)161 private Class<?> getClassNamed(String className) { 162 try { 163 return getClass().getClassLoader().loadClass(className); 164 } catch (ClassNotFoundException e) { 165 throw new RuntimeException(e); 166 } 167 } 168 169 @Implementation loadUrl(String url)170 protected void loadUrl(String url) { 171 loadUrl(url, null); 172 } 173 174 /** 175 * Fires a request to load the given {@code url} in WebView. 176 * 177 * <p>The {@code url} is is not added to the history until {@link #pushEntryToHistory(String)} is 178 * called. If you want to simulate a redirect you can pass the redirect URL to {@link 179 * #pushEntryToHistory(String)}. 180 */ 181 @Implementation loadUrl(String url, Map<String, String> additionalHttpHeaders)182 protected void loadUrl(String url, Map<String, String> additionalHttpHeaders) { 183 originalUrl = url; 184 lastUrl = url; 185 186 if (additionalHttpHeaders != null) { 187 this.lastAdditionalHttpHeaders = Collections.unmodifiableMap(additionalHttpHeaders); 188 } else { 189 this.lastAdditionalHttpHeaders = null; 190 } 191 192 performPageLoadType(url); 193 } 194 195 @Implementation loadDataWithBaseURL( String baseUrl, String data, String mimeType, String encoding, String historyUrl)196 protected void loadDataWithBaseURL( 197 String baseUrl, String data, String mimeType, String encoding, String historyUrl) { 198 if (historyUrl != null) { 199 originalUrl = historyUrl; 200 } 201 lastLoadDataWithBaseURL = 202 new LoadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl); 203 204 performPageLoadType(baseUrl); 205 } 206 207 @Implementation loadData(String data, String mimeType, String encoding)208 protected void loadData(String data, String mimeType, String encoding) { 209 lastLoadData = new LoadData(data, mimeType, encoding); 210 211 performPageLoadType(data); 212 } 213 214 /** 215 * Pushes an entry to the history with the given {@code url}. 216 * 217 * <p>This method can be used after a {@link #loadUrl(String)} call to push that navigation into 218 * the history. This matches the prod behaviour of WebView, a navigation is never committed to 219 * history inline and can take an arbitrary amount of time depending on the network connection. 220 * Notice that the given {@code url} does not need to match that of the {@link #loadUrl(String)} 221 * as URL can be changed e.g. through server-side redirects without WebView being notified by the 222 * time it is committed. 223 * 224 * <p>This method can also be used to simulate navigations started by user interaction, as these 225 * would still add an entry to the history themselves. 226 * 227 * <p>If there are any entries ahead of the current index (for forward navigation) these are 228 * removed. 229 */ pushEntryToHistory(String url)230 public void pushEntryToHistory(String url) { 231 history.subList(historyIndex + 1, history.size()).clear(); 232 233 history.add(url); 234 historyIndex++; 235 236 originalUrl = url; 237 } 238 239 /** 240 * Performs no callbacks on {@link WebViewClient} and {@link WebChromeClient} when any of {@link 241 * #loadUrl}, {@link loadData} or {@link #loadDataWithBaseURL} is called. 242 */ performNoPageLoadClientCallbacks()243 public void performNoPageLoadClientCallbacks() { 244 this.pageLoadType = PageLoadType.UNDEFINED; 245 } 246 247 /** 248 * Performs callbacks on {@link WebViewClient} and {@link WebChromeClient} that simulates a 249 * successful page load when any of {@link #loadUrl}, {@link loadData} or {@link 250 * #loadDataWithBaseURL} is called. 251 */ performSuccessfulPageLoadClientCallbacks()252 public void performSuccessfulPageLoadClientCallbacks() { 253 this.pageLoadType = PageLoadType.SUCCESS; 254 } 255 performPageLoadType(String url)256 private void performPageLoadType(String url) { 257 switch (pageLoadType) { 258 case SUCCESS: 259 performSuccessfulPageLoad(url); 260 break; 261 case UNDEFINED: 262 break; 263 } 264 } 265 performSuccessfulPageLoad(String url)266 private void performSuccessfulPageLoad(String url) { 267 new Handler(Looper.getMainLooper()) 268 .post( 269 () -> { 270 if (webChromeClient != null) { 271 webChromeClient.onProgressChanged(realWebView, 10); 272 } 273 if (webViewClient != null) { 274 webViewClient.onPageStarted(realWebView, url, /* favicon= */ null); 275 } 276 if (webChromeClient != null) { 277 webChromeClient.onProgressChanged(realWebView, 40); 278 webChromeClient.onProgressChanged(realWebView, 80); 279 } 280 if (webViewClient != null && VERSION.SDK_INT >= 23) { 281 webViewClient.onPageCommitVisible(realWebView, url); 282 } 283 if (webChromeClient != null) { 284 webChromeClient.onReceivedTitle(realWebView, url); 285 webChromeClient.onProgressChanged(realWebView, 100); 286 } 287 if (webViewClient != null) { 288 webViewClient.onPageFinished(realWebView, url); 289 } 290 }); 291 } 292 293 /** 294 * @return the last loaded url 295 */ getLastLoadedUrl()296 public String getLastLoadedUrl() { 297 return lastUrl; 298 } 299 300 @Implementation getOriginalUrl()301 protected String getOriginalUrl() { 302 return originalUrl; 303 } 304 305 @Implementation getUrl()306 protected String getUrl() { 307 return originalUrl; 308 } 309 310 @Implementation getTitle()311 protected String getTitle() { 312 return originalUrl; 313 } 314 315 /** 316 * @return the additional Http headers that in the same request with last loaded url 317 */ getLastAdditionalHttpHeaders()318 public Map<String, String> getLastAdditionalHttpHeaders() { 319 return lastAdditionalHttpHeaders; 320 } 321 322 @Implementation getSettings()323 protected WebSettings getSettings() { 324 return webSettings; 325 } 326 327 @Implementation setWebViewClient(WebViewClient client)328 protected void setWebViewClient(WebViewClient client) { 329 webViewClient = client; 330 } 331 332 @Implementation setWebChromeClient(WebChromeClient client)333 protected void setWebChromeClient(WebChromeClient client) { 334 webChromeClient = client; 335 } 336 337 @Implementation(minSdk = VERSION_CODES.O) getWebViewClient()338 public WebViewClient getWebViewClient() { 339 return webViewClient; 340 } 341 342 @Implementation addJavascriptInterface(Object obj, String interfaceName)343 protected void addJavascriptInterface(Object obj, String interfaceName) { 344 javascriptInterfaces.put(interfaceName, obj); 345 } 346 getJavascriptInterface(String interfaceName)347 public Object getJavascriptInterface(String interfaceName) { 348 return javascriptInterfaces.get(interfaceName); 349 } 350 351 @Implementation removeJavascriptInterface(String name)352 protected void removeJavascriptInterface(String name) { 353 javascriptInterfaces.remove(name); 354 } 355 356 @Implementation(minSdk = Build.VERSION_CODES.M) createWebMessageChannel()357 protected WebMessagePort[] createWebMessageChannel() { 358 RoboWebMessagePort[] ports = RoboWebMessagePort.createPair(); 359 allCreatedPorts.add(ports); 360 return ports; 361 } 362 getCreatedPorts()363 public List<RoboWebMessagePort[]> getCreatedPorts() { 364 return ImmutableList.copyOf(allCreatedPorts); 365 } 366 367 @Implementation clearCache(boolean includeDiskFiles)368 protected void clearCache(boolean includeDiskFiles) { 369 clearCacheCalled = true; 370 clearCacheIncludeDiskFiles = includeDiskFiles; 371 } 372 wasClearCacheCalled()373 public boolean wasClearCacheCalled() { 374 return clearCacheCalled; 375 } 376 didClearCacheIncludeDiskFiles()377 public boolean didClearCacheIncludeDiskFiles() { 378 return clearCacheIncludeDiskFiles; 379 } 380 381 @Implementation clearFormData()382 protected void clearFormData() { 383 clearFormDataCalled = true; 384 } 385 wasClearFormDataCalled()386 public boolean wasClearFormDataCalled() { 387 return clearFormDataCalled; 388 } 389 390 @Implementation clearHistory()391 protected void clearHistory() { 392 clearHistoryCalled = true; 393 history.clear(); 394 historyIndex = -1; 395 } 396 wasClearHistoryCalled()397 public boolean wasClearHistoryCalled() { 398 return clearHistoryCalled; 399 } 400 401 @Implementation reload()402 protected void reload() { 403 reloadInvocations++; 404 } 405 406 /** Returns the number of times {@code android.webkit.WebView#reload()} was invoked */ getReloadInvocations()407 public int getReloadInvocations() { 408 return reloadInvocations; 409 } 410 411 @Implementation clearView()412 protected void clearView() { 413 clearViewCalled = true; 414 } 415 wasClearViewCalled()416 public boolean wasClearViewCalled() { 417 return clearViewCalled; 418 } 419 420 @Implementation onPause()421 protected void onPause() { 422 onPauseCalled = true; 423 } 424 wasOnPauseCalled()425 public boolean wasOnPauseCalled() { 426 return onPauseCalled; 427 } 428 429 @Implementation onResume()430 protected void onResume() { 431 onResumeCalled = true; 432 } 433 wasOnResumeCalled()434 public boolean wasOnResumeCalled() { 435 return onResumeCalled; 436 } 437 438 @Implementation destroy()439 protected void destroy() { 440 destroyCalled = true; 441 } 442 wasDestroyCalled()443 public boolean wasDestroyCalled() { 444 return destroyCalled; 445 } 446 447 /** 448 * @return webChromeClient 449 */ 450 @Implementation(minSdk = VERSION_CODES.O) getWebChromeClient()451 public WebChromeClient getWebChromeClient() { 452 return webChromeClient; 453 } 454 455 @Implementation canGoBack()456 protected boolean canGoBack() { 457 // TODO: Remove the canGoBack check when setCanGoBack is deleted. 458 if (canGoBackIsSet) { 459 return canGoBack; 460 } 461 return historyIndex > 0; 462 } 463 464 @Implementation canGoForward()465 protected boolean canGoForward() { 466 return historyIndex < history.size() - 1; 467 } 468 469 @Implementation goBack()470 protected void goBack() { 471 if (canGoBack()) { 472 goBackInvocations++; 473 // TODO: Delete this when setCanGoBack is deleted, since this creates two different behavior 474 // paths. 475 if (canGoBackIsSet) { 476 return; 477 } 478 historyIndex--; 479 originalUrl = history.get(historyIndex); 480 } 481 } 482 483 @Implementation goForward()484 protected void goForward() { 485 if (canGoForward()) { 486 goForwardInvocations++; 487 historyIndex++; 488 originalUrl = history.get(historyIndex); 489 } 490 } 491 492 @Implementation goBackOrForward(int steps)493 protected void goBackOrForward(int steps) { 494 if (steps == 0) { 495 return; 496 } 497 if (steps > 0) { 498 while (steps-- > 0) { 499 goForward(); 500 } 501 return; 502 } 503 504 while (steps++ < 0) { 505 goBack(); 506 } 507 } 508 509 @Implementation copyBackForwardList()510 protected WebBackForwardList copyBackForwardList() { 511 return new BackForwardList(history, historyIndex); 512 } 513 514 @Implementation findAddress(String addr)515 protected static String findAddress(String addr) { 516 return null; 517 } 518 519 /** 520 * Overrides the system implementation for getting the WebView package. 521 * 522 * <p>Returns null by default, but this can be changed with {@code #setCurrentWebviewPackage()}. 523 */ 524 @Implementation(minSdk = Build.VERSION_CODES.O) getCurrentWebViewPackage()525 protected static PackageInfo getCurrentWebViewPackage() { 526 return packageInfo; 527 } 528 529 /** Sets the value to return from {@code #getCurrentWebviewPackage()}. */ setCurrentWebViewPackage(PackageInfo webViewPackageInfo)530 public static void setCurrentWebViewPackage(PackageInfo webViewPackageInfo) { 531 packageInfo = webViewPackageInfo; 532 } 533 534 /** Gets the favicon for the current page set by {@link #setFavicon}. */ 535 @Implementation getFavicon()536 protected Bitmap getFavicon() { 537 return currentFavicon; 538 } 539 540 /** Sets the favicon to return from {@link #getFavicon}. */ setFavicon(Bitmap favicon)541 public void setFavicon(Bitmap favicon) { 542 currentFavicon = favicon; 543 } 544 545 @Implementation evaluateJavascript(String script, ValueCallback<String> callback)546 protected void evaluateJavascript(String script, ValueCallback<String> callback) { 547 this.lastEvaluatedJavascript = script; 548 this.lastEvaluatedJavascriptCallback = callback; 549 } 550 551 /** 552 * Returns the last evaluated Javascript value provided to {@link #evaluateJavascript(String, 553 * ValueCallback)} or null if the method has not been called. 554 */ getLastEvaluatedJavascript()555 public String getLastEvaluatedJavascript() { 556 return lastEvaluatedJavascript; 557 } 558 559 /** 560 * Returns the last callback value provided to {@link #evaluateJavascript(String, ValueCallback)} 561 * or null if the method has not been called. 562 */ getLastEvaluatedJavascriptCallback()563 public ValueCallback<String> getLastEvaluatedJavascriptCallback() { 564 return lastEvaluatedJavascriptCallback; 565 } 566 567 /** 568 * Sets the value to return from {@code android.webkit.WebView#canGoBack()} 569 * 570 * @param canGoBack Value to return from {@code android.webkit.WebView#canGoBack()} 571 * @deprecated Do not depend on this method as it will be removed in a future update. The 572 * preferred method is to populate a fake web history to use for going back. 573 */ 574 @Deprecated setCanGoBack(boolean canGoBack)575 public void setCanGoBack(boolean canGoBack) { 576 canGoBackIsSet = true; 577 this.canGoBack = canGoBack; 578 } 579 580 /** Returns the number of times {@code android.webkit.WebView#goBack()} was invoked. */ getGoBackInvocations()581 public int getGoBackInvocations() { 582 return goBackInvocations; 583 } 584 585 /** Returns the number of times {@code android.webkit.WebView#goForward()} was invoked. */ getGoForwardInvocations()586 public int getGoForwardInvocations() { 587 return goForwardInvocations; 588 } 589 getLastLoadData()590 public LoadData getLastLoadData() { 591 return lastLoadData; 592 } 593 getLastLoadDataWithBaseURL()594 public LoadDataWithBaseURL getLastLoadDataWithBaseURL() { 595 return lastLoadDataWithBaseURL; 596 } 597 598 @Implementation saveState(Bundle outState)599 protected WebBackForwardList saveState(Bundle outState) { 600 if (history.size() > 0) { 601 outState.putStringArrayList(HISTORY_KEY, history); 602 outState.putInt(HISTORY_INDEX_KEY, historyIndex); 603 } 604 return new BackForwardList(history, historyIndex); 605 } 606 607 @Implementation restoreState(Bundle inState)608 protected WebBackForwardList restoreState(Bundle inState) { 609 history = inState.getStringArrayList(HISTORY_KEY); 610 if (history == null) { 611 history = new ArrayList<>(); 612 historyIndex = -1; 613 } else { 614 historyIndex = inState.getInt(HISTORY_INDEX_KEY); 615 } 616 617 if (history.size() > 0) { 618 originalUrl = history.get(historyIndex); 619 lastUrl = history.get(historyIndex); 620 return new BackForwardList(history, historyIndex); 621 } 622 return null; 623 } 624 625 @Implementation getHitTestResult()626 protected HitTestResult getHitTestResult() { 627 return hitTestResult; 628 } 629 630 /** Creates an instance of {@link HitTestResult}. */ createHitTestResult(int type, String extra)631 public static HitTestResult createHitTestResult(int type, String extra) { 632 HitTestResult hitTestResult = new HitTestResult(); 633 hitTestResult.setType(type); 634 hitTestResult.setExtra(extra); 635 return hitTestResult; 636 } 637 638 /** Sets the {@link HitTestResult} that should be returned from {@link #getHitTestResult()}. */ setHitTestResult(HitTestResult hitTestResult)639 public void setHitTestResult(HitTestResult hitTestResult) { 640 this.hitTestResult = hitTestResult; 641 } 642 643 @Resetter reset()644 public static void reset() { 645 packageInfo = null; 646 } 647 648 @Implementation setWebContentsDebuggingEnabled(boolean enabled)649 public static void setWebContentsDebuggingEnabled(boolean enabled) {} 650 651 /** 652 * Sets the {@link android.graphics.Color} int that should be returned from {@link 653 * #getBackgroundColor}. 654 * 655 * <p>WebView uses the background color set by the {@link 656 * android.webkit.WebView#setBackgroundColor} method to internally tint the background color of 657 * web pages until they are drawn. The way this API works is completely independent of the {@link 658 * android.view.View#setBackgroundColor} method and it interacts directly with WebView renderers. 659 * Tests can access the set background color using the {@link #getBackgroundColor} method. 660 */ 661 @Implementation setBackgroundColor(@olorInt int backgroundColor)662 protected void setBackgroundColor(@ColorInt int backgroundColor) { 663 this.backgroundColor = backgroundColor; 664 } 665 666 /** 667 * Returns the {@link android.graphics.Color} int that has been set by {@link 668 * #setBackgroundColor}. 669 */ getBackgroundColor()670 public int getBackgroundColor() { 671 return backgroundColor; 672 } 673 674 @Implementation setDownloadListener(DownloadListener downloadListener)675 protected void setDownloadListener(DownloadListener downloadListener) { 676 this.downloadListener = downloadListener; 677 } 678 679 /** Returns the {@link DownloadListener} set with {@link #setDownloadListener}, if any. */ getDownloadListener()680 public DownloadListener getDownloadListener() { 681 return this.downloadListener; 682 } 683 684 public static class LoadDataWithBaseURL { 685 public final String baseUrl; 686 public final String data; 687 public final String mimeType; 688 public final String encoding; 689 public final String historyUrl; 690 LoadDataWithBaseURL( String baseUrl, String data, String mimeType, String encoding, String historyUrl)691 public LoadDataWithBaseURL( 692 String baseUrl, String data, String mimeType, String encoding, String historyUrl) { 693 this.baseUrl = baseUrl; 694 this.data = data; 695 this.mimeType = mimeType; 696 this.encoding = encoding; 697 this.historyUrl = historyUrl; 698 } 699 } 700 701 public static class LoadData { 702 public final String data; 703 public final String mimeType; 704 public final String encoding; 705 LoadData(String data, String mimeType, String encoding)706 public LoadData(String data, String mimeType, String encoding) { 707 this.data = data; 708 this.mimeType = mimeType; 709 this.encoding = encoding; 710 } 711 } 712 713 /** 714 * Defines a type of page load which is associated with a certain order of {@link WebViewClient} 715 * and {@link WebChromeClient} callbacks. 716 * 717 * <p>A page load is triggered either using {@link #loadUrl}, {@link loadData} or {@link 718 * loadDataWithBaseURL}. 719 */ 720 private enum PageLoadType { 721 /** Default type, triggers no {@link WebViewClient} or {@link WebChromeClient} callbacks. */ 722 UNDEFINED, 723 /** 724 * Represents a successful page load, which triggers all the associated {@link WebViewClient} or 725 * {@link WebChromeClient} callbacks from {@code onPageStarted} until {@code onPageFinished} 726 * without any error. 727 */ 728 SUCCESS 729 } 730 731 private static class BackForwardList extends WebBackForwardList { 732 private final ArrayList<String> history; 733 private final int index; 734 BackForwardList(ArrayList<String> history, int index)735 public BackForwardList(ArrayList<String> history, int index) { 736 this.history = (ArrayList<String>) history.clone(); 737 this.index = index; 738 } 739 740 @Override getCurrentIndex()741 public int getCurrentIndex() { 742 return index; 743 } 744 745 @Override getSize()746 public int getSize() { 747 return history.size(); 748 } 749 750 @Override getCurrentItem()751 public HistoryItem getCurrentItem() { 752 if (history.isEmpty()) { 753 return null; 754 } 755 756 return new HistoryItem(history.get(getCurrentIndex())); 757 } 758 759 @Override getItemAtIndex(int index)760 public HistoryItem getItemAtIndex(int index) { 761 return new HistoryItem(history.get(index)); 762 } 763 764 @Override clone()765 protected WebBackForwardList clone() { 766 return new BackForwardList(history, index); 767 } 768 } 769 770 private static class HistoryItem extends WebHistoryItem { 771 private final String url; 772 HistoryItem(String url)773 public HistoryItem(String url) { 774 this.url = url; 775 } 776 777 @Override getId()778 public int getId() { 779 return url.hashCode(); 780 } 781 782 @Override getFavicon()783 public Bitmap getFavicon() { 784 return null; 785 } 786 787 @Override getOriginalUrl()788 public String getOriginalUrl() { 789 return url; 790 } 791 792 @Override getTitle()793 public String getTitle() { 794 return url; 795 } 796 797 @Override getUrl()798 public String getUrl() { 799 return url; 800 } 801 802 @Override clone()803 protected HistoryItem clone() { 804 return new HistoryItem(url); 805 } 806 } 807 } 808