Java tutorial
/* * Copyright (C) 2015 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.support.test.espresso.web.action; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import android.support.test.espresso.web.bridge.Conduit; import android.support.test.espresso.web.bridge.JavaScriptBridge; import android.support.test.espresso.web.model.Evaluation; import android.support.test.espresso.web.model.ModelCodec; import android.support.test.espresso.web.model.WindowReference; import com.google.common.base.Function; import com.google.common.util.concurrent.AbstractFuture; import com.google.common.util.concurrent.AsyncFunction; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.webkit.ValueCallback; import android.webkit.WebHistoryItem; import android.webkit.WebView; import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.regex.Pattern; import android.support.annotation.Nullable; /** * Wraps scripts into WebDriver atoms, which are used to ensure consistent behaviour cross-browser. */ final class JavascriptEvaluation { private JavascriptEvaluation() { } private static final ScriptPreparer SCRIPT_PREPARER; private static final AsyncFunction<PreparedScript, String> RAW_EVALUATOR; private static final Function<String, Evaluation> DECODE_EVALUATION = new Function<String, Evaluation>() { @Override public Evaluation apply(String in) { return ModelCodec.decodeEvaluation(in); } }; private static final int SANITIZER_SYNC = 1; private static final Handler MAIN_HANDLER = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message m) { switch (m.what) { case SANITIZER_SYNC: ((SanitizerTask) m.obj).sanitizerSync(); break; } } }; static { if (Build.VERSION.SDK_INT < 19) { SCRIPT_PREPARER = new ScriptPreparer(true); RAW_EVALUATOR = new AsyncConduitEvaluation(); } else { SCRIPT_PREPARER = new ScriptPreparer(false); RAW_EVALUATOR = new AsyncJavascriptEvaluation(); } } /** * Evaluates a script on a given WebView. * * Scripts are only evaluated when a WebView is deemed sane. That is: * <ul> * <li>The WebView's back/forward list's last item agrees with the WebView</li> * <li>The WebView's reported content height is non-zero</li> * <li>The WebView's reported progress is 100</li> * <li>The document.documentElement object for the DOM of the selected window is non-null</li> * <ul> * * Scripts are evaluated on the WebKit/Chromium thread (that is - not the Main thread). * A Future is returned which contains the result of the evaluation. */ static ListenableFuture<Evaluation> evaluate(final WebView view, final String script, final List<? extends Object> arguments, @Nullable final WindowReference window) { UnpreparedScript unprepared = new UnpreparedScript(view, script, arguments, window); SanitizerTask sanitizer = new SanitizerTask(unprepared); view.post(sanitizer); ListenableFuture<PreparedScript> preparedScript = Futures.transform(sanitizer, SCRIPT_PREPARER); ListenableFuture<String> rawEvaluation = Futures.transform(preparedScript, RAW_EVALUATOR); ListenableFuture<Evaluation> parsedEvaluation = Futures.transform(rawEvaluation, DECODE_EVALUATION); return parsedEvaluation; } /** * Ensures the WebView meetings minimum sanity guidelines. */ private static class SanitizerTask extends AbstractFuture<UnpreparedScript> implements Runnable { private static final String DOC_ELEMENT_PRESENT = "return document.documentElement != null && document.readyState === 'complete'"; private static final int DELAY = 100; private final UnpreparedScript unprepared; private String sanityMessage = ""; private int count; public SanitizerTask(UnpreparedScript unprepared) { this.unprepared = checkNotNull(unprepared); count = 0; } @Override public void run() { if (Looper.myLooper() != Looper.getMainLooper()) { unprepared.view.post(this); } else { try { innerSanity(); } catch (RuntimeException re) { setException(re); } } } void sanitizerSync() { if (isWebViewSane()) { set(unprepared); } else { // try again! unprepared.view.post(this); } } private void innerSanity() { count++; checkState(count < 250, "Waited over: %s millis but webview never went sane: %s", 250 * DELAY, sanityMessage); if (isWebViewSane()) { PreparedScript docCheckScript = SCRIPT_PREPARER.apply(new UnpreparedScript(unprepared.view, DOC_ELEMENT_PRESENT, Collections.EMPTY_LIST, unprepared.window)); ListenableFuture<String> futureRaw = null; try { futureRaw = RAW_EVALUATOR.apply(docCheckScript); } catch (Exception e) { setException(e); return; } final ListenableFuture<Evaluation> futureParsed = Futures.transform(futureRaw, DECODE_EVALUATION); futureParsed.addListener(new Runnable() { @Override public void run() { try { Evaluation eval = futureParsed.get(); if (eval.getStatus() == 0) { if ((Boolean) eval.getValue()) { if (Build.VERSION.SDK_INT == 10) { set(unprepared); } else { // webview seems ready, but force it to respond to a requestFocusNodeHref call // and check if it is still sane after the response. // This works around flakes in API 15 where progress updates may not be sent // without a requestFocusNodeHref call. unprepared.view.post(new Runnable() { @Override public void run() { unprepared.view.requestFocusNodeHref(MAIN_HANDLER .obtainMessage(SANITIZER_SYNC, SanitizerTask.this)); } }); } } else { unprepared.view.postDelayed(SanitizerTask.this, DELAY); } } else { setException( new RuntimeException("Fatal exception checking document state: " + eval)); } } catch (ExecutionException ee) { setException(ee.getCause()); } catch (InterruptedException ie) { setException(ie.getCause()); } } }, MoreExecutors.sameThreadExecutor()); } else { unprepared.view.postDelayed(this, DELAY); } } private boolean isWebViewSane() { String url = unprepared.view.getUrl(); WebHistoryItem current = unprepared.view.copyBackForwardList().getCurrentItem(); boolean getUrlReady = url != null; boolean webHistoryReady = current != null; if (getUrlReady && webHistoryReady) { String historyUrl = current.getUrl(); boolean viewAndHistoryMatch = url.equals(historyUrl); boolean nonZeroContentHeight = unprepared.view.getContentHeight() != 0; boolean progressComplete = unprepared.view.getProgress() == 100; sanityMessage = String.format( "viewAndHistoryUrlsMatch: %s, nonZeroContentHeight: %s, progressComplete: %s", viewAndHistoryMatch, nonZeroContentHeight, progressComplete); return viewAndHistoryMatch && progressComplete && nonZeroContentHeight; } else { sanityMessage = String.format( "view.getUrl() != null: %s view.copyBackForwardList().getCurrentItem() != null: %s", getUrlReady, webHistoryReady); } return false; } } /** * Contains the raw script, it's arguments, and the webview to run it against. */ private static class UnpreparedScript { private final WebView view; private final String script; private final List<? extends Object> args; @Nullable private final WindowReference window; UnpreparedScript(WebView view, String script, List<? extends Object> args, @Nullable WindowReference window) { this.view = checkNotNull(view); this.script = checkNotNull(script); this.args = checkNotNull(args); this.window = window; } } /** * Contains a script which has been wrapped with the EXECUTE_SCRIPT atom, has been properly * escaped, and potentially conduitized. */ private static class PreparedScript { private final WebView view; private final String script; @Nullable private final Conduit conduit; PreparedScript(WebView view, String script, @Nullable Conduit conduit) { this.view = checkNotNull(view); this.script = checkNotNull(script); this.conduit = conduit; } } private static final class ScriptPreparer implements Function<UnpreparedScript, PreparedScript> { private final boolean conduitize; public ScriptPreparer(boolean conduitize) { this.conduitize = conduitize; } @Override public PreparedScript apply(UnpreparedScript unprepared) { StringBuilder atomized = atomize(unprepared.script, unprepared.args, unprepared.window); Conduit conduit = null; if (conduitize) { conduit = JavaScriptBridge.makeConduit(); atomized = conduit.wrapScriptInConduit(atomized).insert(0, "javascript:"); } return new PreparedScript(unprepared.view, atomized.toString(), conduit); } private StringBuilder atomize(String script, List<? extends Object> args, WindowReference windowReference) { int guessedSize = EvaluationAtom.EXECUTE_SCRIPT_ANDROID.length() + script.length() + 1024; if (windowReference != null) { guessedSize += EvaluationAtom.GET_ELEMENT_ANDROID.length(); } StringBuilder toExecute = new StringBuilder(guessedSize).append("var my_wind = "); if (windowReference != null) { toExecute.append("(").append(EvaluationAtom.GET_ELEMENT_ANDROID).append(")(") .append(ModelCodec.encode(windowReference)).append("[\"WINDOW\"]);"); } else { toExecute.append("null;"); } toExecute.append("return (").append(EvaluationAtom.EXECUTE_SCRIPT_ANDROID).append(")("); escapeAndQuote(toExecute, script).append(",").append(ModelCodec.encode(args)).append(",") .append(conduitize) // JSON.stringify at webdriver level. Necessary for conduits. .append(",").append("my_wind)"); return wrapInFunction(toExecute); } private StringBuilder wrapInFunction(StringBuilder script) { script.insert(0, "(function(){").append("})()"); return script; } private static final Pattern FUNCTION_PATTERN = Pattern.compile("^\\s*function\\s*\\w*\\s*\\(.*\\}\\s*$", Pattern.DOTALL | Pattern.MULTILINE); static boolean isFunctionDefinition(String script) { return FUNCTION_PATTERN.matcher(script).matches(); } private StringBuilder escapeAndQuote(StringBuilder scriptBuffer, String toWrap) { scriptBuffer.append("\""); boolean isFunction = isFunctionDefinition(toWrap); if (isFunction) { scriptBuffer.append("return ("); } for (int i = 0; i < toWrap.length(); i++) { char c = toWrap.charAt(i); switch (c) { case '\"': // literally: " case '\'': // literally: ' case '\\': // literally: \ scriptBuffer.append('\\').append(c); break; case '\n': // literally a unix-newline. scriptBuffer.append("\\n"); break; case '\r': scriptBuffer.append("\\r"); break; case '\u2028': scriptBuffer.append("\\u2028"); break; case '\u2029': scriptBuffer.append("\\u2029"); break; default: scriptBuffer.append(c); } } if (isFunction) { scriptBuffer.append(").apply(null,arguments);"); } scriptBuffer.append("\""); return scriptBuffer; } } private static final class AsyncConduitEvaluation implements AsyncFunction<PreparedScript, String> { @Override public ListenableFuture<String> apply(final PreparedScript in) { if (null == in.conduit) { return Futures.<String>immediateFailedFuture(new RuntimeException("Not a conduit script!")); } else { if (Looper.myLooper() == Looper.getMainLooper()) { in.view.loadUrl(in.script); } else { in.view.post(new Runnable() { @Override public void run() { in.view.loadUrl(in.script); } }); } return in.conduit.getResult(); } } }; private static final class AsyncJavascriptEvaluation implements AsyncFunction<PreparedScript, String> { @Override public ListenableFuture<String> apply(final PreparedScript in) { if (null != in.conduit) { return Futures.<String>immediateFailedFuture(new RuntimeException("Conduit script cannot be used")); } else { final ValueCallbackFuture<String> result = new ValueCallbackFuture<String>(); if (Looper.myLooper() == Looper.getMainLooper()) { in.view.evaluateJavascript(in.script, result); } else { in.view.post(new Runnable() { @Override public void run() { in.view.evaluateJavascript(in.script, result); } }); } return result; } } } private static class ValueCallbackFuture<V> extends AbstractFuture<V> implements ValueCallback<V> { @Override public void onReceiveValue(V value) { set(value); } } }