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