blob: 9e7abd60b68e3416104b71baffbea81a892f2ac2 [file] [log] [blame]
// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.net;
import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.Proxy;
import android.net.ProxyInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import org.chromium.base.BuildConfig;
import org.chromium.base.ContextUtils;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.NativeClassQualifiedName;
import org.chromium.base.annotations.UsedByReflection;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* This class partners with native ProxyConfigServiceAndroid to listen for
* proxy change notifications from Android.
*
* Unfortunately this is called directly via reflection in a number of WebView applications
* to provide a hacky way to set per-application proxy settings, so it must not be mangled by
* Proguard.
*/
@UsedByReflection("WebView embedders call this to override proxy settings")
@JNINamespace("net")
public class ProxyChangeListener {
private static final String TAG = "ProxyChangeListener";
private static boolean sEnabled = true;
private final Looper mLooper;
private final Handler mHandler;
private long mNativePtr;
// |mProxyReceiver| handles system proxy change notifications pre-M, and also proxy change
// notifications triggered via reflection. When its onReceive method is called, either the
// intent contains the new proxy information as an extra, or it indicates that we should
// look up the system property values.
//
// To avoid triggering as a result of system broadcasts, it is registered with an empty intent
// filter on M and above.
private ProxyReceiver mProxyReceiver;
// On M and above we also register |mRealProxyReceiver| with a matching intent filter, to act as
// a trigger for fetching proxy information via ConnectionManager.
private BroadcastReceiver mRealProxyReceiver;
private Delegate mDelegate;
private static class ProxyConfig {
public ProxyConfig(String host, int port, String pacUrl, String[] exclusionList) {
mHost = host;
mPort = port;
mPacUrl = pacUrl;
mExclusionList = exclusionList;
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private static ProxyConfig fromProxyInfo(ProxyInfo proxyInfo) {
if (proxyInfo == null) {
return null;
}
final Uri pacFileUrl = proxyInfo.getPacFileUrl();
return new ProxyConfig(proxyInfo.getHost(), proxyInfo.getPort(),
Uri.EMPTY.equals(pacFileUrl) ? null : pacFileUrl.toString(),
proxyInfo.getExclusionList());
}
public final String mHost;
public final int mPort;
public final String mPacUrl;
public final String[] mExclusionList;
public static final ProxyConfig DIRECT = new ProxyConfig("", 0, "", new String[0]);
}
/**
* The delegate for ProxyChangeListener. Use for testing.
*/
public interface Delegate { public void proxySettingsChanged(); }
private ProxyChangeListener() {
mLooper = Looper.myLooper();
mHandler = new Handler(mLooper);
}
public static void setEnabled(boolean enabled) {
sEnabled = enabled;
}
public void setDelegateForTesting(Delegate delegate) {
mDelegate = delegate;
}
@CalledByNative
public static ProxyChangeListener create() {
return new ProxyChangeListener();
}
@CalledByNative
public static String getProperty(String property) {
return System.getProperty(property);
}
@CalledByNative
public void start(long nativePtr) {
assertOnThread();
assert mNativePtr == 0;
mNativePtr = nativePtr;
registerReceiver();
}
@CalledByNative
public void stop() {
assertOnThread();
mNativePtr = 0;
unregisterReceiver();
}
@UsedByReflection("WebView embedders call this to override proxy settings")
private class ProxyReceiver extends BroadcastReceiver {
@Override
@UsedByReflection("WebView embedders call this to override proxy settings")
public void onReceive(Context context, final Intent intent) {
if (intent.getAction().equals(Proxy.PROXY_CHANGE_ACTION)) {
runOnThread(() -> proxySettingsChanged(extractNewProxy(intent)));
}
}
// Extract a ProxyConfig object from the supplied Intent's extra data
// bundle. The android.net.ProxyProperties class is not exported from
// the Android SDK, so we have to use reflection to get at it and invoke
// methods on it. If we fail, return an empty proxy config (meaning
// use system properties).
private ProxyConfig extractNewProxy(Intent intent) {
Bundle extras = intent.getExtras();
if (extras == null) {
return null;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return ProxyConfig.fromProxyInfo(
(ProxyInfo) extras.get("android.intent.extra.PROXY_INFO"));
}
try {
final String getHostName = "getHost";
final String getPortName = "getPort";
final String getPacFileUrl = "getPacFileUrl";
final String getExclusionList = "getExclusionList";
final String className = "android.net.ProxyProperties";
Object props = extras.get("proxy");
if (props == null) {
return null;
}
Class<?> cls = Class.forName(className);
Method getHostMethod = cls.getDeclaredMethod(getHostName);
Method getPortMethod = cls.getDeclaredMethod(getPortName);
Method getExclusionListMethod = cls.getDeclaredMethod(getExclusionList);
String host = (String) getHostMethod.invoke(props);
int port = (Integer) getPortMethod.invoke(props);
String[] exclusionList;
String s = (String) getExclusionListMethod.invoke(props);
exclusionList = s.split(",");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Method getPacFileUrlMethod = cls.getDeclaredMethod(getPacFileUrl);
String pacFileUrl = (String) getPacFileUrlMethod.invoke(props);
if (!TextUtils.isEmpty(pacFileUrl)) {
return new ProxyConfig(host, port, pacFileUrl, exclusionList);
}
}
return new ProxyConfig(host, port, null, exclusionList);
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException
| InvocationTargetException | NullPointerException ex) {
Log.e(TAG, "Using no proxy configuration due to exception:" + ex);
return null;
}
}
}
private void proxySettingsChanged(ProxyConfig cfg) {
assertOnThread();
if (!sEnabled) {
return;
}
if (mDelegate != null) {
// proxySettingsChanged is called even if mNativePtr == 0, for testing purposes.
mDelegate.proxySettingsChanged();
}
if (mNativePtr == 0) {
return;
}
if (cfg != null) {
nativeProxySettingsChangedTo(
mNativePtr, cfg.mHost, cfg.mPort, cfg.mPacUrl, cfg.mExclusionList);
} else {
nativeProxySettingsChanged(mNativePtr);
}
}
@TargetApi(Build.VERSION_CODES.M)
private ProxyConfig getProxyConfig() {
ConnectivityManager connectivityManager =
(ConnectivityManager) ContextUtils.getApplicationContext().getSystemService(
Context.CONNECTIVITY_SERVICE);
ProxyInfo proxyInfo = connectivityManager.getDefaultProxy();
return proxyInfo == null ? ProxyConfig.DIRECT : ProxyConfig.fromProxyInfo(proxyInfo);
}
/* package */ void updateProxyConfigFromConnectivityManager() {
runOnThread(() -> proxySettingsChanged(getProxyConfig()));
}
private void registerReceiver() {
assertOnThread();
assert mProxyReceiver == null;
assert mRealProxyReceiver == null;
IntentFilter filter = new IntentFilter();
filter.addAction(Proxy.PROXY_CHANGE_ACTION);
mProxyReceiver = new ProxyReceiver();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
// Proxy change broadcast receiver for Pre-M. Uses reflection to extract proxy
// information from the intent extra.
ContextUtils.getApplicationContext().registerReceiver(mProxyReceiver, filter);
} else {
// Register the instance of ProxyReceiver with an empty intent filter, so that it is
// still found via reflection, but is not called by the system. See: crbug.com/851995
ContextUtils.getApplicationContext().registerReceiver(
mProxyReceiver, new IntentFilter());
// Create a BroadcastReceiver that uses M+ APIs to fetch the proxy confuguration from
// ConnectionManager.
mRealProxyReceiver = new ProxyBroadcastReceiver(this);
ContextUtils.getApplicationContext().registerReceiver(mRealProxyReceiver, filter);
}
}
private void unregisterReceiver() {
assertOnThread();
assert mProxyReceiver != null;
ContextUtils.getApplicationContext().unregisterReceiver(mProxyReceiver);
if (mRealProxyReceiver != null) {
ContextUtils.getApplicationContext().unregisterReceiver(mRealProxyReceiver);
}
mProxyReceiver = null;
mRealProxyReceiver = null;
}
private boolean onThread() {
return mLooper == Looper.myLooper();
}
private void assertOnThread() {
if (BuildConfig.DCHECK_IS_ON && !onThread()) {
throw new IllegalStateException("Must be called on ProxyChangeListener thread.");
}
}
private void runOnThread(Runnable r) {
if (onThread()) {
r.run();
} else {
mHandler.post(r);
}
}
/**
* See net/proxy_resolution/proxy_config_service_android.cc
*/
@NativeClassQualifiedName("ProxyConfigServiceAndroid::JNIDelegate")
private native void nativeProxySettingsChangedTo(
long nativePtr, String host, int port, String pacUrl, String[] exclusionList);
@NativeClassQualifiedName("ProxyConfigServiceAndroid::JNIDelegate")
private native void nativeProxySettingsChanged(long nativePtr);
}