2 * Copyright © 2016-2019 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
6 * Privacy Browser is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
11 * Privacy Browser is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with Privacy Browser. If not, see <http://www.gnu.org/licenses/>.
20 package com.stoutner.privacybrowser.helpers;
22 import android.annotation.SuppressLint;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.SharedPreferences;
26 import android.net.Proxy;
27 import android.net.Uri;
28 import android.os.Build;
29 import android.os.Parcelable;
30 import android.util.ArrayMap;
31 import android.util.Log;
32 import android.view.View;
34 import androidx.preference.PreferenceManager;
35 import androidx.webkit.ProxyConfig;
36 import androidx.webkit.ProxyController;
37 import androidx.webkit.WebViewFeature;
39 import com.google.android.material.snackbar.Snackbar;
41 import com.stoutner.privacybrowser.R;
42 import com.stoutner.privacybrowser.activities.MainWebViewActivity;
44 import java.lang.reflect.Field;
45 import java.lang.reflect.InvocationTargetException;
46 import java.lang.reflect.Method;
47 import java.util.concurrent.Executor;
49 public class ProxyHelper {
50 public static final String NONE = "None";
51 public static final String TOR = "Tor";
52 public static final String I2P = "I2P";
53 public static final String CUSTOM = "Custom";
55 public static void setProxy(Context context, View activityView, String proxyMode) {
56 // Initialize the proxy host and port strings.
57 String proxyHost = "0";
58 String proxyPort = "0";
60 // Create a proxy config builder.
61 ProxyConfig.Builder proxyConfigBuilder = new ProxyConfig.Builder();
63 // Run the commands that correlate to the proxy mode.
66 // Clear the proxy values.
67 System.clearProperty("proxyHost");
68 System.clearProperty("proxyHost");
72 // Update the proxy host and port strings. These can be removed once the minimum API >= 21.
73 proxyHost = "localhost";
76 // Set the proxy values. These can be removed once the minimum API >= 21.
77 System.setProperty("proxyHost", proxyHost);
78 System.setProperty("proxyPort", proxyPort);
80 // Add the proxy to the builder. The proxy config builder can use a SOCKS proxy.
81 proxyConfigBuilder.addProxyRule("socks://localhost:9050");
83 // Ask Orbot to connect if its current status is not `"ON"`.
84 if (!MainWebViewActivity.orbotStatus.equals("ON")) {
85 // Create an intent to request Orbot to start.
86 Intent orbotIntent = new Intent("org.torproject.android.intent.action.START");
88 // Send the intent to the Orbot package.
89 orbotIntent.setPackage("org.torproject.android");
91 // Request a status response be sent back to this package.
92 orbotIntent.putExtra("org.torproject.android.intent.extra.PACKAGE_NAME", context.getPackageName());
95 context.sendBroadcast(orbotIntent);
100 // Update the proxy host and port strings. These can be removed once the minimum API >= 21.
101 proxyHost = "localhost";
104 // Set the proxy values. These can be removed once the minimum API >= 21.
105 System.setProperty("proxyHost", proxyHost);
106 System.setProperty("proxyPort", proxyPort);
108 // Add the proxy to the builder.
109 proxyConfigBuilder.addProxyRule("http://localhost:4444");
113 // Get a handle for the shared preferences.
114 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
116 // Get the custom proxy URL string.
117 String customProxyUrlString = sharedPreferences.getString("proxy_custom_url", context.getString(R.string.proxy_custom_url_default_value));
119 // Parse the custom proxy URL.
121 // Convert the custom proxy URL string to a URI.
122 Uri customProxyUri = Uri.parse(customProxyUrlString);
124 // Get the proxy host and port strings from the shared preferences. These can be removed once the minimum API >= 21.
125 proxyHost = customProxyUri.getHost();
126 proxyPort = String.valueOf(customProxyUri.getPort());
128 // Set the proxy values. These can be removed once the minimum API >= 21.
129 System.setProperty("proxyHost", proxyHost);
130 System.setProperty("proxyPort", proxyPort);
132 // Add the proxy to the builder.
133 proxyConfigBuilder.addProxyRule(customProxyUrlString);
134 } catch (Exception exception){ // The custom proxy URL is invalid.
135 // Display a Snackbar.
136 Snackbar.make(activityView, R.string.custom_proxy_invalid, Snackbar.LENGTH_LONG).show();
141 // Apply the proxy settings
142 if (WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) { // The fancy new proxy config can be used because the API >= 21.
143 // Convert the proxy config builder into a proxy config.
144 ProxyConfig proxyConfig = proxyConfigBuilder.build();
146 // Get the proxy controller.
147 ProxyController proxyController = ProxyController.getInstance();
149 // Applying a proxy requires an executor.
150 Executor executor = runnable -> {
154 // Applying a proxy requires a runnable.
155 Runnable runnable = () -> {
159 // Apply the proxy settings.
160 if (proxyMode.equals(NONE)) { // Remove the proxy.
161 proxyController.clearProxyOverride(executor, runnable);
162 } else { // Apply the proxy.
165 proxyController.setProxyOverride(proxyConfig, executor, runnable);
166 } catch (IllegalArgumentException exception) { // The proxy config is invalid.
167 // Display a Snackbar.
168 Snackbar.make(activityView, R.string.custom_proxy_invalid, Snackbar.LENGTH_LONG).show();
171 } else { // The old proxy method must be used, either because an old WebView is installed or because the API == 19;
172 // Get a handle for the shared preferences.
173 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
175 // Check to make sure a SOCKS proxy is not selected.
176 if (proxyMode.equals(CUSTOM) && sharedPreferences.getString("proxy_custom_url", context.getString(R.string.proxy_custom_url_default_value)).startsWith("socks://")) {
177 // Display a Snackbar.
178 Snackbar.make(activityView, R.string.socks_proxies_do_not_work_on_kitkat, Snackbar.LENGTH_LONG).show();
179 } else { // Use reflection to apply the new proxy values.
181 // Get the application and APK classes. Suppress the lint warning that reflection may not always work in the future and on all devices.
182 Class applicationClass = Class.forName("android.app.Application");
183 @SuppressLint("PrivateApi") Class loadedApkClass = Class.forName("android.app.LoadedApk");
185 // Get the declared fields. Suppress the lint warning that `mLoadedApk` cannot be resolved.
186 @SuppressWarnings("JavaReflectionMemberAccess") Field methodLoadedApkField = applicationClass.getDeclaredField("mLoadedApk");
187 Field methodReceiversField = loadedApkClass.getDeclaredField("mReceivers");
189 // Allow the values to be changed.
190 methodLoadedApkField.setAccessible(true);
191 methodReceiversField.setAccessible(true);
193 // Get the APK object.
194 Object methodLoadedApkObject = methodLoadedApkField.get(context);
196 // Get an array map of the receivers.
197 ArrayMap receivers = (ArrayMap) methodReceiversField.get(methodLoadedApkObject);
199 // Set the proxy if the receivers has at least one entry.
200 if (receivers != null) {
201 for (Object receiverMap : receivers.values()) {
202 for (Object receiver : ((ArrayMap) receiverMap).keySet()) {
203 // Get the receiver class.
204 // `Class<?>`, which is an `unbounded wildcard parameterized type`, must be used instead of `Class`, which is a `raw type`. Otherwise, `receiveClass.getDeclaredMethod()` is unhappy.
205 Class<?> receiverClass = receiver.getClass();
207 // Apply the new proxy settings to any classes whose names contain `ProxyChangeListener`.
208 if (receiverClass.getName().contains("ProxyChangeListener")) {
209 // Get the `onReceive` method from the class.
210 Method onReceiveMethod = receiverClass.getDeclaredMethod("onReceive", Context.class, Intent.class);
212 // Create a proxy change intent.
213 Intent proxyChangeIntent = new Intent(Proxy.PROXY_CHANGE_ACTION);
215 if (Build.VERSION.SDK_INT >= 21) {
216 // Get a proxy info class.
217 // `Class<?>`, which is an `unbounded wildcard parameterized type`, must be used instead of `Class`, which is a `raw type`. Otherwise, `proxyInfoClass.getMethod()` is unhappy.
218 Class<?> proxyInfoClass = Class.forName("android.net.ProxyInfo");
220 // Get the build direct proxy method from the proxy info class.
221 Method buildDirectProxyMethod = proxyInfoClass.getMethod("buildDirectProxy", String.class, Integer.TYPE);
223 // Populate a proxy info object with the new proxy information.
224 Object proxyInfoObject = buildDirectProxyMethod.invoke(proxyInfoClass, proxyHost, Integer.valueOf(proxyPort));
226 // Add the proxy info object into the proxy change intent.
227 proxyChangeIntent.putExtra("proxy", (Parcelable) proxyInfoObject);
230 // Pass the proxy change intent to the `onReceive` method of the receiver class.
231 onReceiveMethod.invoke(receiver, context, proxyChangeIntent);
236 } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException | NoSuchMethodException | InvocationTargetException exception) {
237 Log.d("enableProxyThroughOrbot", "Exception: " + exception);