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