/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.webkit.cts; import android.graphics.Bitmap; import android.graphics.Picture; import android.os.Looper; import android.os.SystemClock; import android.webkit.WebChromeClient; import android.webkit.WebView; import android.webkit.WebView.PictureListener; import android.webkit.WebViewClient; import androidx.annotation.CallSuper; import com.android.compatibility.common.util.PollingCheck; import junit.framework.Assert; import java.util.Map; /** * Utility class to simplify tests that need to load data into a WebView and wait for completion * conditions. * * May be used from any thread. */ public class WebViewSyncLoader { /** * Set to true after onPageFinished is called. */ private boolean mLoaded; /** * Set to true after onNewPicture is called. Reset when onPageStarted * is called. */ private boolean mNewPicture; /** * The progress, in percentage, of the page load. Valid values are between * 0 and 100. */ private int mProgress; /** * The WebView that calls will be made on. */ private WebView mWebView; public WebViewSyncLoader(WebView webView) { init(webView, new WaitForLoadedClient(this), new WaitForProgressClient(this), new WaitForNewPicture(this)); } public WebViewSyncLoader( WebView webView, WaitForLoadedClient waitForLoadedClient, WaitForProgressClient waitForProgressClient, WaitForNewPicture waitForNewPicture) { init(webView, waitForLoadedClient, waitForProgressClient, waitForNewPicture); } private void init( final WebView webView, final WaitForLoadedClient waitForLoadedClient, final WaitForProgressClient waitForProgressClient, final WaitForNewPicture waitForNewPicture) { if (!isUiThread()) { WebkitUtils.onMainThreadSync(() -> { init(webView, waitForLoadedClient, waitForProgressClient, waitForNewPicture); }); return; } mWebView = webView; mWebView.setWebViewClient(waitForLoadedClient); mWebView.setWebChromeClient(waitForProgressClient); mWebView.setPictureListener(waitForNewPicture); } /** * Detach listeners from this WebView, undoing the changes made to enable sync loading. */ public void detach() { if (!isUiThread()) { WebkitUtils.onMainThreadSync(this::detach); return; } mWebView.setWebChromeClient(null); mWebView.setWebViewClient(null); mWebView.setPictureListener(null); } /** * Detach listeners. */ public void destroy() { if (!isUiThread()) { WebkitUtils.onMainThreadSync(this::destroy); return; } WebView webView = mWebView; detach(); webView.clearHistory(); webView.clearCache(true); } /** * Accessor for underlying WebView. * @return The WebView being wrapped by this class. */ public WebView getWebView() { return mWebView; } /** * Called from WaitForNewPicture, this is used to indicate that * the page has been drawn. */ public synchronized void onNewPicture() { mNewPicture = true; this.notifyAll(); } /** * Called from WaitForLoadedClient, this is used to clear the picture * draw state so that draws before the URL begins loading don't count. */ public synchronized void onPageStarted() { mNewPicture = false; // Earlier paints won't count. } /** * Called from WaitForLoadedClient, this is used to indicate that * the page is loaded, but not drawn yet. */ public synchronized void onPageFinished() { mLoaded = true; this.notifyAll(); } /** * Called from the WebChrome client, this sets the current progress * for a page. * @param progress The progress made so far between 0 and 100. */ public synchronized void onProgressChanged(int progress) { mProgress = progress; this.notifyAll(); } /** * Calls {@link WebView#loadUrl} on the WebView and then waits for completion. * *
Test fails if the load timeout elapses.
*/
public void loadUrlAndWaitForCompletion(final String url) {
callAndWait(() -> mWebView.loadUrl(url));
}
/**
* Calls {@link WebView#loadUrl(String,Map Test fails if the load timeout elapses.
*/
public void loadUrlAndWaitForCompletion(final String url,
final Map Test fails if the load timeout elapses.
*
* @param url The URL to load.
* @param postData the data will be passed to "POST" request.
*/
public void postUrlAndWaitForCompletion(final String url, final byte[] postData) {
callAndWait(() -> mWebView.postUrl(url, postData));
}
/**
* Calls {@link WebView#loadData(String,String,String)} on the WebView and then waits for
* completion.
*
* Test fails if the load timeout elapses.
*/
public void loadDataAndWaitForCompletion(final String data,
final String mimeType, final String encoding) {
callAndWait(() -> mWebView.loadData(data, mimeType, encoding));
}
/**
* Calls {@link WebView#loadDataWithBaseUrl(String,String,String,String,String)} on the WebView
* and then waits for completion.
*
* Test fails if the load timeout elapses.
*/
public void loadDataWithBaseURLAndWaitForCompletion(final String baseUrl,
final String data, final String mimeType, final String encoding,
final String historyUrl) {
callAndWait(
() -> mWebView.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl));
}
/**
* Reloads a page and waits for it to complete reloading. Use reload
* if it is a form resubmission and the onFormResubmission responds
* by telling WebView not to resubmit it.
*/
public void reloadAndWaitForCompletion() {
callAndWait(() -> mWebView.reload());
}
/**
* Use this only when JavaScript causes a page load to wait for the
* page load to complete. Otherwise use loadUrlAndWaitForCompletion or
* similar functions.
*/
public void waitForLoadCompletion() {
if (isUiThread()) {
waitForLoadCompletionOnUiThread(WebkitUtils.TEST_TIMEOUT_MS);
} else {
waitForLoadCompletionOnTestThread(WebkitUtils.TEST_TIMEOUT_MS);
}
clearLoad();
}
/**
* @return Whether or not the load has finished.
*/
private synchronized boolean isLoaded() {
return mLoaded && mNewPicture && mProgress == 100;
}
/**
* @return A summary of the current loading status for error reporting.
*/
private synchronized String getLoadStatus() {
return "Current load status: mLoaded=" + mLoaded + ", mNewPicture="
+ mNewPicture + ", mProgress=" + mProgress;
}
/**
* Makes a WebView call, waits for completion and then resets the
* load state in preparation for the next load call.
*
* This method may be called on the UI thread.
*
* @param call The call to make on the UI thread prior to waiting.
*/
private void callAndWait(Runnable call) {
Assert.assertTrue("WebViewSyncLoader.load*AndWaitForCompletion calls "
+ "may not be mixed with load* calls directly on WebView "
+ "without calling waitForLoadCompletion after the load",
!isLoaded());
clearLoad(); // clear any extraneous signals from a previous load.
if (isUiThread()) {
call.run();
} else {
WebkitUtils.onMainThread(call);
}
waitForLoadCompletion();
}
/**
* Called whenever a load has been completed so that a subsequent call to
* waitForLoadCompletion doesn't return immediately.
*/
private synchronized void clearLoad() {
mLoaded = false;
mNewPicture = false;
mProgress = 0;
}
/**
* Uses a polling mechanism, while pumping messages to check when the
* load is done.
*/
private void waitForLoadCompletionOnUiThread(long timeout) {
new PollingCheck(timeout) {
@Override
protected boolean check() {
pumpMessages();
try {
return isLoaded();
} catch (Exception e) {
Assert.fail("Unexpected error while checking load completion: "
+ e.getMessage());
return true;
}
}
}.run();
}
/**
* Uses a wait/notify to check when the load is done.
*/
private synchronized void waitForLoadCompletionOnTestThread(long timeout) {
try {
long waitEnd = SystemClock.uptimeMillis() + timeout;
long timeRemaining = timeout;
while (!isLoaded() && timeRemaining > 0) {
this.wait(timeRemaining);
timeRemaining = waitEnd - SystemClock.uptimeMillis();
}
if (!isLoaded()) {
Assert.fail("Action failed to complete before timeout: " + getLoadStatus());
}
} catch (InterruptedException e) {
// We'll just drop out of the loop and fail
} catch (Exception e) {
Assert.fail("Unexpected error while checking load completion: "
+ e.getMessage());
}
}
/**
* Pumps all currently-queued messages in the UI thread and then exits.
* This is useful to force processing while running tests in the UI thread.
*/
private void pumpMessages() {
class ExitLoopException extends RuntimeException {
}
// Pumping messages only makes sense if the current thread is the one we're waiting on.
Assert.assertEquals("Waiting for a WebView event on the wrong thread",
Looper.myLooper(), mWebView.getHandler().getLooper());
// Force loop to exit when processing this. Loop.quit() doesn't
// work because this is the main Loop.
Runnable exitRunnable = new Runnable() {
@Override
public void run() {
throw new ExitLoopException(); // exit loop!
}
};
mWebView.getHandler().post(exitRunnable);
try {
// Pump messages until our message gets through.
Looper.loop();
} catch (ExitLoopException e) {
} finally {
// If we're exiting the loop due to another task throwing an unrelated exception then we
// need to remove the Runnable we added; leaving it in the queue will crash the regular
// event loop if it gets to run. This is a no-op if it's already been removed from the
// queue.
mWebView.getHandler().removeCallbacks(exitRunnable);
}
}
/**
* Returns true if the current thread is the UI thread based on the
* Looper.
*/
private static boolean isUiThread() {
return (Looper.myLooper() == Looper.getMainLooper());
}
/**
* A WebChromeClient used to capture the onProgressChanged for use
* in waitFor functions. If a test must override the WebChromeClient,
* it can derive from this class or call onProgressChanged
* directly.
*/
public static class WaitForProgressClient extends WebChromeClient {
private WebViewSyncLoader mWebViewSyncLoader;
public WaitForProgressClient(WebViewSyncLoader webViewSyncLoader) {
mWebViewSyncLoader = webViewSyncLoader;
}
@Override
@CallSuper
public void onProgressChanged(WebView view, int newProgress) {
super.onProgressChanged(view, newProgress);
mWebViewSyncLoader.onProgressChanged(newProgress);
}
}
/**
* A WebViewClient that captures the onPageFinished for use in
* waitFor functions. Using initializeWebView sets the WaitForLoadedClient
* into the WebView. If a test needs to set a specific WebViewClient and
* needs the waitForCompletion capability then it should derive from
* WaitForLoadedClient or call WebViewSyncLoader.onPageFinished.
*/
public static class WaitForLoadedClient extends WebViewClient {
private WebViewSyncLoader mWebViewSyncLoader;
public WaitForLoadedClient(WebViewSyncLoader webViewSyncLoader) {
mWebViewSyncLoader = webViewSyncLoader;
}
@Override
@CallSuper
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
mWebViewSyncLoader.onPageFinished();
}
@Override
@CallSuper
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
mWebViewSyncLoader.onPageStarted();
}
}
/**
* A PictureListener that captures the onNewPicture for use in
* waitForLoadCompletion. Using initializeWebView sets the PictureListener
* into the WebView. If a test needs to set a specific PictureListener and
* needs the waitForCompletion capability then it should call
* WebViewSyncLoader.onNewPicture.
*/
public static class WaitForNewPicture implements PictureListener {
private WebViewSyncLoader mWebViewSyncLoader;
public WaitForNewPicture(WebViewSyncLoader webViewSyncLoader) {
mWebViewSyncLoader = webViewSyncLoader;
}
@Override
@CallSuper
public void onNewPicture(WebView view, Picture picture) {
mWebViewSyncLoader.onNewPicture();
}
}
}