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.cts.util.TestThread;
21 import android.graphics.Bitmap;
22 import android.graphics.Picture;
23 import android.graphics.Rect;
24 import android.net.Uri;
25 import android.os.Bundle;
26 import android.os.Looper;
27 import android.os.Message;
28 import android.os.SystemClock;
29 import android.print.PrintDocumentAdapter;
30 import android.test.InstrumentationTestCase;
31 import android.util.DisplayMetrics;
32 import android.view.View;
33 import android.view.ViewGroup;
34 import android.view.ViewParent;
35 import android.webkit.DownloadListener;
36 import android.webkit.CookieManager;
37 import android.webkit.ValueCallback;
38 import android.webkit.WebBackForwardList;
39 import android.webkit.WebChromeClient;
40 import android.webkit.WebMessage;
41 import android.webkit.WebMessagePort;
42 import android.webkit.WebSettings;
43 import android.webkit.WebView.HitTestResult;
44 import android.webkit.WebView.PictureListener;
45 import android.webkit.WebView.VisualStateCallback;
46 import android.webkit.WebView;
47 import android.webkit.WebViewClient;
48 
49 import junit.framework.Assert;
50 
51 import java.io.File;
52 import java.util.concurrent.Callable;
53 import java.util.Map;
54 
55 /**
56  * Many tests need to run WebView code in the UI thread. This class
57  * wraps a WebView so that calls are ensured to arrive on the UI thread.
58  *
59  * All methods may be run on either the UI thread or test thread.
60  */
61 public class WebViewOnUiThread {
62     /**
63      * The maximum time, in milliseconds (10 seconds) to wait for a load
64      * to be triggered.
65      */
66     private static final long LOAD_TIMEOUT = 10000;
67 
68     /**
69      * Set to true after onPageFinished is called.
70      */
71     private boolean mLoaded;
72 
73     /**
74      * Set to true after onNewPicture is called. Reset when onPageStarted
75      * is called.
76      */
77     private boolean mNewPicture;
78 
79     /**
80      * The progress, in percentage, of the page load. Valid values are between
81      * 0 and 100.
82      */
83     private int mProgress;
84 
85     /**
86      * The test that this class is being used in. Used for runTestOnUiThread.
87      */
88     private InstrumentationTestCase mTest;
89 
90     /**
91      * The WebView that calls will be made on.
92      */
93     private WebView mWebView;
94 
95     /**
96      * Initializes the webView with a WebViewClient, WebChromeClient,
97      * and PictureListener to prepare for loadUrlAndWaitForCompletion.
98      *
99      * A new WebViewOnUiThread should be called during setUp so as to
100      * reinitialize between calls.
101      *
102      * @param test The test in which this is being run.
103      * @param webView The webView that the methods should call.
104      * @see loadUrlAndWaitForCompletion
105      */
WebViewOnUiThread(InstrumentationTestCase test, WebView webView)106     public WebViewOnUiThread(InstrumentationTestCase test, WebView webView) {
107         mTest = test;
108         mWebView = webView;
109         final WebViewClient webViewClient = new WaitForLoadedClient(this);
110         final WebChromeClient webChromeClient = new WaitForProgressClient(this);
111         runOnUiThread(new Runnable() {
112             @Override
113             public void run() {
114                 mWebView.setWebViewClient(webViewClient);
115                 mWebView.setWebChromeClient(webChromeClient);
116                 mWebView.setPictureListener(new WaitForNewPicture());
117             }
118         });
119     }
120 
121     /**
122      * Called after a test is complete and the WebView should be disengaged from
123      * the tests.
124      */
cleanUp()125     public void cleanUp() {
126         clearHistory();
127         clearCache(true);
128         setPictureListener(null);
129         setWebChromeClient(null);
130         setWebViewClient(null);
131     }
132 
133     /**
134      * Called from WaitForNewPicture, this is used to indicate that
135      * the page has been drawn.
136      */
onNewPicture()137     synchronized public void onNewPicture() {
138         mNewPicture = true;
139         this.notifyAll();
140     }
141 
142     /**
143      * Called from WaitForLoadedClient, this is used to clear the picture
144      * draw state so that draws before the URL begins loading don't count.
145      */
onPageStarted()146     synchronized public void onPageStarted() {
147         mNewPicture = false; // Earlier paints won't count.
148     }
149 
150     /**
151      * Called from WaitForLoadedClient, this is used to indicate that
152      * the page is loaded, but not drawn yet.
153      */
onPageFinished()154     synchronized public void onPageFinished() {
155         mLoaded = true;
156         this.notifyAll();
157     }
158 
159     /**
160      * Called from the WebChrome client, this sets the current progress
161      * for a page.
162      * @param progress The progress made so far between 0 and 100.
163      */
onProgressChanged(int progress)164     synchronized public void onProgressChanged(int progress) {
165         mProgress = progress;
166         this.notifyAll();
167     }
168 
setWebViewClient(final WebViewClient webViewClient)169     public void setWebViewClient(final WebViewClient webViewClient) {
170         runOnUiThread(new Runnable() {
171             @Override
172             public void run() {
173                 mWebView.setWebViewClient(webViewClient);
174             }
175         });
176     }
177 
setWebChromeClient(final WebChromeClient webChromeClient)178     public void setWebChromeClient(final WebChromeClient webChromeClient) {
179         runOnUiThread(new Runnable() {
180             @Override
181             public void run() {
182                 mWebView.setWebChromeClient(webChromeClient);
183             }
184         });
185     }
186 
setPictureListener(final PictureListener pictureListener)187     public void setPictureListener(final PictureListener pictureListener) {
188         runOnUiThread(new Runnable() {
189             @Override
190             public void run() {
191                 mWebView.setPictureListener(pictureListener);
192             }
193         });
194     }
195 
setNetworkAvailable(final boolean available)196     public void setNetworkAvailable(final boolean available) {
197         runOnUiThread(new Runnable() {
198             @Override
199             public void run() {
200                 mWebView.setNetworkAvailable(available);
201             }
202         });
203     }
204 
setDownloadListener(final DownloadListener listener)205     public void setDownloadListener(final DownloadListener listener) {
206         runOnUiThread(new Runnable() {
207             @Override
208             public void run() {
209                 mWebView.setDownloadListener(listener);
210             }
211         });
212     }
213 
setBackgroundColor(final int color)214     public void setBackgroundColor(final int color) {
215         runOnUiThread(new Runnable() {
216             @Override
217             public void run() {
218                 mWebView.setBackgroundColor(color);
219             }
220         });
221     }
222 
clearCache(final boolean includeDiskFiles)223     public void clearCache(final boolean includeDiskFiles) {
224         runOnUiThread(new Runnable() {
225             @Override
226             public void run() {
227                 mWebView.clearCache(includeDiskFiles);
228             }
229         });
230     }
231 
clearHistory()232     public void clearHistory() {
233         runOnUiThread(new Runnable() {
234             @Override
235             public void run() {
236                 mWebView.clearHistory();
237             }
238         });
239     }
240 
requestFocus()241     public void requestFocus() {
242         runOnUiThread(new Runnable() {
243             @Override
244             public void run() {
245                 mWebView.requestFocus();
246             }
247         });
248     }
249 
canZoomIn()250     public boolean canZoomIn() {
251         return getValue(new ValueGetter<Boolean>() {
252             @Override
253             public Boolean capture() {
254                 return mWebView.canZoomIn();
255             }
256         });
257     }
258 
259     public boolean canZoomOut() {
260         return getValue(new ValueGetter<Boolean>() {
261             @Override
262             public Boolean capture() {
263                 return mWebView.canZoomOut();
264             }
265         });
266     }
267 
268     public boolean zoomIn() {
269         return getValue(new ValueGetter<Boolean>() {
270             @Override
271             public Boolean capture() {
272                 return mWebView.zoomIn();
273             }
274         });
275     }
276 
277     public boolean zoomOut() {
278         return getValue(new ValueGetter<Boolean>() {
279             @Override
280             public Boolean capture() {
281                 return mWebView.zoomOut();
282             }
283         });
284     }
285 
286     public void zoomBy(final float zoomFactor) {
287         runOnUiThread(new Runnable() {
288             @Override
289             public void run() {
290                 mWebView.zoomBy(zoomFactor);
291             }
292         });
293     }
294 
295     public void setFindListener(final WebView.FindListener listener) {
296         runOnUiThread(new Runnable() {
297             @Override
298             public void run() {
299                 mWebView.setFindListener(listener);
300             }
301         });
302     }
303 
304     public void removeJavascriptInterface(final String interfaceName) {
305         runOnUiThread(new Runnable() {
306             @Override
307             public void run() {
308                 mWebView.removeJavascriptInterface(interfaceName);
309             }
310         });
311     }
312 
313     public WebMessagePort[] createWebMessageChannel() {
314         return getValue(new ValueGetter<WebMessagePort[]>() {
315             @Override
316             public WebMessagePort[] capture() {
317                 return mWebView.createWebMessageChannel();
318             }
319         });
320     }
321 
322     public void postWebMessage(final WebMessage message, final Uri targetOrigin) {
323         runOnUiThread(new Runnable() {
324             @Override
325             public void run() {
326                 mWebView.postWebMessage(message, targetOrigin);
327             }
328         });
329     }
330 
331     public void addJavascriptInterface(final Object object, final String name) {
332         runOnUiThread(new Runnable() {
333             @Override
334             public void run() {
335                 mWebView.addJavascriptInterface(object, name);
336             }
337         });
338     }
339 
340     public void flingScroll(final int vx, final int vy) {
341         runOnUiThread(new Runnable() {
342             @Override
343             public void run() {
344                 mWebView.flingScroll(vx, vy);
345             }
346         });
347     }
348 
349     public void requestFocusNodeHref(final Message hrefMsg) {
350         runOnUiThread(new Runnable() {
351             @Override
352             public void run() {
353                 mWebView.requestFocusNodeHref(hrefMsg);
354             }
355         });
356     }
357 
358     public void requestImageRef(final Message msg) {
359         runOnUiThread(new Runnable() {
360             @Override
361             public void run() {
362                 mWebView.requestImageRef(msg);
363             }
364         });
365     }
366 
367     public void setInitialScale(final int scaleInPercent) {
368         runOnUiThread(new Runnable() {
369             @Override
370             public void run() {
371                 mWebView.setInitialScale(scaleInPercent);
372             }
373         });
374     }
375 
376     public void clearSslPreferences() {
377         runOnUiThread(new Runnable() {
378             @Override
379             public void run() {
380                 mWebView.clearSslPreferences();
381             }
382         });
383     }
384 
385     public void clearClientCertPreferences(final Runnable onCleared) {
386         runOnUiThread(new Runnable() {
387             @Override
388             public void run() {
389                 WebView.clearClientCertPreferences(onCleared);
390             }
391         });
392     }
393 
394     public void resumeTimers() {
395         runOnUiThread(new Runnable() {
396             @Override
397             public void run() {
398                 mWebView.resumeTimers();
399             }
400         });
401     }
402 
403     public void findNext(final boolean forward) {
404         runOnUiThread(new Runnable() {
405             @Override
406             public void run() {
407                 mWebView.findNext(forward);
408             }
409         });
410     }
411 
412     public void clearMatches() {
413         runOnUiThread(new Runnable() {
414             @Override
415             public void run() {
416                 mWebView.clearMatches();
417             }
418         });
419     }
420 
421     /**
422      * Calls loadUrl on the WebView and then waits onPageFinished,
423      * onNewPicture and onProgressChange to reach 100.
424      * Test fails if the load timeout elapses.
425      * @param url The URL to load.
426      */
427     public void loadUrlAndWaitForCompletion(final String url) {
428         callAndWait(new Runnable() {
429             @Override
430             public void run() {
431                 mWebView.loadUrl(url);
432             }
433         });
434     }
435 
436     /**
437      * Calls loadUrl on the WebView and then waits onPageFinished,
438      * onNewPicture and onProgressChange to reach 100.
439      * Test fails if the load timeout elapses.
440      * @param url The URL to load.
441      * @param extraHeaders The additional headers to be used in the HTTP request.
442      */
443     public void loadUrlAndWaitForCompletion(final String url,
444             final Map<String, String> extraHeaders) {
445         callAndWait(new Runnable() {
446             @Override
447             public void run() {
448                 mWebView.loadUrl(url, extraHeaders);
449             }
450         });
451     }
452 
453     public void loadUrl(final String url) {
454         runOnUiThread(new Runnable() {
455             @Override
456             public void run() {
457                 mWebView.loadUrl(url);
458             }
459         });
460     }
461 
462     public void stopLoading() {
463         runOnUiThread(new Runnable() {
464             @Override
465             public void run() {
466                 mWebView.stopLoading();
467             }
468         });
469     }
470 
471     public void postUrlAndWaitForCompletion(final String url, final byte[] postData) {
472         callAndWait(new Runnable() {
473             @Override
474             public void run() {
475                 mWebView.postUrl(url, postData);
476             }
477         });
478     }
479 
480     public void loadDataAndWaitForCompletion(final String data,
481             final String mimeType, final String encoding) {
482         callAndWait(new Runnable() {
483             @Override
484             public void run() {
485                 mWebView.loadData(data, mimeType, encoding);
486             }
487         });
488     }
489 
490     public void loadDataWithBaseURLAndWaitForCompletion(final String baseUrl,
491             final String data, final String mimeType, final String encoding,
492             final String historyUrl) {
493         callAndWait(new Runnable() {
494             @Override
495             public void run() {
496                 mWebView.loadDataWithBaseURL(baseUrl, data, mimeType, encoding,
497                         historyUrl);
498             }
499         });
500     }
501 
502     /**
503      * Reloads a page and waits for it to complete reloading. Use reload
504      * if it is a form resubmission and the onFormResubmission responds
505      * by telling WebView not to resubmit it.
506      */
507     public void reloadAndWaitForCompletion() {
508         callAndWait(new Runnable() {
509             @Override
510             public void run() {
511                 mWebView.reload();
512             }
513         });
514     }
515 
516     /**
517      * Reload the previous URL. Use reloadAndWaitForCompletion unless
518      * it is a form resubmission and the onFormResubmission responds
519      * by telling WebView not to resubmit it.
520      */
521     public void reload() {
522         runOnUiThread(new Runnable() {
523             @Override
524             public void run() {
525                 mWebView.reload();
526             }
527         });
528     }
529 
530     /**
531      * Use this only when JavaScript causes a page load to wait for the
532      * page load to complete. Otherwise use loadUrlAndWaitForCompletion or
533      * similar functions.
534      */
535     public void waitForLoadCompletion() {
536         waitForCriteria(LOAD_TIMEOUT,
537                 new Callable<Boolean>() {
538                     @Override
539                     public Boolean call() {
540                         return isLoaded();
541                     }
542                 });
543         clearLoad();
544     }
545 
546     private void waitForCriteria(long timeout, Callable<Boolean> doneCriteria) {
547         if (isUiThread()) {
548             waitOnUiThread(timeout, doneCriteria);
549         } else {
550             waitOnTestThread(timeout, doneCriteria);
551         }
552     }
553 
554     public String getTitle() {
555         return getValue(new ValueGetter<String>() {
556             @Override
557             public String capture() {
558                 return mWebView.getTitle();
559             }
560         });
561     }
562 
563     public WebSettings getSettings() {
564         return getValue(new ValueGetter<WebSettings>() {
565             @Override
566             public WebSettings capture() {
567                 return mWebView.getSettings();
568             }
569         });
570     }
571 
572     public WebBackForwardList copyBackForwardList() {
573         return getValue(new ValueGetter<WebBackForwardList>() {
574             @Override
575             public WebBackForwardList capture() {
576                 return mWebView.copyBackForwardList();
577             }
578         });
579     }
580 
581     public Bitmap getFavicon() {
582         return getValue(new ValueGetter<Bitmap>() {
583             @Override
584             public Bitmap capture() {
585                 return mWebView.getFavicon();
586             }
587         });
588     }
589 
590     public String getUrl() {
591         return getValue(new ValueGetter<String>() {
592             @Override
593             public String capture() {
594                 return mWebView.getUrl();
595             }
596         });
597     }
598 
599     public int getProgress() {
600         return getValue(new ValueGetter<Integer>() {
601             @Override
602             public Integer capture() {
603                 return mWebView.getProgress();
604             }
605         });
606     }
607 
608     public int getHeight() {
609         return getValue(new ValueGetter<Integer>() {
610             @Override
611             public Integer capture() {
612                 return mWebView.getHeight();
613             }
614         });
615     }
616 
617     public int getContentHeight() {
618         return getValue(new ValueGetter<Integer>() {
619             @Override
620             public Integer capture() {
621                 return mWebView.getContentHeight();
622             }
623         });
624     }
625 
626     public boolean savePicture(final Bundle b, final File dest) {
627         return getValue(new ValueGetter<Boolean>() {
628             @Override
629             public Boolean capture() {
630                 return mWebView.savePicture(b, dest);
631             }
632         });
633     }
634 
635     public boolean pageUp(final boolean top) {
636         return getValue(new ValueGetter<Boolean>() {
637             @Override
638             public Boolean capture() {
639                 return mWebView.pageUp(top);
640             }
641         });
642     }
643 
644     public boolean pageDown(final boolean bottom) {
645         return getValue(new ValueGetter<Boolean>() {
646             @Override
647             public Boolean capture() {
648                 return mWebView.pageDown(bottom);
649             }
650         });
651     }
652 
653     public void postVisualStateCallback(final long requestId, final VisualStateCallback callback) {
654         runOnUiThread(new Runnable() {
655             @Override
656             public void run() {
657                 mWebView.postVisualStateCallback(requestId, callback);
658             }
659         });
660     }
661 
662     public int[] getLocationOnScreen() {
663         final int[] location = new int[2];
664         return getValue(new ValueGetter<int[]>() {
665             @Override
666             public int[] capture() {
667                 mWebView.getLocationOnScreen(location);
668                 return location;
669             }
670         });
671     }
672 
673     public float getScale() {
674         return getValue(new ValueGetter<Float>() {
675             @Override
676             public Float capture() {
677                 return mWebView.getScale();
678             }
679         });
680     }
681 
682     public boolean requestFocus(final int direction,
683             final Rect previouslyFocusedRect) {
684         return getValue(new ValueGetter<Boolean>() {
685             @Override
686             public Boolean capture() {
687                 return mWebView.requestFocus(direction, previouslyFocusedRect);
688             }
689         });
690     }
691 
692     public HitTestResult getHitTestResult() {
693         return getValue(new ValueGetter<HitTestResult>() {
694             @Override
695             public HitTestResult capture() {
696                 return mWebView.getHitTestResult();
697             }
698         });
699     }
700 
701     public int getScrollX() {
702         return getValue(new ValueGetter<Integer>() {
703             @Override
704             public Integer capture() {
705                 return mWebView.getScrollX();
706             }
707         });
708     }
709 
710     public int getScrollY() {
711         return getValue(new ValueGetter<Integer>() {
712             @Override
713             public Integer capture() {
714                 return mWebView.getScrollY();
715             }
716         });
717     }
718 
719     public final DisplayMetrics getDisplayMetrics() {
720         return getValue(new ValueGetter<DisplayMetrics>() {
721             @Override
722             public DisplayMetrics capture() {
723                 return mWebView.getContext().getResources().getDisplayMetrics();
724             }
725         });
726     }
727 
728     public boolean requestChildRectangleOnScreen(final View child,
729             final Rect rect,
730             final boolean immediate) {
731         return getValue(new ValueGetter<Boolean>() {
732             @Override
733             public Boolean capture() {
734                 return mWebView.requestChildRectangleOnScreen(child, rect,
735                         immediate);
736             }
737         });
738     }
739 
740     public int findAll(final String find) {
741         return getValue(new ValueGetter<Integer>() {
742             @Override
743             public Integer capture() {
744                 return mWebView.findAll(find);
745             }
746         });
747     }
748 
749     public Picture capturePicture() {
750         return getValue(new ValueGetter<Picture>() {
751             @Override
752             public Picture capture() {
753                 return mWebView.capturePicture();
754             }
755         });
756     }
757 
758     public void evaluateJavascript(final String script, final ValueCallback<String> result) {
759         runOnUiThread(new Runnable() {
760             @Override
761             public void run() {
762                 mWebView.evaluateJavascript(script, result);
763             }
764         });
765     }
766 
767     public void saveWebArchive(final String basename, final boolean autoname,
768                                final ValueCallback<String> callback) {
769         runOnUiThread(new Runnable() {
770             @Override
771             public void run() {
772                 mWebView.saveWebArchive(basename, autoname, callback);
773             }
774         });
775     }
776 
777     public WebView createWebView() {
778         return getValue(new ValueGetter<WebView>() {
779             @Override
780             public WebView capture() {
781                 return new WebView(mWebView.getContext());
782             }
783         });
784     }
785 
786     public PrintDocumentAdapter createPrintDocumentAdapter() {
787         return getValue(new ValueGetter<PrintDocumentAdapter>() {
788             @Override
789             public PrintDocumentAdapter capture() {
790                 return mWebView.createPrintDocumentAdapter();
791             }
792         });
793     }
794 
795     public void setLayoutHeightToMatchParent() {
796         runOnUiThread(new Runnable() {
797             @Override
798             public void run() {
799                 ViewParent parent = mWebView.getParent();
800                 if (parent instanceof ViewGroup) {
801                     ((ViewGroup) parent).getLayoutParams().height =
802                         ViewGroup.LayoutParams.MATCH_PARENT;
803                 }
804                 mWebView.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
805                 mWebView.requestLayout();
806             }
807         });
808     }
809 
810     public void setAcceptThirdPartyCookies(final boolean accept) {
811         runOnUiThread(new Runnable() {
812             @Override
813             public void run() {
814                 CookieManager.getInstance().setAcceptThirdPartyCookies(mWebView, accept);
815             }
816         });
817     }
818 
819     public boolean acceptThirdPartyCookies() {
820         return getValue(new ValueGetter<Boolean>() {
821             @Override
822             public Boolean capture() {
823                 return CookieManager.getInstance().acceptThirdPartyCookies(mWebView);
824             }
825         });
826     }
827 
828     /**
829      * Helper for running code on the UI thread where an exception is
830      * a test failure. If this is already the UI thread then it runs
831      * the code immediately.
832      *
833      * @see runTestOnUiThread
834      * @param r The code to run in the UI thread
835      */
836     public void runOnUiThread(Runnable r) {
837         try {
838             if (isUiThread()) {
839                 r.run();
840             } else {
841                 mTest.runTestOnUiThread(r);
842             }
843         } catch (Throwable t) {
844             Assert.fail("Unexpected error while running on UI thread: "
845                     + t.getMessage());
846         }
847     }
848 
849     /**
850      * Accessor for underlying WebView.
851      * @return The WebView being wrapped by this class.
852      */
853     public WebView getWebView() {
854         return mWebView;
855     }
856 
857     private<T> T getValue(ValueGetter<T> getter) {
858         runOnUiThread(getter);
859         return getter.getValue();
860     }
861 
862     private abstract class ValueGetter<T> implements Runnable {
863         private T mValue;
864 
865         @Override
866         public void run() {
867             mValue = capture();
868         }
869 
870         protected abstract T capture();
871 
872         public T getValue() {
873            return mValue;
874         }
875     }
876 
877     /**
878      * Returns true if the current thread is the UI thread based on the
879      * Looper.
880      */
881     private static boolean isUiThread() {
882         return (Looper.myLooper() == Looper.getMainLooper());
883     }
884 
885     /**
886      * @return Whether or not the load has finished.
887      */
888     private synchronized boolean isLoaded() {
889         return mLoaded && mNewPicture && mProgress == 100;
890     }
891 
892     /**
893      * Makes a WebView call, waits for completion and then resets the
894      * load state in preparation for the next load call.
895      * @param call The call to make on the UI thread prior to waiting.
896      */
897     private void callAndWait(Runnable call) {
898         Assert.assertTrue("WebViewOnUiThread.load*AndWaitForCompletion calls "
899                 + "may not be mixed with load* calls directly on WebView "
900                 + "without calling waitForLoadCompletion after the load",
901                 !isLoaded());
902         clearLoad(); // clear any extraneous signals from a previous load.
903         runOnUiThread(call);
904         waitForLoadCompletion();
905     }
906 
907     /**
908      * Called whenever a load has been completed so that a subsequent call to
909      * waitForLoadCompletion doesn't return immediately.
910      */
911     synchronized private void clearLoad() {
912         mLoaded = false;
913         mNewPicture = false;
914         mProgress = 0;
915     }
916 
917     /**
918      * Uses a polling mechanism, while pumping messages to check when the
919      * criteria is met.
920      */
921     private void waitOnUiThread(long timeout, final Callable<Boolean> doneCriteria) {
922         new PollingCheck(timeout) {
923             @Override
924             protected boolean check() {
925                 pumpMessages();
926                 try {
927                     return doneCriteria.call();
928                 } catch (Exception e) {
929                     Assert.fail("Unexpected error while checking the criteria: "
930                             + e.getMessage());
931                     return true;
932                 }
933             }
934         }.run();
935     }
936 
937     /**
938      * Uses a wait/notify to check when the criteria is met.
939      */
940     private synchronized void waitOnTestThread(long timeout, Callable<Boolean> doneCriteria) {
941         try {
942             long waitEnd = SystemClock.uptimeMillis() + timeout;
943             long timeRemaining = timeout;
944             while (!doneCriteria.call() && timeRemaining > 0) {
945                 this.wait(timeRemaining);
946                 timeRemaining = waitEnd - SystemClock.uptimeMillis();
947             }
948             Assert.assertTrue("Action failed to complete before timeout", doneCriteria.call());
949         } catch (InterruptedException e) {
950             // We'll just drop out of the loop and fail
951         } catch (Exception e) {
952             Assert.fail("Unexpected error while checking the criteria: "
953                     + e.getMessage());
954         }
955     }
956 
957     /**
958      * Pumps all currently-queued messages in the UI thread and then exits.
959      * This is useful to force processing while running tests in the UI thread.
960      */
961     private void pumpMessages() {
962         class ExitLoopException extends RuntimeException {
963         }
964 
965         // Force loop to exit when processing this. Loop.quit() doesn't
966         // work because this is the main Loop.
967         mWebView.getHandler().post(new Runnable() {
968             @Override
969             public void run() {
970                 throw new ExitLoopException(); // exit loop!
971             }
972         });
973         try {
974             // Pump messages until our message gets through.
975             Looper.loop();
976         } catch (ExitLoopException e) {
977         }
978     }
979 
980     /**
981      * A WebChromeClient used to capture the onProgressChanged for use
982      * in waitFor functions. If a test must override the WebChromeClient,
983      * it can derive from this class or call onProgressChanged
984      * directly.
985      */
986     public static class WaitForProgressClient extends WebChromeClient {
987         private WebViewOnUiThread mOnUiThread;
988 
989         public WaitForProgressClient(WebViewOnUiThread onUiThread) {
990             mOnUiThread = onUiThread;
991         }
992 
993         @Override
994         public void onProgressChanged(WebView view, int newProgress) {
995             super.onProgressChanged(view, newProgress);
996             mOnUiThread.onProgressChanged(newProgress);
997         }
998     }
999 
1000     /**
1001      * A WebViewClient that captures the onPageFinished for use in
1002      * waitFor functions. Using initializeWebView sets the WaitForLoadedClient
1003      * into the WebView. If a test needs to set a specific WebViewClient and
1004      * needs the waitForCompletion capability then it should derive from
1005      * WaitForLoadedClient or call WebViewOnUiThread.onPageFinished.
1006      */
1007     public static class WaitForLoadedClient extends WebViewClient {
1008         private WebViewOnUiThread mOnUiThread;
1009 
1010         public WaitForLoadedClient(WebViewOnUiThread onUiThread) {
1011             mOnUiThread = onUiThread;
1012         }
1013 
1014         @Override
1015         public void onPageFinished(WebView view, String url) {
1016             super.onPageFinished(view, url);
1017             mOnUiThread.onPageFinished();
1018         }
1019 
1020         @Override
1021         public void onPageStarted(WebView view, String url, Bitmap favicon) {
1022             super.onPageStarted(view, url, favicon);
1023             mOnUiThread.onPageStarted();
1024         }
1025     }
1026 
1027     /**
1028      * A PictureListener that captures the onNewPicture for use in
1029      * waitForLoadCompletion. Using initializeWebView sets the PictureListener
1030      * into the WebView. If a test needs to set a specific PictureListener and
1031      * needs the waitForCompletion capability then it should call
1032      * WebViewOnUiThread.onNewPicture.
1033      */
1034     private class WaitForNewPicture implements PictureListener {
1035         @Override
1036         public void onNewPicture(WebView view, Picture picture) {
1037             WebViewOnUiThread.this.onNewPicture();
1038         }
1039     }
1040 }
1041