Friday, January 14, 2011

Android WebView Proxy setting




Android webview is shaky at best - starting from missing root certificates, weird UI display issues like double text boxes and the most shaky of all - the connectivity itself. Apparently the webkit engine connectivity breaks very frequently with some(slow?) networks. Same issue exist in the andorid web browser application as well as webview reuses the same native browser implementation.
In some cases, the culprit is a slow proxy setup by the network provider by default. Bypassing the proxy could potentially fix that issue, but unfortunately there are no direct APIs to do that programatically, (WebView.enablePlatformNotifications(); require the user to manually enter proxy settings) so the below hack using reflection. The idea is to reset the proxyHost field in the android.net.http.RequestQueue instance within the Network object, so the WebKit engine finds it and sends the request accordingly. Code below.  (Warning: code below uses private APIs)

import android.content.Context;
import org.apache.http.HttpHost;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

/**
 * Utility class for setting WebKit proxy used by Android WebView
 *
 */
public class ProxySettings {

    /**
     * Override WebKit Proxy settings
     *
     * @param ctx Android ApplicationContext
     * @param host
     * @param port
     * @return  true if Proxy was successfully set
     */
    public static boolean setProxy(Context ctx, String host, int port) {
        boolean ret = false;
        try {
            Object requestQueueObject = getRequestQueue(ctx);
            if (requestQueueObject != null) {
                //Create Proxy config object and set it into request Q
                HttpHost httpHost = new HttpHost(host, port, "http");
                setDeclaredField(requestQueueObject, "mProxyHost", httpHost);
                ret = true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ret;
    }

    public static void resetProxy(Context ctx) throws Exception {
        Object requestQueueObject = getRequestQueue(ctx);
        if (requestQueueObject != null) {
            setDeclaredField(requestQueueObject, "mProxyHost", null);
        }
    }

    public static Object getRequestQueue(Context ctx) throws Exception {
        Object ret = null;
        Class networkClass = Class.forName("android.webkit.Network");
        if (networkClass != null) {
            Object networkObj = invokeMethod(networkClass, "getInstance", new Object[]{ctx}, Context.class);
            if (networkObj != null) {
                ret = getDeclaredField(networkObj, "mRequestQueue");
            }
        }
        return ret;
    }

    private static Object getDeclaredField(Object obj, String name)
            throws SecurityException, NoSuchFieldException,
            IllegalArgumentException, IllegalAccessException {
        Field f = obj.getClass().getDeclaredField(name);
        f.setAccessible(true);
        Object out = f.get(obj);
        //System.out.println(obj.getClass().getName() + "." + name + " = "+ out);
        return out;
    }

    private static void setDeclaredField(Object obj, String name, Object value)
            throws SecurityException, NoSuchFieldException,
            IllegalArgumentException, IllegalAccessException {
        Field f = obj.getClass().getDeclaredField(name);
        f.setAccessible(true);
        f.set(obj, value);
    }

    private static Object invokeMethod(Object object, String methodName, Object[] params, Class... types) throws Exception {
        Object out = null;
        Class c = object instanceof Class ? (Class) object : object.getClass();
        if (types != null) {
            Method method = c.getMethod(methodName, types);
            out = method.invoke(object, params);
        } else {
            Method method = c.getMethod(methodName);
            out = method.invoke(object);
        }
        //System.out.println(object.getClass().getName() + "." + methodName + "() = "+ out);
        return out;
    }
}

9 comments:

luk17 said...

This class could be used to redirect all requests through a specific proxy, chose at runtime? If yes, how?

n8fr8 said...

Wow, brilliant idea! It is working well for me here. I think we are going to incorporate this into Orweb, our privacy+proxying web browser, as since Android 1.6, we had no way of controlling proxy settings. This hack works great for now.

https://guardianproject.info/apps/orweb/

El Poeta Guarro said...

Hi, nice post, is there a way to setup proxy authentication using this code

Anonymous said...

Hi. Nice trick! I've tried on ICS 4.0 and seems not work. Some tip?

Anonymous said...

Does this still work? Trying on 2.3.3 and no luck. Do you happen to have a full example? Perhaps I am not calling a method correctly.

Anonymous said...

Not work on ICS 4.0? Can anyone help?

Unknown said...

Thank you for the illuminating post! I just implemented your suggestion into Zirco Browser and setting the proxy settings with app like Proxy Settings I can finally surf the web from my company network!

Max said...

for SDK > 14, use these codes:

Class webViewCoreClass = Class.forName("android.webkit.WebViewCore");
Class proxyPropertiesClass = Class.forName("android.net.ProxyProperties");
if (webViewCoreClass != null && proxyPropertiesClass != null) {
Method m = webViewCoreClass.getDeclaredMethod("sendStaticMessage", Integer.TYPE,
Object.class);
Constructor c = proxyPropertiesClass.getConstructor(String.class, Integer.TYPE,
String.class);
m.setAccessible(true);
c.setAccessible(true);
Object properties = c.newInstance(host, port, null);
m.invoke(null, PROXY_CHANGED, properties);
return true;
}

Anonymous said...

Max
I tried using the sendStaticMessage method , it didn't work for me . i assumed that PROXY_CHANGED --> WebViewCore.EventHub.PROXY_CHANGED is that right ? i am trying to look into the webkit and webviewcore source files. any tips/advice will be really helpful. Thanks in Advance
-Vj