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