8

How can we set proxy in Android webview programmatically on latest Kitkat release?

This SO link WebView android proxy talks about version upto SDK version 18. But those solution no more works with Kitkat as underlying webkit implementation is changed and it uses chromium now.

Cœur
  • 37,241
  • 25
  • 195
  • 267
Krishna Kumar
  • 143
  • 1
  • 1
  • 7

6 Answers6

20

Here is my solution:

public static void setKitKatWebViewProxy(Context appContext, String host, int port) {
    System.setProperty("http.proxyHost", host);
    System.setProperty("http.proxyPort", port + "");
    System.setProperty("https.proxyHost", host);
    System.setProperty("https.proxyPort", port + "");
    try {
        Class applictionCls = Class.forName("android.app.Application");
        Field loadedApkField = applictionCls.getDeclaredField("mLoadedApk");
        loadedApkField.setAccessible(true);
        Object loadedApk = loadedApkField.get(appContext);
        Class loadedApkCls = Class.forName("android.app.LoadedApk");
        Field receiversField = loadedApkCls.getDeclaredField("mReceivers");
        receiversField.setAccessible(true);
        ArrayMap receivers = (ArrayMap) receiversField.get(loadedApk);
        for (Object receiverMap : receivers.values()) {
            for (Object rec : ((ArrayMap) receiverMap).keySet()) {
                Class clazz = rec.getClass();
                if (clazz.getName().contains("ProxyChangeListener")) {
                    Method onReceiveMethod = clazz.getDeclaredMethod("onReceive", Context.class, Intent.class);
                    Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION);

                    /*********** optional, may be need in future *************/
                    final String CLASS_NAME = "android.net.ProxyProperties";
                    Class cls = Class.forName(CLASS_NAME);
                    Constructor constructor = cls.getConstructor(String.class, Integer.TYPE, String.class);
                    constructor.setAccessible(true);
                    Object proxyProperties = constructor.newInstance(host, port, null);
                    intent.putExtra("proxy", (Parcelable) proxyProperties);
                    /*********** optional, may be need in future *************/

                    onReceiveMethod.invoke(rec, appContext, intent);
                }
            }
        }
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (IllegalArgumentException e) {
        e.printStackTrace();
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    } catch (InstantiationException e) {
        e.printStackTrace();
    }
}

I hope it can help you.

Note: The Context parameter should be an Application context as the parameter name showed, you could use your own implemented Application instance which extend Application.

