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