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.graphics.Bitmap;
20 import android.graphics.Picture;
21 import android.os.Looper;
22 import android.os.SystemClock;
23 import android.webkit.WebChromeClient;
24 import android.webkit.WebView;
25 import android.webkit.WebView.PictureListener;
26 import android.webkit.WebViewClient;
27 
28 import androidx.annotation.CallSuper;
29 
30 import com.android.compatibility.common.util.PollingCheck;
31 
32 import junit.framework.Assert;
33 
34 import java.util.Map;
35 import java.util.concurrent.Callable;
36 
37 /**
38  * Utility class to simplify tests that need to load data into a WebView and wait for completion
39  * conditions.
40  *
41  * May be used from any thread.
42  */
43 public class WebViewSyncLoader {
44     /**
45      * Set to true after onPageFinished is called.
46      */
47     private boolean mLoaded;
48 
49     /**
50      * Set to true after onNewPicture is called. Reset when onPageStarted
51      * is called.
52      */
53     private boolean mNewPicture;
54 
55     /**
56      * The progress, in percentage, of the page load. Valid values are between
57      * 0 and 100.
58      */
59     private int mProgress;
60 
61     /**
62      * The WebView that calls will be made on.
63      */
64     private WebView mWebView;
65 
66 
WebViewSyncLoader(WebView webView)67     public WebViewSyncLoader(WebView webView) {
68         init(webView, new WaitForLoadedClient(this), new WaitForProgressClient(this),
69                 new WaitForNewPicture(this));
70     }
71 
WebViewSyncLoader( WebView webView, WaitForLoadedClient waitForLoadedClient, WaitForProgressClient waitForProgressClient, WaitForNewPicture waitForNewPicture)72     public WebViewSyncLoader(
73             WebView webView,
74             WaitForLoadedClient waitForLoadedClient,
75             WaitForProgressClient waitForProgressClient,
76             WaitForNewPicture waitForNewPicture) {
77         init(webView, waitForLoadedClient, waitForProgressClient, waitForNewPicture);
78     }
79 
init( final WebView webView, final WaitForLoadedClient waitForLoadedClient, final WaitForProgressClient waitForProgressClient, final WaitForNewPicture waitForNewPicture)80     private void init(
81             final WebView webView,
82             final WaitForLoadedClient waitForLoadedClient,
83             final WaitForProgressClient waitForProgressClient,
84             final WaitForNewPicture waitForNewPicture) {
85         if (!isUiThread()) {
86             WebkitUtils.onMainThreadSync(() -> {
87                 init(webView, waitForLoadedClient, waitForProgressClient, waitForNewPicture);
88             });
89             return;
90         }
91         mWebView = webView;
92         mWebView.setWebViewClient(waitForLoadedClient);
93         mWebView.setWebChromeClient(waitForProgressClient);
94         mWebView.setPictureListener(waitForNewPicture);
95     }
96 
97     /**
98      * Detach listeners from this WebView, undoing the changes made to enable sync loading.
99      */
detach()100     public void detach() {
101         if (!isUiThread()) {
102             WebkitUtils.onMainThreadSync(this::detach);
103             return;
104         }
105         mWebView.setWebChromeClient(null);
106         mWebView.setWebViewClient(null);
107         mWebView.setPictureListener(null);
108         mWebView = null;
109     }
110 
111     /**
112      * Detach listeners, and destroy this webview.
113      */
destroy()114     public void destroy() {
115         if (!isUiThread()) {
116             WebkitUtils.onMainThreadSync(this::destroy);
117             return;
118         }
119         WebView webView = mWebView;
120         detach();
121         webView.clearHistory();
122         webView.clearCache(true);
123         webView.destroy();
124     }
125 
126     /**
127      * Accessor for underlying WebView.
128      * @return The WebView being wrapped by this class.
129      */
getWebView()130     public WebView getWebView() {
131         return mWebView;
132     }
133 
134     /**
135      * Called from WaitForNewPicture, this is used to indicate that
136      * the page has been drawn.
137      */
onNewPicture()138     public synchronized void onNewPicture() {
139         mNewPicture = true;
140         this.notifyAll();
141     }
142 
143     /**
144      * Called from WaitForLoadedClient, this is used to clear the picture
145      * draw state so that draws before the URL begins loading don't count.
146      */
onPageStarted()147     public synchronized void onPageStarted() {
148         mNewPicture = false; // Earlier paints won't count.
149     }
150 
151     /**
152      * Called from WaitForLoadedClient, this is used to indicate that
153      * the page is loaded, but not drawn yet.
154      */
onPageFinished()155     public synchronized void onPageFinished() {
156         mLoaded = true;
157         this.notifyAll();
158     }
159 
160     /**
161      * Called from the WebChrome client, this sets the current progress
162      * for a page.
163      * @param progress The progress made so far between 0 and 100.
164      */
onProgressChanged(int progress)165     public synchronized void onProgressChanged(int progress) {
166         mProgress = progress;
167         this.notifyAll();
168     }
169 
170     /**
171      * Calls {@link WebView#loadUrl} on the WebView and then waits for completion.
172      *
173      * <p>Test fails if the load timeout elapses.
174      */
loadUrlAndWaitForCompletion(final String url)175     public void loadUrlAndWaitForCompletion(final String url) {
176         callAndWait(() -> mWebView.loadUrl(url));
177     }
178 
179     /**
180      * Calls {@link WebView#loadUrl(String,Map<String,String>)} on the WebView and waits for
181      * completion.
182      *
183      * <p>Test fails if the load timeout elapses.
184      */
loadUrlAndWaitForCompletion(final String url, final Map<String, String> extraHeaders)185     public void loadUrlAndWaitForCompletion(final String url,
186             final Map<String, String> extraHeaders) {
187         callAndWait(() -> mWebView.loadUrl(url, extraHeaders));
188     }
189 
190     /**
191      * Calls {@link WebView#postUrl(String,byte[])} on the WebView and then waits for completion.
192      *
193      * <p>Test fails if the load timeout elapses.
194      *
195      * @param url The URL to load.
196      * @param postData the data will be passed to "POST" request.
197      */
postUrlAndWaitForCompletion(final String url, final byte[] postData)198     public void postUrlAndWaitForCompletion(final String url, final byte[] postData) {
199         callAndWait(() -> mWebView.postUrl(url, postData));
200     }
201 
202     /**
203      * Calls {@link WebView#loadData(String,String,String)} on the WebView and then waits for
204      * completion.
205      *
206      * <p>Test fails if the load timeout elapses.
207      */
loadDataAndWaitForCompletion(final String data, final String mimeType, final String encoding)208     public void loadDataAndWaitForCompletion(final String data,
209             final String mimeType, final String encoding) {
210         callAndWait(() -> mWebView.loadData(data, mimeType, encoding));
211     }
212 
213     /**
214      * Calls {@link WebView#loadDataWithBaseUrl(String,String,String,String,String)} on the WebView
215      * and then waits for completion.
216      *
217      * <p>Test fails if the load timeout elapses.
218      */
loadDataWithBaseURLAndWaitForCompletion(final String baseUrl, final String data, final String mimeType, final String encoding, final String historyUrl)219     public void loadDataWithBaseURLAndWaitForCompletion(final String baseUrl,
220             final String data, final String mimeType, final String encoding,
221             final String historyUrl) {
222         callAndWait(
223                 () -> mWebView.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl));
224     }
225 
226     /**
227      * Reloads a page and waits for it to complete reloading. Use reload
228      * if it is a form resubmission and the onFormResubmission responds
229      * by telling WebView not to resubmit it.
230      */
reloadAndWaitForCompletion()231     public void reloadAndWaitForCompletion() {
232         callAndWait(() -> mWebView.reload());
233     }
234 
235     /**
236      * Use this only when JavaScript causes a page load to wait for the
237      * page load to complete. Otherwise use loadUrlAndWaitForCompletion or
238      * similar functions.
239      */
waitForLoadCompletion()240     public void waitForLoadCompletion() {
241         waitForCriteria(WebkitUtils.TEST_TIMEOUT_MS,
242                 new Callable<Boolean>() {
243                     @Override
244                     public Boolean call() {
245                         return isLoaded();
246                     }
247                 });
248         clearLoad();
249     }
250 
waitForCriteria(long timeout, Callable<Boolean> doneCriteria)251     private void waitForCriteria(long timeout, Callable<Boolean> doneCriteria) {
252         if (isUiThread()) {
253             waitOnUiThread(timeout, doneCriteria);
254         } else {
255             waitOnTestThread(timeout, doneCriteria);
256         }
257     }
258 
259     /**
260      * @return Whether or not the load has finished.
261      */
isLoaded()262     private synchronized boolean isLoaded() {
263         return mLoaded && mNewPicture && mProgress == 100;
264     }
265 
266     /**
267      * Makes a WebView call, waits for completion and then resets the
268      * load state in preparation for the next load call.
269      *
270      * <p>This method may be called on the UI thread.
271      *
272      * @param call The call to make on the UI thread prior to waiting.
273      */
callAndWait(Runnable call)274     private void callAndWait(Runnable call) {
275         Assert.assertTrue("WebViewSyncLoader.load*AndWaitForCompletion calls "
276                 + "may not be mixed with load* calls directly on WebView "
277                 + "without calling waitForLoadCompletion after the load",
278                 !isLoaded());
279         clearLoad(); // clear any extraneous signals from a previous load.
280         if (isUiThread()) {
281             call.run();
282         } else {
283             WebkitUtils.onMainThread(call);
284         }
285         waitForLoadCompletion();
286     }
287 
288     /**
289      * Called whenever a load has been completed so that a subsequent call to
290      * waitForLoadCompletion doesn't return immediately.
291      */
clearLoad()292     private synchronized void clearLoad() {
293         mLoaded = false;
294         mNewPicture = false;
295         mProgress = 0;
296     }
297 
298     /**
299      * Uses a polling mechanism, while pumping messages to check when the
300      * criteria is met.
301      */
waitOnUiThread(long timeout, final Callable<Boolean> doneCriteria)302     private void waitOnUiThread(long timeout, final Callable<Boolean> doneCriteria) {
303         new PollingCheck(timeout) {
304             @Override
305             protected boolean check() {
306                 pumpMessages();
307                 try {
308                     return doneCriteria.call();
309                 } catch (Exception e) {
310                     Assert.fail("Unexpected error while checking the criteria: "
311                             + e.getMessage());
312                     return true;
313                 }
314             }
315         }.run();
316     }
317 
318     /**
319      * Uses a wait/notify to check when the criteria is met.
320      */
waitOnTestThread(long timeout, Callable<Boolean> doneCriteria)321     private synchronized void waitOnTestThread(long timeout, Callable<Boolean> doneCriteria) {
322         try {
323             long waitEnd = SystemClock.uptimeMillis() + timeout;
324             long timeRemaining = timeout;
325             while (!doneCriteria.call() && timeRemaining > 0) {
326                 this.wait(timeRemaining);
327                 timeRemaining = waitEnd - SystemClock.uptimeMillis();
328             }
329             Assert.assertTrue("Action failed to complete before timeout", doneCriteria.call());
330         } catch (InterruptedException e) {
331             // We'll just drop out of the loop and fail
332         } catch (Exception e) {
333             Assert.fail("Unexpected error while checking the criteria: "
334                     + e.getMessage());
335         }
336     }
337 
338     /**
339      * Pumps all currently-queued messages in the UI thread and then exits.
340      * This is useful to force processing while running tests in the UI thread.
341      */
pumpMessages()342     private void pumpMessages() {
343         class ExitLoopException extends RuntimeException {
344         }
345 
346         // Force loop to exit when processing this. Loop.quit() doesn't
347         // work because this is the main Loop.
348         mWebView.getHandler().post(new Runnable() {
349             @Override
350             public void run() {
351                 throw new ExitLoopException(); // exit loop!
352             }
353         });
354         try {
355             // Pump messages until our message gets through.
356             Looper.loop();
357         } catch (ExitLoopException e) {
358         }
359     }
360 
361     /**
362      * Returns true if the current thread is the UI thread based on the
363      * Looper.
364      */
isUiThread()365     private static boolean isUiThread() {
366         return (Looper.myLooper() == Looper.getMainLooper());
367     }
368 
369     /**
370      * A WebChromeClient used to capture the onProgressChanged for use
371      * in waitFor functions. If a test must override the WebChromeClient,
372      * it can derive from this class or call onProgressChanged
373      * directly.
374      */
375     public static class WaitForProgressClient extends WebChromeClient {
376         private WebViewSyncLoader mWebViewSyncLoader;
377 
WaitForProgressClient(WebViewSyncLoader webViewSyncLoader)378         public WaitForProgressClient(WebViewSyncLoader webViewSyncLoader) {
379             mWebViewSyncLoader = webViewSyncLoader;
380         }
381 
382         @Override
383         @CallSuper
onProgressChanged(WebView view, int newProgress)384         public void onProgressChanged(WebView view, int newProgress) {
385             super.onProgressChanged(view, newProgress);
386             mWebViewSyncLoader.onProgressChanged(newProgress);
387         }
388     }
389 
390     /**
391      * A WebViewClient that captures the onPageFinished for use in
392      * waitFor functions. Using initializeWebView sets the WaitForLoadedClient
393      * into the WebView. If a test needs to set a specific WebViewClient and
394      * needs the waitForCompletion capability then it should derive from
395      * WaitForLoadedClient or call WebViewSyncLoader.onPageFinished.
396      */
397     public static class WaitForLoadedClient extends WebViewClient {
398         private WebViewSyncLoader mWebViewSyncLoader;
399 
WaitForLoadedClient(WebViewSyncLoader webViewSyncLoader)400         public WaitForLoadedClient(WebViewSyncLoader webViewSyncLoader) {
401             mWebViewSyncLoader = webViewSyncLoader;
402         }
403 
404         @Override
405         @CallSuper
onPageFinished(WebView view, String url)406         public void onPageFinished(WebView view, String url) {
407             super.onPageFinished(view, url);
408             mWebViewSyncLoader.onPageFinished();
409         }
410 
411         @Override
412         @CallSuper
onPageStarted(WebView view, String url, Bitmap favicon)413         public void onPageStarted(WebView view, String url, Bitmap favicon) {
414             super.onPageStarted(view, url, favicon);
415             mWebViewSyncLoader.onPageStarted();
416         }
417     }
418 
419     /**
420      * A PictureListener that captures the onNewPicture for use in
421      * waitForLoadCompletion. Using initializeWebView sets the PictureListener
422      * into the WebView. If a test needs to set a specific PictureListener and
423      * needs the waitForCompletion capability then it should call
424      * WebViewSyncLoader.onNewPicture.
425      */
426     public static class WaitForNewPicture implements PictureListener {
427         private WebViewSyncLoader mWebViewSyncLoader;
428 
WaitForNewPicture(WebViewSyncLoader webViewSyncLoader)429         public WaitForNewPicture(WebViewSyncLoader webViewSyncLoader) {
430             mWebViewSyncLoader = webViewSyncLoader;
431         }
432 
433         @Override
434         @CallSuper
onNewPicture(WebView view, Picture picture)435         public void onNewPicture(WebView view, Picture picture) {
436             mWebViewSyncLoader.onNewPicture();
437         }
438     }
439 }
440