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