xjy2061
  • 513
  • 3
  • 12
  • does not work for me.. java.lang.NoSuchFieldException: mLoadedApk (Neuxs 5, ART) – nubela Jan 23 '14 at 12:11
  • I try you code with android 4.4.2 Emulator but it fail to found `02-24 08:26:56.324: W/System.err(2678): java.lang.NoSuchFieldException: mLoadedApk` What is that "mLoadedApk" in your code. – Herry Feb 24 '14 at 13:27
  • 1
    you should use the application context as the first parameter, as my recent update said. @Herry – xjy2061 Mar 05 '14 at 02:19
  • great job, but I can't seem to use this code to reset the proxy back to normal after setting it a first time. Any ideas? – koenmetsu Apr 05 '14 at 07:09
  • 4
    for reset you can change the first 4 lines to: `Properties properties = System.getProperties(); properties.remove("http.proxyHost"); properties.remove("http.proxyPort"); properties.remove("https.proxyHost"); properties.remove("https.proxyPort");`, and change the optional block to `intent.putExtra("proxy", null);` . @KoMet – xjy2061 Apr 08 '14 at 11:21
  • does anyone have a code for android-l that works? this does not work for android-l – nubela Aug 12 '14 at 19:20
  • @xjy2061 do you have something for android L preview? (since you came up with the one for kitkat). i'm offering +300 bounty for the solution for android L: http://stackoverflow.com/questions/25272393/android-webview-set-proxy-programmatically-on-android-l – nubela Aug 21 '14 at 02:00
  • @xjy2061 This solution still doesn't route the video data from WebView that is being played on the website through the proxy. In fact, the video data from the WebView is not routed through the proxy on any version of Android. I have created a separate question for this here - http://stackoverflow.com/questions/26095946/android-webview-media-player-set-proxy – MediumOne Sep 29 '14 at 08:56
  • I am getting the following error in the console when running the code above. java.lang.ClassCastException: java.util.HashMap cannot be cast to android.support.v4.util.ArrayMap. Any ideas? – njtman Nov 05 '14 at 14:24
  • 1
    @njtman may be you import wrong class, the `ArrayMap` should be android.util.ArrayMap which added in api level 19 – xjy2061 Nov 06 '14 at 03:03
  • @xjy2061 Thanks, sure enough, that was it! – njtman Nov 06 '14 at 13:23
  • This only works for me if I start the app, then close the app by pressing the back button and then restart it from the launcher. Any ideas? – Andrin von Rechenberg Jan 15 '15 at 16:15
  • @xjy2061 Thanks, that set and reset worked in KitKat. Since you have suggested solution for KitKat. Can you suggest something in resetting/ removing proxy in ICS and JB? I am able to set proxy in ICS and JB but not able to reset/ remove it. Here is link to SO question : [http://stackoverflow.com/questions/39718818/how-to-reset-proxy-in-android-webview-for-android-versions-below-than-kitkat] – Perry Sep 27 '16 at 07:41
4

I've made some changes to @xjy2061's answer.

Changes are:

  1. getDeclaredField to getField --> You use this if you declared your own application class. Else it won't find it.

Also, remember to change "com.your.application" to your own application's class canonical name.

private static boolean setKitKatWebViewProxy(WebView webView, String host, int port) {
    Context appContext = webView.getContext().getApplicationContext();
    System.setProperty("http.proxyHost", host);
    System.setProperty("http.proxyPort", port + "");
    System.setProperty("https.proxyHost", host);
    System.setProperty("https.proxyPort", port + "");
    try {
        Class applictionCls = Class.forName("acr.browser.barebones.Jerky");
        Field loadedApkField = applictionCls.getField("mLoadedApk");
        loadedApkField.setAccessible(true);
        Object loadedApk = loadedApkField.get(appContext);
        Class loadedApkCls = Class.forName("android.app.LoadedApk");
        Field receiversField = loadedApkCls.getDeclaredField("mReceivers");
        receiversField.setAccessible(true);
        ArrayMap receivers = (ArrayMap) receiversField.get(loadedApk);
        for (Object receiverMap : receivers.values()) {
            for (Object rec : ((ArrayMap) receiverMap).keySet()) {
                Class clazz = rec.getClass();
                if (clazz.getName().contains("ProxyChangeListener")) {
                    Method onReceiveMethod = clazz.getDeclaredMethod("onReceive", Context.class, Intent.class);
                    Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION);

                    /*********** optional, may be need in future *************/
                    final String CLASS_NAME = "android.net.ProxyProperties";
                    Class cls = Class.forName(CLASS_NAME);
                    Constructor constructor = cls.getConstructor(String.class, Integer.TYPE, String.class);
                    constructor.setAccessible(true);
                    Object proxyProperties = constructor.newInstance(host, port, null);
                    intent.putExtra("proxy", (Parcelable) proxyProperties);
                    /*********** optional, may be need in future *************/

                    onReceiveMethod.invoke(rec, appContext, intent);
                }
            }
        }
        return true;
    } catch (ClassNotFoundException e) {
        StringWriter sw = new StringWriter();
        e.printStackTrace(new PrintWriter(sw));
        String exceptionAsString = sw.toString();
        Log.v(LOG_TAG, e.getMessage());
        Log.v(LOG_TAG, exceptionAsString);
    } catch (NoSuchFieldException e) {
        StringWriter sw = new StringWriter();
        e.printStackTrace(new PrintWriter(sw));
        String exceptionAsString = sw.toString();
        Log.v(LOG_TAG, e.getMessage());
        Log.v(LOG_TAG, exceptionAsString);
    } catch (IllegalAccessException e) {
        StringWriter sw = new StringWriter();
        e.printStackTrace(new PrintWriter(sw));
        String exceptionAsString = sw.toString();
        Log.v(LOG_TAG, e.getMessage());
        Log.v(LOG_TAG, exceptionAsString);
    } catch (IllegalArgumentException e) {
        StringWriter sw = new StringWriter();
        e.printStackTrace(new PrintWriter(sw));
        String exceptionAsString = sw.toString();
        Log.v(LOG_TAG, e.getMessage());
        Log.v(LOG_TAG, exceptionAsString);
    } catch (NoSuchMethodException e) {
        StringWriter sw = new StringWriter();
        e.printStackTrace(new PrintWriter(sw));
        String exceptionAsString = sw.toString();
        Log.v(LOG_TAG, e.getMessage());
        Log.v(LOG_TAG, exceptionAsString);
    } catch (InvocationTargetException e) {
        StringWriter sw = new StringWriter();
        e.printStackTrace(new PrintWriter(sw));
        String exceptionAsString = sw.toString();
        Log.v(LOG_TAG, e.getMessage());
        Log.v(LOG_TAG, exceptionAsString);
    } catch (InstantiationException e) {
        StringWriter sw = new StringWriter();
        e.printStackTrace(new PrintWriter(sw));
        String exceptionAsString = sw.toString();
        Log.v(LOG_TAG, e.getMessage());
        Log.v(LOG_TAG, exceptionAsString);
    }
    return false;
}
nubela
  • 1
  • 24
  • 75
  • 123
