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 17package android.webkit.cts; 18 19import android.cts.util.PollingCheck; 20import android.graphics.Bitmap; 21import android.graphics.Picture; 22import android.graphics.Rect; 23import android.os.Bundle; 24import android.os.Looper; 25import android.os.Message; 26import android.os.SystemClock; 27import android.test.InstrumentationTestCase; 28import android.util.DisplayMetrics; 29import android.view.View; 30import android.webkit.DownloadListener; 31import android.webkit.WebBackForwardList; 32import android.webkit.WebChromeClient; 33import android.webkit.WebSettings; 34import android.webkit.WebView; 35import android.webkit.WebView.HitTestResult; 36import android.webkit.WebView.PictureListener; 37import android.webkit.WebViewClient; 38 39import junit.framework.Assert; 40 41import 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 */ 50public 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 Assert.assertTrue("WebViewOnUiThread.load*AndWaitForCompletion calls " 665 + "may not be mixed with load* calls directly on WebView " 666 + "without calling waitForLoadCompletion after the load", 667 !isLoaded()); 668 clearLoad(); // clear any extraneous signals from a previous load. 669 runOnUiThread(call); 670 waitForLoadCompletion(); 671 } 672 673 /** 674 * Called whenever a load has been completed so that a subsequent call to 675 * waitForLoadCompletion doesn't return immediately. 676 */ 677 synchronized private void clearLoad() { 678 mLoaded = false; 679 mNewPicture = false; 680 mProgress = 0; 681 } 682 683 /** 684 * Uses a polling mechanism, while pumping messages to check when the 685 * load completes. 686 */ 687 private void waitOnUiThread() { 688 new PollingCheck(LOAD_TIMEOUT) { 689 @Override 690 protected boolean check() { 691 pumpMessages(); 692 return isLoaded(); 693 } 694 }.run(); 695 } 696 697 /** 698 * Uses a wait/notify to check when the load completes. 699 */ 700 private synchronized void waitOnTestThread() { 701 try { 702 long waitEnd = SystemClock.uptimeMillis() + LOAD_TIMEOUT; 703 long timeRemaining = LOAD_TIMEOUT; 704 while (!isLoaded() && timeRemaining > 0) { 705 this.wait(timeRemaining); 706 timeRemaining = waitEnd - SystemClock.uptimeMillis(); 707 } 708 } catch (InterruptedException e) { 709 // We'll just drop out of the loop and fail 710 } 711 Assert.assertTrue("Load failed to complete before timeout", isLoaded()); 712 } 713 714 /** 715 * Pumps all currently-queued messages in the UI thread and then exits. 716 * This is useful to force processing while running tests in the UI thread. 717 */ 718 private void pumpMessages() { 719 class ExitLoopException extends RuntimeException { 720 } 721 722 // Force loop to exit when processing this. Loop.quit() doesn't 723 // work because this is the main Loop. 724 mWebView.getHandler().post(new Runnable() { 725 @Override 726 public void run() { 727 throw new ExitLoopException(); // exit loop! 728 } 729 }); 730 try { 731 // Pump messages until our message gets through. 732 Looper.loop(); 733 } catch (ExitLoopException e) { 734 } 735 } 736 737 /** 738 * A WebChromeClient used to capture the onProgressChanged for use 739 * in waitFor functions. If a test must override the WebChromeClient, 740 * it can derive from this class or call onProgressChanged 741 * directly. 742 */ 743 public static class WaitForProgressClient extends WebChromeClient { 744 private WebViewOnUiThread mOnUiThread; 745 746 public WaitForProgressClient(WebViewOnUiThread onUiThread) { 747 mOnUiThread = onUiThread; 748 } 749 750 @Override 751 public void onProgressChanged(WebView view, int newProgress) { 752 super.onProgressChanged(view, newProgress); 753 mOnUiThread.onProgressChanged(newProgress); 754 } 755 } 756 757 /** 758 * A WebViewClient that captures the onPageFinished for use in 759 * waitFor functions. Using initializeWebView sets the WaitForLoadedClient 760 * into the WebView. If a test needs to set a specific WebViewClient and 761 * needs the waitForCompletion capability then it should derive from 762 * WaitForLoadedClient or call WebViewOnUiThread.onPageFinished. 763 */ 764 public static class WaitForLoadedClient extends WebViewClient { 765 private WebViewOnUiThread mOnUiThread; 766 767 public WaitForLoadedClient(WebViewOnUiThread onUiThread) { 768 mOnUiThread = onUiThread; 769 } 770 771 @Override 772 public void onPageFinished(WebView view, String url) { 773 super.onPageFinished(view, url); 774 mOnUiThread.onPageFinished(); 775 } 776 777 @Override 778 public void onPageStarted(WebView view, String url, Bitmap favicon) { 779 super.onPageStarted(view, url, favicon); 780 mOnUiThread.onPageStarted(); 781 } 782 } 783 784 /** 785 * A PictureListener that captures the onNewPicture for use in 786 * waitForLoadCompletion. Using initializeWebView sets the PictureListener 787 * into the WebView. If a test needs to set a specific PictureListener and 788 * needs the waitForCompletion capability then it should call 789 * WebViewOnUiThread.onNewPicture. 790 */ 791 private class WaitForNewPicture implements PictureListener { 792 @Override 793 public void onNewPicture(WebView view, Picture picture) { 794 WebViewOnUiThread.this.onNewPicture(); 795 } 796 } 797} 798