package org.robolectric.shadows; import android.view.ViewGroup.LayoutParams; import android.webkit.WebChromeClient; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.annotation.RealObject; import org.robolectric.annotation.HiddenApi; import org.robolectric.fakes.RoboWebSettings; import org.robolectric.util.ReflectionHelpers; import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Collections; import java.util.HashMap; import java.util.Map; @SuppressWarnings({"UnusedDeclaration"}) @Implements(value = WebView.class, inheritImplementationMethods = true) public class ShadowWebView extends ShadowAbsoluteLayout { @RealObject private WebView realWebView; private String lastUrl; private Map<String, String> lastAdditionalHttpHeaders; private HashMap<String, Object> javascriptInterfaces = new HashMap<>(); private WebSettings webSettings = new RoboWebSettings(); private WebViewClient webViewClient = null; private boolean runFlag = false; private boolean clearCacheCalled = false; private boolean clearCacheIncludeDiskFiles = false; private boolean clearFormDataCalled = false; private boolean clearHistoryCalled = false; private boolean clearViewCalled = false; private boolean destroyCalled = false; private boolean onPauseCalled = false; private boolean onResumeCalled = false; private WebChromeClient webChromeClient; private boolean canGoBack; private int goBackInvocations = 0; private LoadData lastLoadData; private LoadDataWithBaseURL lastLoadDataWithBaseURL; @HiddenApi @Implementation public void ensureProviderCreated() { final ClassLoader classLoader = getClass().getClassLoader(); Class<?> webViewProviderClass = getClassNamed("android.webkit.WebViewProvider"); Field mProvider; try { mProvider = WebView.class.getDeclaredField("mProvider"); mProvider.setAccessible(true); if (mProvider.get(realView) == null) { Object provider = Proxy.newProxyInstance(classLoader, new Class[]{webViewProviderClass}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getName().equals("getViewDelegate") || method.getName().equals("getScrollDelegate")) { return Proxy.newProxyInstance(classLoader, new Class[]{ getClassNamed("android.webkit.WebViewProvider$ViewDelegate"), getClassNamed("android.webkit.WebViewProvider$ScrollDelegate") }, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return nullish(method); } }); } return nullish(method); } }); mProvider.set(realView, provider); } } catch (NoSuchFieldException | IllegalAccessException e) { throw new RuntimeException(e); } } @Implementation public void setLayoutParams(LayoutParams params) { ReflectionHelpers.setField(realWebView, "mLayoutParams", params); } private Object nullish(Method method) { Class<?> returnType = method.getReturnType(); if (returnType.equals(long.class) || returnType.equals(double.class) || returnType.equals(int.class) || returnType.equals(float.class) || returnType.equals(short.class) || returnType.equals(byte.class) ) return 0; if (returnType.equals(char.class)) return '\0'; if (returnType.equals(boolean.class)) return false; return null; } private Class<?> getClassNamed(String className) { try { return getClass().getClassLoader().loadClass(className); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } @Implementation public void loadUrl(String url) { loadUrl(url, null); } @Implementation public void loadUrl(String url, Map<String, String> additionalHttpHeaders) { lastUrl = url; if (additionalHttpHeaders != null) { this.lastAdditionalHttpHeaders = Collections.unmodifiableMap(additionalHttpHeaders); } else { this.lastAdditionalHttpHeaders = null; } } @Implementation public void loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String historyUrl) { lastLoadDataWithBaseURL = new LoadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl); } @Implementation public void loadData(String data, String mimeType, String encoding) { lastLoadData = new LoadData(data, mimeType, encoding); } /** * @return the last loaded url */ public String getLastLoadedUrl() { return lastUrl; } /** * @return the additional Http headers that in the same request with last loaded url */ public Map<String, String> getLastAdditionalHttpHeaders() { return lastAdditionalHttpHeaders; } @Implementation public WebSettings getSettings() { return webSettings; } @Implementation public void setWebViewClient(WebViewClient client) { webViewClient = client; } @Implementation public void setWebChromeClient(WebChromeClient client) { webChromeClient = client; } public WebViewClient getWebViewClient() { return webViewClient; } @Implementation public void addJavascriptInterface(Object obj, String interfaceName) { javascriptInterfaces.put(interfaceName, obj); } public Object getJavascriptInterface(String interfaceName) { return javascriptInterfaces.get(interfaceName); } @Implementation public void clearCache(boolean includeDiskFiles) { clearCacheCalled = true; clearCacheIncludeDiskFiles = includeDiskFiles; } public boolean wasClearCacheCalled() { return clearCacheCalled; } public boolean didClearCacheIncludeDiskFiles() { return clearCacheIncludeDiskFiles; } @Implementation public void clearFormData() { clearFormDataCalled = true; } public boolean wasClearFormDataCalled() { return clearFormDataCalled; } @Implementation public void clearHistory() { clearHistoryCalled = true; } public boolean wasClearHistoryCalled() { return clearHistoryCalled; } @Implementation public void clearView() { clearViewCalled = true; } public boolean wasClearViewCalled() { return clearViewCalled; } @Implementation public void onPause(){ onPauseCalled = true; } public boolean wasOnPauseCalled() { return onPauseCalled; } @Implementation public void onResume() { onResumeCalled = true; } public boolean wasOnResumeCalled() { return onResumeCalled; } @Implementation public void destroy() { destroyCalled = true; } public boolean wasDestroyCalled() { return destroyCalled; } @Implementation public void post(Runnable action) { action.run(); runFlag = true; } public boolean getRunFlag() { return runFlag; } /** * @return webChromeClient */ public WebChromeClient getWebChromeClient() { return webChromeClient; } @Implementation public boolean canGoBack() { return canGoBack; } @Implementation public void goBack() { goBackInvocations++; } @Implementation public static String findAddress(String addr) { return null; } /** * @return goBackInvocations the number of times {@code android.webkit.WebView#goBack()} * was invoked */ public int getGoBackInvocations() { return goBackInvocations; } /** * Sets the value to return from {@code android.webkit.WebView#canGoBack()} * * @param canGoBack Value to return from {@code android.webkit.WebView#canGoBack()} */ public void setCanGoBack(boolean canGoBack) { this.canGoBack = canGoBack; } public LoadData getLastLoadData() { return lastLoadData; } public LoadDataWithBaseURL getLastLoadDataWithBaseURL() { return lastLoadDataWithBaseURL; } public static void setWebContentsDebuggingEnabled(boolean enabled) { } public class LoadDataWithBaseURL { public final String baseUrl; public final String data; public final String mimeType; public final String encoding; public final String historyUrl; public LoadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String historyUrl) { this.baseUrl = baseUrl; this.data = data; this.mimeType = mimeType; this.encoding = encoding; this.historyUrl = historyUrl; } } public class LoadData { public final String data; public final String mimeType; public final String encoding; public LoadData(String data, String mimeType, String encoding) { this.data = data; this.mimeType = mimeType; this.encoding = encoding; } } }