2

I am creating a cordova android application, and couldn't figure out why ajax requests to internal hosts on my company's network were failing on KitKat. All native web requests succeeded, and all ajax requests on android versions below 4.4 succeeded aswell. The ajax requests only failed when on the internal company wifi which was even more perplexing.

Turns out KitKat uses a new chrome webview which is different from the standard webviews used in previous android versions. There is a bug in the version of chromium that kitkat uses where it doesn't respect the proxy exclusion list. Our company wifi sets a proxy server, and and excludes all internal hosts. The ajax requests were ultimately failing because authentication to the proxy was failing. Since these requests are to internal hosts, it should have never been going through the proxy to begin with. I was able to adapt xjy2061's answer to fit my usecase.

Hopefully this helps someone in the future and saves them a few days of head banging.

//Set KitKat proxy w/ proxy exclusion.    
@TargetApi(Build.VERSION_CODES.KITKAT)
public static void setKitKatWebViewProxy(Context appContext, String host, int port, String exclusionList) {

    Properties properties = System.getProperties();
    properties.setProperty("http.proxyHost", host);
    properties.setProperty("http.proxyPort", port + "");
    properties.setProperty("https.proxyHost", host);
    properties.setProperty("https.proxyPort", port + "");
    properties.setProperty("http.nonProxyHosts", exclusionList);
    properties.setProperty("https.nonProxyHosts", exclusionList);

    try {
        Class applictionCls = Class.forName("android.app.Application");
        Field loadedApkField = applictionCls.getDeclaredField("mLoadedApk");
        loadedApkField.setAccessible(true);
        Object loadedApk = loadedApkField.get(appContext);
        Class loadedApkCls = Class.forName("android.app.LoadedApk");
        Field receiversField = loadedApkCls.getDeclaredField("mReceivers");
        receiversField.setAccessible(true);
        ArrayMap receivers = (ArrayMap) receiversField.get(loadedApk);
        for (Object receiverMap : receivers.values()) {
            for (Object rec : ((ArrayMap) receiverMap).keySet()) {
                Class clazz = rec.getClass();
                if (clazz.getName().contains("ProxyChangeListener")) {
                    Method onReceiveMethod = clazz.getDeclaredMethod("onReceive", Context.class, Intent.class);
                    Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION);

                    /*********** optional, may be need in future *************/
                    final String CLASS_NAME = "android.net.ProxyProperties";
                    Class cls = Class.forName(CLASS_NAME);
                    Constructor constructor = cls.getConstructor(String.class, Integer.TYPE, String.class);
                    constructor.setAccessible(true);
                    Object proxyProperties = constructor.newInstance(host, port, exclusionList);
                    intent.putExtra("proxy", (Parcelable) proxyProperties);
                    /*********** optional, may be need in future *************/

                    onReceiveMethod.invoke(rec, appContext, intent);
                }
            }
        }
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (IllegalArgumentException e) {
        e.printStackTrace();
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    } catch (InstantiationException e) {
        e.printStackTrace();
    }
}

You would call the method above as follows:

First import this library at the top of your file.

import android.util.ArrayMap;

Then call the method

int currentapiVersion = android.os.Build.VERSION.SDK_INT;
//check first to see if we are running KitKat
if (currentapiVersion >= Build.VERSION_CODES.KITKAT){
    setKitKatWebViewProxy(context, proxy, port, exclusionList);
}
njtman
  • 2,160
  • 1
  • 19
  • 32
1

https://android.googlesource.com/platform/external/chromium/+/android-4.4_r1/net/proxy/proxy_config_service_android.cc

Has methods to set the proxy. I am still trying to figure out how to invoke this from Java code. Pointers?

Karthik
  • 770
  • 1
  • 6
  • 12
0

https://codereview.chromium.org/26763005

Guess from this patch, you'll be able to set up a proxy again in the near future, perhaps.

0

Had some issues with the provided solution on some devices when loading page from onCreate right away after setting the proxy configuration. Opening the web page after some small delay solved the problem. Seems like the proxy config needs some time to get effective.

fabe
  • 466
  • 5
  • 6