blob: 6e817c3fef7ad70dfa7a4a51d2369384e1d679fc [file] [log] [blame]
/*
* Copyright 2018 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 androidx.webkit;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import android.graphics.Bitmap;
import android.os.Looper;
import android.os.SystemClock;
import android.support.test.InstrumentationRegistry;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import java.util.concurrent.Callable;
class WebViewOnUiThread {
/**
* The maximum time, in milliseconds (10 seconds) to wait for a load
* to be triggered.
*/
private static final long LOAD_TIMEOUT = 10000;
/**
* Set to true after onPageFinished is called.
*/
private boolean mLoaded;
/**
* 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 WebViewOnUiThread() {
InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
mWebView = new WebView(InstrumentationRegistry.getTargetContext());
mWebView.setWebViewClient(new WaitForLoadedClient(WebViewOnUiThread.this));
mWebView.setWebChromeClient(new WaitForProgressClient(WebViewOnUiThread.this));
}
});
}
public void loadUrl(final String url) {
InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
mWebView.loadUrl(url);
}
});
}
public void setWebViewClient(final WebViewClient webviewClient) {
InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
mWebView.setWebViewClient(webviewClient);
}
});
}
public void postVisualStateCallbackCompat(final long requestId,
final WebViewCompat.VisualStateCallback callback) {
InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
WebViewCompat.postVisualStateCallback(mWebView, requestId, callback);
}
});
}
public WebSettings getSettings() {
return getValue(new ValueGetter<WebSettings>() {
@Override
public WebSettings capture() {
return mWebView.getSettings();
}
});
}
public void addJavascriptInterface(final Object object, final String name) {
InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
mWebView.addJavascriptInterface(object, name);
}
});
}
/**
* Called after a test is complete and the WebView should be disengaged from
* the tests.
*/
public void cleanUp() {
InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
mWebView.clearHistory();
mWebView.clearCache(true);
mWebView.setWebChromeClient(null);
mWebView.setWebViewClient(null);
mWebView.destroy();
}
});
}
WebView getWebViewOnCurrentThread() {
return mWebView;
}
/**
* Called from WaitForLoadedClient.
*/
synchronized void onPageStarted() {}
/**
* Called from WaitForLoadedClient, this is used to indicate that
* the page is loaded, but not drawn yet.
*/
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.
*/
synchronized void onProgressChanged(int progress) {
mProgress = progress;
this.notifyAll();
}
/**
* Calls loadUrl on the WebView and then waits onPageFinished,
* onNewPicture and onProgressChange to reach 100.
* Test fails if the load timeout elapses.
* @param url The URL to load.
*/
void loadUrlAndWaitForCompletion(final String url) {
callAndWait(new Runnable() {
@Override
public void run() {
mWebView.loadUrl(url);
}
});
}
/**
* Use this only when JavaScript causes a page load to wait for the
* page load to complete. Otherwise use loadUrlAndWaitForCompletion or
* similar functions.
*/
void waitForLoadCompletion() {
waitForCriteria(LOAD_TIMEOUT,
new Callable<Boolean>() {
@Override
public Boolean call() {
return isLoaded();
}
});
clearLoad();
}
private void waitForCriteria(long timeout, Callable<Boolean> doneCriteria) {
if (isUiThread()) {
waitOnUiThread(timeout, doneCriteria);
} else {
waitOnTestThread(timeout, doneCriteria);
}
}
void evaluateJavascript(final String script, final ValueCallback<String> result) {
InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
mWebView.evaluateJavascript(script, result);
}
});
}
/**
* Returns true if the current thread is the UI thread based on the
* Looper.
*/
private static boolean isUiThread() {
return (Looper.myLooper() == Looper.getMainLooper());
}
/**
* @return Whether or not the load has finished.
*/
private synchronized boolean isLoaded() {
return mLoaded && mProgress == 100;
}
/**
* Makes a WebView call, waits for completion and then resets the
* load state in preparation for the next load call.
* @param call The call to make on the UI thread prior to waiting.
*/
private void callAndWait(Runnable call) {
assertTrue("WebViewOnUiThread.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.
InstrumentationRegistry.getInstrumentation().runOnMainSync(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;
mProgress = 0;
}
/**
* Uses a polling mechanism, while pumping messages to check when the
* criteria is met.
*/
private void waitOnUiThread(long timeout, final Callable<Boolean> doneCriteria) {
new PollingCheck(timeout) {
@Override
protected boolean check() {
pumpMessages();
try {
return doneCriteria.call();
} catch (Exception e) {
fail("Unexpected error while checking the criteria: " + e.getMessage());
return true;
}
}
}.run();
}
/**
* Uses a wait/notify to check when the criteria is met.
*/
private synchronized void waitOnTestThread(long timeout, Callable<Boolean> doneCriteria) {
try {
long waitEnd = SystemClock.uptimeMillis() + timeout;
long timeRemaining = timeout;
while (!doneCriteria.call() && timeRemaining > 0) {
this.wait(timeRemaining);
timeRemaining = waitEnd - SystemClock.uptimeMillis();
}
assertTrue("Action failed to complete before timeout", doneCriteria.call());
} catch (InterruptedException e) {
// We'll just drop out of the loop and fail
} catch (Exception e) {
fail("Unexpected error while checking the criteria: " + 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 {
}
// Force loop to exit when processing this. Loop.quit() doesn't
// work because this is the main Loop.
mWebView.getHandler().post(new Runnable() {
@Override
public void run() {
throw new ExitLoopException(); // exit loop!
}
});
try {
// Pump messages until our message gets through.
Looper.loop();
} catch (ExitLoopException e) {
}
}
private <T> T getValue(ValueGetter<T> getter) {
InstrumentationRegistry.getInstrumentation().runOnMainSync(getter);
return getter.getValue();
}
private abstract class ValueGetter<T> implements Runnable {
private T mValue;
@Override
public void run() {
mValue = capture();
}
protected abstract T capture();
public T getValue() {
return mValue;
}
}
/**
* 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 WebViewOnUiThread mOnUiThread;
WaitForProgressClient(WebViewOnUiThread onUiThread) {
mOnUiThread = onUiThread;
}
@Override
public void onProgressChanged(WebView view, int newProgress) {
super.onProgressChanged(view, newProgress);
mOnUiThread.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 WebViewOnUiThread.onPageFinished.
*/
public static class WaitForLoadedClient extends WebViewClient {
private WebViewOnUiThread mOnUiThread;
WaitForLoadedClient(WebViewOnUiThread onUiThread) {
mOnUiThread = onUiThread;
}
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
mOnUiThread.onPageFinished();
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
mOnUiThread.onPageStarted();
}
}
}