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 // Pumping messages only makes sense if the current thread is the one we're waiting on. 347 Assert.assertEquals("Waiting for a WebView event on the wrong thread", 348 Looper.myLooper(), mWebView.getHandler().getLooper()); 349 350 // Force loop to exit when processing this. Loop.quit() doesn't 351 // work because this is the main Loop. 352 Runnable exitRunnable = new Runnable() { 353 @Override 354 public void run() { 355 throw new ExitLoopException(); // exit loop! 356 } 357 }; 358 mWebView.getHandler().post(exitRunnable); 359 try { 360 // Pump messages until our message gets through. 361 Looper.loop(); 362 } catch (ExitLoopException e) { 363 } finally { 364 // If we're exiting the loop due to another task throwing an unrelated exception then we 365 // need to remove the Runnable we added; leaving it in the queue will crash the regular 366 // event loop if it gets to run. This is a no-op if it's already been removed from the 367 // queue. 368 mWebView.getHandler().removeCallbacks(exitRunnable); 369 } 370 } 371 372 /** 373 * Returns true if the current thread is the UI thread based on the 374 * Looper. 375 */ isUiThread()376 private static boolean isUiThread() { 377 return (Looper.myLooper() == Looper.getMainLooper()); 378 } 379 380 /** 381 * A WebChromeClient used to capture the onProgressChanged for use 382 * in waitFor functions. If a test must override the WebChromeClient, 383 * it can derive from this class or call onProgressChanged 384 * directly. 385 */ 386 public static class WaitForProgressClient extends WebChromeClient { 387 private WebViewSyncLoader mWebViewSyncLoader; 388 WaitForProgressClient(WebViewSyncLoader webViewSyncLoader)389 public WaitForProgressClient(WebViewSyncLoader webViewSyncLoader) { 390 mWebViewSyncLoader = webViewSyncLoader; 391 } 392 393 @Override 394 @CallSuper onProgressChanged(WebView view, int newProgress)395 public void onProgressChanged(WebView view, int newProgress) { 396 super.onProgressChanged(view, newProgress); 397 mWebViewSyncLoader.onProgressChanged(newProgress); 398 } 399 } 400 401 /** 402 * A WebViewClient that captures the onPageFinished for use in 403 * waitFor functions. Using initializeWebView sets the WaitForLoadedClient 404 * into the WebView. If a test needs to set a specific WebViewClient and 405 * needs the waitForCompletion capability then it should derive from 406 * WaitForLoadedClient or call WebViewSyncLoader.onPageFinished. 407 */ 408 public static class WaitForLoadedClient extends WebViewClient { 409 private WebViewSyncLoader mWebViewSyncLoader; 410 WaitForLoadedClient(WebViewSyncLoader webViewSyncLoader)411 public WaitForLoadedClient(WebViewSyncLoader webViewSyncLoader) { 412 mWebViewSyncLoader = webViewSyncLoader; 413 } 414 415 @Override 416 @CallSuper onPageFinished(WebView view, String url)417 public void onPageFinished(WebView view, String url) { 418 super.onPageFinished(view, url); 419 mWebViewSyncLoader.onPageFinished(); 420 } 421 422 @Override 423 @CallSuper onPageStarted(WebView view, String url, Bitmap favicon)424 public void onPageStarted(WebView view, String url, Bitmap favicon) { 425 super.onPageStarted(view, url, favicon); 426 mWebViewSyncLoader.onPageStarted(); 427 } 428 } 429 430 /** 431 * A PictureListener that captures the onNewPicture for use in 432 * waitForLoadCompletion. Using initializeWebView sets the PictureListener 433 * into the WebView. If a test needs to set a specific PictureListener and 434 * needs the waitForCompletion capability then it should call 435 * WebViewSyncLoader.onNewPicture. 436 */ 437 public static class WaitForNewPicture implements PictureListener { 438 private WebViewSyncLoader mWebViewSyncLoader; 439 WaitForNewPicture(WebViewSyncLoader webViewSyncLoader)440 public WaitForNewPicture(WebViewSyncLoader webViewSyncLoader) { 441 mWebViewSyncLoader = webViewSyncLoader; 442 } 443 444 @Override 445 @CallSuper onNewPicture(WebView view, Picture picture)446 public void onNewPicture(WebView view, Picture picture) { 447 mWebViewSyncLoader.onNewPicture(); 448 } 449 } 450 } 451