1// Copyright 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package org.chromium.net;
6
7import android.content.BroadcastReceiver;
8import android.content.Context;
9import android.content.Intent;
10import android.content.IntentFilter;
11import android.net.Proxy;
12import android.net.Uri;
13import android.os.Build;
14import android.text.TextUtils;
15import android.util.Log;
16
17import org.chromium.base.CalledByNative;
18import org.chromium.base.JNINamespace;
19import org.chromium.base.NativeClassQualifiedName;
20
21import java.lang.reflect.InvocationTargetException;
22import java.lang.reflect.Method;
23
24/**
25 * This class partners with native ProxyConfigServiceAndroid to listen for
26 * proxy change notifications from Android.
27 */
28@JNINamespace("net")
29public class ProxyChangeListener {
30    private static final String TAG = "ProxyChangeListener";
31    private static boolean sEnabled = true;
32
33    private long mNativePtr;
34    private Context mContext;
35    private ProxyReceiver mProxyReceiver;
36    private Delegate mDelegate;
37
38    private static class ProxyConfig {
39        public ProxyConfig(String host, int port, String pacUrl, String[] exclusionList) {
40            mHost = host;
41            mPort = port;
42            mPacUrl = pacUrl;
43            mExclusionList = exclusionList;
44        }
45        public final String mHost;
46        public final int mPort;
47        public final String mPacUrl;
48        public final String[] mExclusionList;
49    }
50
51    /**
52     * The delegate for ProxyChangeListener. Use for testing.
53     */
54    public interface Delegate {
55        public void proxySettingsChanged();
56    }
57
58    private ProxyChangeListener(Context context) {
59        mContext = context;
60    }
61
62    public static void setEnabled(boolean enabled) {
63        sEnabled = enabled;
64    }
65
66    public void setDelegateForTesting(Delegate delegate) {
67        mDelegate = delegate;
68    }
69
70    @CalledByNative
71    public static ProxyChangeListener create(Context context) {
72        return new ProxyChangeListener(context);
73    }
74
75    @CalledByNative
76    public static String getProperty(String property) {
77        return System.getProperty(property);
78    }
79
80    @CalledByNative
81    public void start(long nativePtr) {
82        assert mNativePtr == 0;
83        mNativePtr = nativePtr;
84        registerReceiver();
85    }
86
87    @CalledByNative
88    public void stop() {
89        mNativePtr = 0;
90        unregisterReceiver();
91    }
92
93    private class ProxyReceiver extends BroadcastReceiver {
94        @Override
95        public void onReceive(Context context, Intent intent) {
96            if (intent.getAction().equals(Proxy.PROXY_CHANGE_ACTION)) {
97                proxySettingsChanged(extractNewProxy(intent));
98            }
99        }
100
101        // Extract a ProxyConfig object from the supplied Intent's extra data
102        // bundle. The android.net.ProxyProperties class is not exported from
103        // the Android SDK, so we have to use reflection to get at it and invoke
104        // methods on it. If we fail, return an empty proxy config (meaning
105        // 'direct').
106        // TODO(sgurun): once android.net.ProxyInfo is public, rewrite this.
107        private ProxyConfig extractNewProxy(Intent intent) {
108            try {
109                final String getHostName = "getHost";
110                final String getPortName = "getPort";
111                final String getPacFileUrl = "getPacFileUrl";
112                final String getExclusionList = "getExclusionList";
113                String className;
114                String proxyInfo;
115                if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
116                    className = "android.net.ProxyProperties";
117                    proxyInfo = "proxy";
118                } else {
119                    className = "android.net.ProxyInfo";
120                    proxyInfo = "android.intent.extra.PROXY_INFO";
121                }
122
123                Object props = intent.getExtras().get(proxyInfo);
124                if (props == null) {
125                    return null;
126                }
127
128                Class<?> cls = Class.forName(className);
129                Method getHostMethod = cls.getDeclaredMethod(getHostName);
130                Method getPortMethod = cls.getDeclaredMethod(getPortName);
131                Method getExclusionListMethod = cls.getDeclaredMethod(getExclusionList);
132
133                String host = (String) getHostMethod.invoke(props);
134                int port = (Integer) getPortMethod.invoke(props);
135
136                String[] exclusionList;
137                if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
138                    String s = (String) getExclusionListMethod.invoke(props);
139                    exclusionList = s.split(",");
140                } else {
141                    exclusionList = (String[]) getExclusionListMethod.invoke(props);
142                }
143                // TODO(xunjieli): rewrite this once the API is public.
144                if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
145                    Method getPacFileUrlMethod =
146                        cls.getDeclaredMethod(getPacFileUrl);
147                    String pacFileUrl = (String) getPacFileUrlMethod.invoke(props);
148                    if (!TextUtils.isEmpty(pacFileUrl)) {
149                       return new ProxyConfig(host, port, pacFileUrl, exclusionList);
150                    }
151                } else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
152                    Method getPacFileUrlMethod =
153                        cls.getDeclaredMethod(getPacFileUrl);
154                    Uri pacFileUrl = (Uri) getPacFileUrlMethod.invoke(props);
155                    if (!Uri.EMPTY.equals(pacFileUrl)) {
156                      return new ProxyConfig(host, port, pacFileUrl.toString(), exclusionList);
157                    }
158                }
159                return new ProxyConfig(host, port, null, exclusionList);
160            } catch (ClassNotFoundException ex) {
161                Log.e(TAG, "Using no proxy configuration due to exception:" + ex);
162                return null;
163            } catch (NoSuchMethodException ex) {
164                Log.e(TAG, "Using no proxy configuration due to exception:" + ex);
165                return null;
166            } catch (IllegalAccessException ex) {
167                Log.e(TAG, "Using no proxy configuration due to exception:" + ex);
168                return null;
169            } catch (InvocationTargetException ex) {
170                Log.e(TAG, "Using no proxy configuration due to exception:" + ex);
171                return null;
172            } catch (NullPointerException ex) {
173                Log.e(TAG, "Using no proxy configuration due to exception:" + ex);
174                return null;
175            }
176        }
177    }
178
179    private void proxySettingsChanged(ProxyConfig cfg) {
180        if (!sEnabled) {
181            return;
182        }
183        if (mDelegate != null) {
184            mDelegate.proxySettingsChanged();
185        }
186        if (mNativePtr == 0) {
187            return;
188        }
189        // Note that this code currently runs on a MESSAGE_LOOP_UI thread, but
190        // the C++ code must run the callbacks on the network thread.
191        if (cfg != null) {
192            nativeProxySettingsChangedTo(mNativePtr, cfg.mHost, cfg.mPort, cfg.mPacUrl,
193                    cfg.mExclusionList);
194        } else {
195            nativeProxySettingsChanged(mNativePtr);
196        }
197    }
198
199    private void registerReceiver() {
200        if (mProxyReceiver != null) {
201            return;
202        }
203        IntentFilter filter = new IntentFilter();
204        filter.addAction(Proxy.PROXY_CHANGE_ACTION);
205        mProxyReceiver = new ProxyReceiver();
206        mContext.getApplicationContext().registerReceiver(mProxyReceiver, filter);
207    }
208
209    private void unregisterReceiver() {
210        if (mProxyReceiver == null) {
211            return;
212        }
213        mContext.unregisterReceiver(mProxyReceiver);
214        mProxyReceiver = null;
215    }
216
217    /**
218     * See net/proxy/proxy_config_service_android.cc
219     */
220    @NativeClassQualifiedName("ProxyConfigServiceAndroid::JNIDelegate")
221    private native void nativeProxySettingsChangedTo(long nativePtr,
222                                                     String host,
223                                                     int port,
224                                                     String pacUrl,
225                                                     String[] exclusionList);
226    @NativeClassQualifiedName("ProxyConfigServiceAndroid::JNIDelegate")
227    private native void nativeProxySettingsChanged(long nativePtr);
228}
229