1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.webkit.cts; 18 19 import android.cts.util.PollingCheck; 20 import android.graphics.Bitmap; 21 import android.graphics.Picture; 22 import android.graphics.Rect; 23 import android.os.Bundle; 24 import android.os.Looper; 25 import android.os.Message; 26 import android.os.SystemClock; 27 import android.test.InstrumentationTestCase; 28 import android.util.DisplayMetrics; 29 import android.view.View; 30 import android.webkit.DownloadListener; 31 import android.webkit.WebBackForwardList; 32 import android.webkit.WebChromeClient; 33 import android.webkit.WebSettings; 34 import android.webkit.WebView; 35 import android.webkit.WebView.HitTestResult; 36 import android.webkit.WebView.PictureListener; 37 import android.webkit.WebViewClient; 38 39 import junit.framework.Assert; 40 41 import java.io.File; 42 43 44 /** 45 * Many tests need to run WebView code in the UI thread. This class 46 * wraps a WebView so that calls are ensured to arrive on the UI thread. 47 * 48 * All methods may be run on either the UI thread or test thread. 49 */ 50 public class WebViewOnUiThread { 51 /** 52 * The maximum time, in milliseconds (10 seconds) to wait for a load 53 * to be triggered. 54 */ 55 private static final long LOAD_TIMEOUT = 10000; 56 57 /** 58 * Set to true after onPageFinished is called. 59 */ 60 private boolean mLoaded; 61 62 /** 63 * Set to true after onNewPicture is called. Reset when onPageStarted 64 * is called. 65 */ 66 private boolean mNewPicture; 67 68 /** 69 * The progress, in percentage, of the page load. Valid values are between 70 * 0 and 100. 71 */ 72 private int mProgress; 73 74 /** 75 * The test that this class is being used in. Used for runTestOnUiThread. 76 */ 77 private InstrumentationTestCase mTest; 78 79 /** 80 * The WebView that calls will be made on. 81 */ 82 private WebView mWebView; 83 84 /** 85 * Initializes the webView with a WebViewClient, WebChromeClient, 86 * and PictureListener to prepare for loadUrlAndWaitForCompletion. 87 * 88 * A new WebViewOnUiThread should be called during setUp so as to 89 * reinitialize between calls. 90 * 91 * @param test The test in which this is being run. 92 * @param webView The webView that the methods should call. 93 * @see loadUrlAndWaitForCompletion 94 */ 95 public WebViewOnUiThread(InstrumentationTestCase test, WebView webView) { 96 mTest = test; 97 mWebView = webView; 98 final WebViewClient webViewClient = new WaitForLoadedClient(this); 99 final WebChromeClient webChromeClient = new WaitForProgressClient(this); 100 runOnUiThread(new Runnable() { 101 @Override 102 public void run() { 103 mWebView.setWebViewClient(webViewClient); 104 mWebView.setWebChromeClient(webChromeClient); 105 mWebView.setPictureListener(new WaitForNewPicture()); 106 } 107 }); 108 } 109 110 /** 111 * Called after a test is complete and the WebView should be disengaged from 112 * the tests. 113 */ 114 public void cleanUp() { 115 clearHistory(); 116 clearCache(true); 117 setPictureListener(null); 118 setWebChromeClient(null); 119 setWebViewClient(null); 120 } 121 122 /** 123 * Called from WaitForNewPicture, this is used to indicate that 124 * the page has been drawn. 125 */ 126 synchronized public void onNewPicture() { 127 mNewPicture = true; 128 this.notifyAll(); 129 } 130 131 /** 132 * Called from WaitForLoadedClient, this is used to clear the picture 133 * draw state so that draws before the URL begins loading don't count. 134 */ 135 synchronized public void onPageStarted() { 136 mNewPicture = false; // Earlier paints won't count. 137 } 138 139 /** 140 * Called from WaitForLoadedClient, this is used to indicate that 141 * the page is loaded, but not drawn yet. 142 */ 143 synchronized public void onPageFinished() { 144 mLoaded = true; 145 this.notifyAll(); 146 } 147 148 /** 149 * Called from the WebChrome client, this sets the current progress 150 * for a page. 151 * @param progress The progress made so far between 0 and 100. 152 */ 153 synchronized public void onProgressChanged(int progress) { 154 mProgress = progress; 155 this.notifyAll(); 156 } 157 158 public void setWebViewClient(final WebViewClient webViewClient) { 159 runOnUiThread(new Runnable() { 160 @Override 161 public void run() { 162 mWebView.setWebViewClient(webViewClient); 163 } 164 }); 165 } 166 167 public void setWebChromeClient(final WebChromeClient webChromeClient) { 168 runOnUiThread(new Runnable() { 169 @Override 170 public void run() { 171 mWebView.setWebChromeClient(webChromeClient); 172 } 173 }); 174 } 175 176 public void setPictureListener(final PictureListener pictureListener) { 177 runOnUiThread(new Runnable() { 178 @Override 179 public void run() { 180 mWebView.setPictureListener(pictureListener); 181 } 182 }); 183 } 184 185 public void setDownloadListener(final DownloadListener listener) { 186 runOnUiThread(new Runnable() { 187 @Override 188 public void run() { 189 mWebView.setDownloadListener(listener); 190 } 191 }); 192 } 193 194 public void setBackgroundColor(final int color) { 195 runOnUiThread(new Runnable() { 196 @Override 197 public void run() { 198 mWebView.setBackgroundColor(color); 199 } 200 }); 201 } 202 203 public void clearCache(final boolean includeDiskFiles) { 204 runOnUiThread(new Runnable() { 205 @Override 206 public void run() { 207 mWebView.clearCache(includeDiskFiles); 208 } 209 }); 210 } 211 212 public void clearHistory() { 213 runOnUiThread(new Runnable() { 214 @Override 215 public void run() { 216 mWebView.clearHistory(); 217 } 218 }); 219 } 220 221 public void requestFocus() { 222 runOnUiThread(new Runnable() { 223 @Override 224 public void run() { 225 mWebView.requestFocus(); 226 } 227 }); 228 } 229 230 public void zoomIn() { 231 runOnUiThread(new Runnable() { 232 @Override 233 public void run() { 234 mWebView.zoomIn(); 235 } 236 }); 237 } 238 239 public void removeJavascriptInterface(final String interfaceName) { 240 runOnUiThread(new Runnable() { 241 @Override 242 public void run() { 243 mWebView.removeJavascriptInterface(interfaceName); 244 } 245 }); 246 } 247 248 public void addJavascriptInterface(final Object object, final String name) { 249 runOnUiThread(new Runnable() { 250 @Override 251 public void run() { 252 mWebView.addJavascriptInterface(object, name); 253 } 254 }); 255 } 256 257 public void flingScroll(final int vx, final int vy) { 258 runOnUiThread(new Runnable() { 259 @Override 260 public void run() { 261 mWebView.flingScroll(vx, vy); 262 } 263 }); 264 } 265 266 public void requestFocusNodeHref(final Message hrefMsg) { 267 runOnUiThread(new Runnable() { 268 @Override 269 public void run() { 270 mWebView.requestFocusNodeHref(hrefMsg); 271 } 272 }); 273 } 274 275 public void requestImageRef(final Message msg) { 276 runOnUiThread(new Runnable() { 277 @Override 278 public void run() { 279 mWebView.requestImageRef(msg); 280 } 281 }); 282 } 283 284 public void setInitialScale(final int scaleInPercent) { 285 runOnUiThread(new Runnable() { 286 @Override 287 public void run() { 288 mWebView.setInitialScale(scaleInPercent); 289 } 290 }); 291 } 292 293 public void clearSslPreferences() { 294 runOnUiThread(new Runnable() { 295 @Override 296 public void run() { 297 mWebView.clearSslPreferences(); 298 } 299 }); 300 } 301 302 public void resumeTimers() { 303 runOnUiThread(new Runnable() { 304 @Override 305 public void run() { 306 mWebView.resumeTimers(); 307 } 308 }); 309 } 310 311 public void findNext(final boolean forward) { 312 runOnUiThread(new Runnable() { 313 @Override 314 public void run() { 315 mWebView.findNext(forward); 316 } 317 }); 318 } 319 320 public void clearMatches() { 321 runOnUiThread(new Runnable() { 322 @Override 323 public void run() { 324 mWebView.clearMatches(); 325 } 326 }); 327 } 328 329 /** 330 * Calls loadUrl on the WebView and then waits onPageFinished, 331 * onNewPicture and onProgressChange to reach 100. 332 * Test fails if the load timeout elapses. 333 * @param url The URL to load. 334 */ 335 public void loadUrlAndWaitForCompletion(final String url) { 336 callAndWait(new Runnable() { 337 @Override 338 public void run() { 339 mWebView.loadUrl(url); 340 } 341 }); 342 } 343 344 public void loadDataAndWaitForCompletion(final String data, 345 final String mimeType, final String encoding) { 346 callAndWait(new Runnable() { 347 @Override 348 public void run() { 349 mWebView.loadData(data, mimeType, encoding); 350 } 351 }); 352 } 353 354 public void loadDataWithBaseURLAndWaitForCompletion(final String baseUrl, 355 final String data, final String mimeType, final String encoding, 356 final String historyUrl) { 357 callAndWait(new Runnable() { 358 @Override 359 public void run() { 360 mWebView.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, 361 historyUrl); 362 } 363 }); 364 } 365 366 /** 367 * Reloads a page and waits for it to complete reloading. Use reload 368 * if it is a form resubmission and the onFormResubmission responds 369 * by telling WebView not to resubmit it. 370 */ 371 public void reloadAndWaitForCompletion() { 372 callAndWait(new Runnable() { 373 @Override 374 public void run() { 375 mWebView.reload(); 376 } 377 }); 378 } 379 380 /** 381 * Reload the previous URL. Use reloadAndWaitForCompletion unless 382 * it is a form resubmission and the onFormResubmission responds 383 * by telling WebView not to resubmit it. 384 */ 385 public void reload() { 386 runOnUiThread(new Runnable() { 387 @Override 388 public void run() { 389 mWebView.reload(); 390 } 391 }); 392 } 393 394 /** 395 * Use this only when JavaScript causes a page load to wait for the 396 * page load to complete. Otherwise use loadUrlAndWaitForCompletion or 397 * similar functions. 398 */ 399 public void waitForLoadCompletion() { 400 if (isUiThread()) { 401 waitOnUiThread(); 402 } else { 403 waitOnTestThread(); 404 } 405 clearLoad(); 406 } 407 408 public String getTitle() { 409 return getValue(new ValueGetter<String>() { 410 @Override 411 public String capture() { 412 return mWebView.getTitle(); 413 } 414 }); 415 } 416 417 public WebSettings getSettings() { 418 return getValue(new ValueGetter<WebSettings>() { 419 @Override 420 public WebSettings capture() { 421 return mWebView.getSettings(); 422 } 423 }); 424 } 425 426 public WebBackForwardList copyBackForwardList() { 427 return getValue(new ValueGetter<WebBackForwardList>() { 428 @Override 429 public WebBackForwardList capture() { 430 return mWebView.copyBackForwardList(); 431 } 432 }); 433 } 434 435 public Bitmap getFavicon() { 436 return getValue(new ValueGetter<Bitmap>() { 437 @Override 438 public Bitmap capture() { 439 return mWebView.getFavicon(); 440 } 441 }); 442 } 443 444 public String getUrl() { 445 return getValue(new ValueGetter<String>() { 446 @Override 447 public String capture() { 448 return mWebView.getUrl(); 449 } 450 }); 451 } 452 453 public int getProgress() { 454 return getValue(new ValueGetter<Integer>() { 455 @Override 456 public Integer capture() { 457 return mWebView.getProgress(); 458 } 459 }); 460 } 461 462 public int getHeight() { 463 return getValue(new ValueGetter<Integer>() { 464 @Override 465 public Integer capture() { 466 return mWebView.getHeight(); 467 } 468 }); 469 } 470 471 public int getContentHeight() { 472 return getValue(new ValueGetter<Integer>() { 473 @Override 474 public Integer capture() { 475 return mWebView.getContentHeight(); 476 } 477 }); 478 } 479 480 public boolean savePicture(final Bundle b, final File dest) { 481 return getValue(new ValueGetter<Boolean>() { 482 @Override 483 public Boolean capture() { 484 return mWebView.savePicture(b, dest); 485 } 486 }); 487 } 488 489 public boolean pageUp(final boolean top) { 490 return getValue(new ValueGetter<Boolean>() { 491 @Override 492 public Boolean capture() { 493 return mWebView.pageUp(top); 494 } 495 }); 496 } 497 498 public boolean pageDown(final boolean bottom) { 499 return getValue(new ValueGetter<Boolean>() { 500 @Override 501 public Boolean capture() { 502 return mWebView.pageDown(bottom); 503 } 504 }); 505 } 506 507 public int[] getLocationOnScreen() { 508 final int[] location = new int[2]; 509 return getValue(new ValueGetter<int[]>() { 510 @Override 511 public int[] capture() { 512 mWebView.getLocationOnScreen(location); 513 return location; 514 } 515 }); 516 } 517 518 public float getScale() { 519 return getValue(new ValueGetter<Float>() { 520 @Override 521 public Float capture() { 522 return mWebView.getScale(); 523 } 524 }); 525 } 526 527 public boolean requestFocus(final int direction, 528 final Rect previouslyFocusedRect) { 529 return getValue(new ValueGetter<Boolean>() { 530 @Override 531 public Boolean capture() { 532 return mWebView.requestFocus(direction, previouslyFocusedRect); 533 } 534 }); 535 } 536 537 public HitTestResult getHitTestResult() { 538 return getValue(new ValueGetter<HitTestResult>() { 539 @Override 540 public HitTestResult capture() { 541 return mWebView.getHitTestResult(); 542 } 543 }); 544 } 545 546 public int getScrollX() { 547 return getValue(new ValueGetter<Integer>() { 548 @Override 549 public Integer capture() { 550 return mWebView.getScrollX(); 551 } 552 }); 553 } 554 555 public int getScrollY() { 556 return getValue(new ValueGetter<Integer>() { 557 @Override 558 public Integer capture() { 559 return mWebView.getScrollY(); 560 } 561 }); 562 } 563 564 public final DisplayMetrics getDisplayMetrics() { 565 return getValue(new ValueGetter<DisplayMetrics>() { 566 @Override 567 public DisplayMetrics capture() { 568 return mWebView.getContext().getResources().getDisplayMetrics(); 569 } 570 }); 571 } 572 573 public boolean requestChildRectangleOnScreen(final View child, 574 final Rect rect, 575 final boolean immediate) { 576 return getValue(new ValueGetter<Boolean>() { 577 @Override 578 public Boolean capture() { 579 return mWebView.requestChildRectangleOnScreen(child, rect, 580 immediate); 581 } 582 }); 583 } 584 585 public int findAll(final String find) { 586 return getValue(new ValueGetter<Integer>() { 587 @Override 588 public Integer capture() { 589 return mWebView.findAll(find); 590 } 591 }); 592 } 593 594 /** 595 * Helper for running code on the UI thread where an exception is 596 * a test failure. If this is already the UI thread then it runs 597 * the code immediately. 598 * 599 * @see runTestOnUiThread 600 * @param r The code to run in the UI thread 601 */ 602 public void runOnUiThread(Runnable r) { 603 try { 604 if (isUiThread()) { 605 r.run(); 606 } else { 607 mTest.runTestOnUiThread(r); 608 } 609 } catch (Throwable t) { 610 Assert.fail("Unexpected error while running on UI thread: " 611 + t.getMessage()); 612 } 613 } 614 615 /** 616 * Accessor for underlying WebView. 617 * @return The WebView being wrapped by this class. 618 */ 619 public WebView getWebView() { 620 return mWebView; 621 } 622 623 private <T> T getValue(ValueGetter<T> getter) { 624 runOnUiThread(getter); 625 return getter.getValue(); 626 } 627 628 private abstract class ValueGetter<T> implements Runnable { 629 private T mValue; 630 631 @Override 632 public void run() { 633 mValue = capture(); 634 } 635 636 protected abstract T capture(); 637 638 public T getValue() { 639 return mValue; 640 } 641 } 642 643 /** 644 * Returns true if the current thread is the UI thread based on the 645 * Looper. 646 */ 647 private static boolean isUiThread() { 648 return (Looper.myLooper() == Looper.getMainLooper()); 649 } 650 651 /** 652 * @return Whether or not the load has finished. 653 */ 654 private synchronized boolean isLoaded() { 655 return mLoaded && mNewPicture && mProgress == 100; 656 } 657 658 /** 659 * Makes a WebView call, waits for completion and then resets the 660 * load state in preparation for the next load call. 661 * @param call The call to make on the UI thread prior to waiting. 662 */ 663 private void callAndWait(Runnable call) { 664 synchronized (this) { 665 Assert.assertTrue("WebViewOnUiThread.load*AndWaitForCompletion calls " 666 + "may not be mixed with load* calls directly on WebView " 667 + "without calling waitForLoadCompletion after the load", 668 !mLoaded); 669 } 670 runOnUiThread(call); 671 waitForLoadCompletion(); 672 } 673 674 /** 675 * Called whenever a load has been completed so that a subsequent call to 676 * waitForLoadCompletion doesn't return immediately. 677 */ 678 synchronized private void clearLoad() { 679 mLoaded = false; 680 mNewPicture = false; 681 mProgress = 0; 682 } 683 684 /** 685 * Uses a polling mechanism, while pumping messages to check when the 686 * load completes. 687 */ 688 private void waitOnUiThread() { 689 new PollingCheck(LOAD_TIMEOUT) { 690 @Override 691 protected boolean check() { 692 pumpMessages(); 693 return isLoaded(); 694 } 695 }.run(); 696 } 697 698 /** 699 * Uses a wait/notify to check when the load completes. 700 */ 701 private synchronized void waitOnTestThread() { 702 try { 703 long waitEnd = SystemClock.uptimeMillis() + LOAD_TIMEOUT; 704 long timeRemaining = LOAD_TIMEOUT; 705 while (!isLoaded() && timeRemaining > 0) { 706 this.wait(timeRemaining); 707 timeRemaining = waitEnd - SystemClock.uptimeMillis(); 708 } 709 } catch (InterruptedException e) { 710 // We'll just drop out of the loop and fail 711 } 712 Assert.assertTrue("Load failed to complete before timeout", isLoaded()); 713 } 714 715 /** 716 * Pumps all currently-queued messages in the UI thread and then exits. 717 * This is useful to force processing while running tests in the UI thread. 718 */ 719 private void pumpMessages() { 720 class ExitLoopException extends RuntimeException { 721 } 722 723 // Force loop to exit when processing this. Loop.quit() doesn't 724 // work because this is the main Loop. 725 mWebView.getHandler().post(new Runnable() { 726 @Override 727 public void run() { 728 throw new ExitLoopException(); // exit loop! 729 } 730 }); 731 try { 732 // Pump messages until our message gets through. 733 Looper.loop(); 734 } catch (ExitLoopException e) { 735 } 736 } 737 738 /** 739 * A WebChromeClient used to capture the onProgressChanged for use 740 * in waitFor functions. If a test must override the WebChromeClient, 741 * it can derive from this class or call onProgressChanged 742 * directly. 743 */ 744 public static class WaitForProgressClient extends WebChromeClient { 745 private WebViewOnUiThread mOnUiThread; 746 747 public WaitForProgressClient(WebViewOnUiThread onUiThread) { 748 mOnUiThread = onUiThread; 749 } 750 751 @Override 752 public void onProgressChanged(WebView view, int newProgress) { 753 super.onProgressChanged(view, newProgress); 754 mOnUiThread.onProgressChanged(newProgress); 755 } 756 } 757 758 /** 759 * A WebViewClient that captures the onPageFinished for use in 760 * waitFor functions. Using initializeWebView sets the WaitForLoadedClient 761 * into the WebView. If a test needs to set a specific WebViewClient and 762 * needs the waitForCompletion capability then it should derive from 763 * WaitForLoadedClient or call WebViewOnUiThread.onPageFinished. 764 */ 765 public static class WaitForLoadedClient extends WebViewClient { 766 private WebViewOnUiThread mOnUiThread; 767 768 public WaitForLoadedClient(WebViewOnUiThread onUiThread) { 769 mOnUiThread = onUiThread; 770 } 771 772 @Override 773 public void onPageFinished(WebView view, String url) { 774 super.onPageFinished(view, url); 775 mOnUiThread.onPageFinished(); 776 } 777 778 @Override 779 public void onPageStarted(WebView view, String url, Bitmap favicon) { 780 super.onPageStarted(view, url, favicon); 781 mOnUiThread.onPageStarted(); 782 } 783 } 784 785 /** 786 * A PictureListener that captures the onNewPicture for use in 787 * waitForLoadCompletion. Using initializeWebView sets the PictureListener 788 * into the WebView. If a test needs to set a specific PictureListener and 789 * needs the waitForCompletion capability then it should call 790 * WebViewOnUiThread.onNewPicture. 791 */ 792 private class WaitForNewPicture implements PictureListener { 793 @Override 794 public void onNewPicture(WebView view, Picture picture) { 795 WebViewOnUiThread.this.onNewPicture(); 796 } 797 } 798 } 799