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