2 * Copyright © 2016-2020 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.Uri;
27 import android.os.Build;
28 import android.os.Parcelable;
29 import android.util.ArrayMap;
30 import android.view.View;
32 import androidx.preference.PreferenceManager;
33 import androidx.webkit.ProxyConfig;
34 import androidx.webkit.ProxyController;
35 import androidx.webkit.WebViewFeature;
37 import com.google.android.material.snackbar.Snackbar;
39 import com.stoutner.privacybrowser.R;
40 import com.stoutner.privacybrowser.activities.MainWebViewActivity;
42 import java.lang.reflect.Field;
43 import java.lang.reflect.InvocationTargetException;
44 import java.lang.reflect.Method;
45 import java.net.InetSocketAddress;
46 import java.net.Proxy;
47 import java.net.SocketAddress;
48 import java.util.concurrent.Executor;
50 public class ProxyHelper {
51 public static final String NONE = "None";
52 public static final String TOR = "Tor";
53 public static final String I2P = "I2P";
54 public static final String CUSTOM = "Custom";
56 public static void setProxy(Context context, View activityView, String proxyMode) {
57 // Initialize the proxy host and port strings.
58 String proxyHost = "0";
59 String proxyPort = "0";
61 // Create a proxy config builder.
62 ProxyConfig.Builder proxyConfigBuilder = new ProxyConfig.Builder();
64 // Run the commands that correlate to the proxy mode.
67 // Clear the proxy values.
68 System.clearProperty("proxyHost");
69 System.clearProperty("proxyHost");
73 // Update the proxy host and port strings. These can be removed once the minimum API >= 21.
74 proxyHost = "localhost";
77 // Set the proxy values. These can be removed once the minimum API >= 21.
78 System.setProperty("proxyHost", proxyHost);
79 System.setProperty("proxyPort", proxyPort);
81 // Add the proxy to the builder. The proxy config builder can use a SOCKS proxy.
82 proxyConfigBuilder.addProxyRule("socks://localhost:9050");
84 // Ask Orbot to connect if its current status is not `"ON"`.
85 if (!MainWebViewActivity.orbotStatus.equals("ON")) {
86 // Create an intent to request Orbot to start.
87 Intent orbotIntent = new Intent("org.torproject.android.intent.action.START");
89 // Send the intent to the Orbot package.
90 orbotIntent.setPackage("org.torproject.android");
92 // Request a status response be sent back to this package.
93 orbotIntent.putExtra("org.torproject.android.intent.extra.PACKAGE_NAME", context.getPackageName());
96 context.sendBroadcast(orbotIntent);
101 // Update the proxy host and port strings. These can be removed once the minimum API >= 21.
102 proxyHost = "localhost";
105 // Set the proxy values. These can be removed once the minimum API >= 21.
106 System.setProperty("proxyHost", proxyHost);
107 System.setProperty("proxyPort", proxyPort);
109 // Add the proxy to the builder.
110 proxyConfigBuilder.addProxyRule("http://localhost:4444");
114 // Get a handle for the shared preferences.
115 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
117 // Get the custom proxy URL string.
118 String customProxyUrlString = sharedPreferences.getString("proxy_custom_url", context.getString(R.string.proxy_custom_url_default_value));
120 // Parse the custom proxy URL.
122 // Convert the custom proxy URL string to a URI.
123 Uri customProxyUri = Uri.parse(customProxyUrlString);
125 // Get the proxy host and port strings from the shared preferences. These can be removed once the minimum API >= 21.
126 proxyHost = customProxyUri.getHost();
127 proxyPort = String.valueOf(customProxyUri.getPort());
129 // Set the proxy values. These can be removed once the minimum API >= 21.
130 System.setProperty("proxyHost", proxyHost);
131 System.setProperty("proxyPort", proxyPort);
133 // Add the proxy to the builder.
134 proxyConfigBuilder.addProxyRule(customProxyUrlString);
135 } catch (Exception exception){ // The custom proxy URL is invalid.
136 // Display a Snackbar.
137 Snackbar.make(activityView, R.string.custom_proxy_invalid, Snackbar.LENGTH_LONG).show();
142 // Apply the proxy settings
143 if (WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) { // The fancy new proxy config can be used because the API >= 21.
144 // Convert the proxy config builder into a proxy config.
145 ProxyConfig proxyConfig = proxyConfigBuilder.build();
147 // Get the proxy controller.
148 ProxyController proxyController = ProxyController.getInstance();
150 // Applying a proxy requires an executor.
151 Executor executor = runnable -> {
155 // Applying a proxy requires a runnable.
156 Runnable runnable = () -> {
160 // Apply the proxy settings.
161 if (proxyMode.equals(NONE)) { // Remove the proxy.
162 proxyController.clearProxyOverride(executor, runnable);
163 } else { // Apply the proxy.
166 proxyController.setProxyOverride(proxyConfig, executor, runnable);
167 } catch (IllegalArgumentException exception) { // The proxy config is invalid.
168 // Display a Snackbar.
169 Snackbar.make(activityView, R.string.custom_proxy_invalid, Snackbar.LENGTH_LONG).show();
172 } else { // The old proxy method must be used, either because an old WebView is installed or because the API == 19;
173 // Get a handle for the shared preferences.
174 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
176 // Check to make sure a SOCKS proxy is not selected.
177 if (proxyMode.equals(CUSTOM) && sharedPreferences.getString("proxy_custom_url", context.getString(R.string.proxy_custom_url_default_value)).startsWith("socks://")) {
178 // Display a Snackbar.
179 Snackbar.make(activityView, R.string.socks_proxies_do_not_work_on_kitkat, Snackbar.LENGTH_LONG).show();
180 } else { // Use reflection to apply the new proxy values.
182 // Get the application and APK classes. Suppress the lint warning that reflection may not always work in the future and on all devices.
183 Class applicationClass = Class.forName("android.app.Application");
184 @SuppressLint("PrivateApi") Class loadedApkClass = Class.forName("android.app.LoadedApk");
186 // Get the declared fields. Suppress the lint warning that `mLoadedApk` cannot be resolved.
187 @SuppressWarnings("JavaReflectionMemberAccess") Field methodLoadedApkField = applicationClass.getDeclaredField("mLoadedApk");
188 Field methodReceiversField = loadedApkClass.getDeclaredField("mReceivers");
190 // Allow the values to be changed.
191 methodLoadedApkField.setAccessible(true);
192 methodReceiversField.setAccessible(true);
194 // Get the APK object.
195 Object methodLoadedApkObject = methodLoadedApkField.get(context);
197 // Get an array map of the receivers.
198 ArrayMap receivers = (ArrayMap) methodReceiversField.get(methodLoadedApkObject);
200 // Set the proxy if the receivers has at least one entry.
201 if (receivers != null) {
202 for (Object receiverMap : receivers.values()) {
203 for (Object receiver : ((ArrayMap) receiverMap).keySet()) {
204 // Get the receiver class.
205 // `Class<?>`, which is an `unbounded wildcard parameterized type`, must be used instead of `Class`, which is a `raw type`. Otherwise, `receiveClass.getDeclaredMethod()` is unhappy.
206 Class<?> receiverClass = receiver.getClass();
208 // Apply the new proxy settings to any classes whose names contain `ProxyChangeListener`.
209 if (receiverClass.getName().contains("ProxyChangeListener")) {
210 // Get the `onReceive` method from the class.
211 Method onReceiveMethod = receiverClass.getDeclaredMethod("onReceive", Context.class, Intent.class);
213 // Create a proxy change intent.
214 Intent proxyChangeIntent = new Intent(android.net.Proxy.PROXY_CHANGE_ACTION);
216 if (Build.VERSION.SDK_INT >= 21) {
217 // Get a proxy info class.
218 // `Class<?>`, which is an `unbounded wildcard parameterized type`, must be used instead of `Class`, which is a `raw type`. Otherwise, `proxyInfoClass.getMethod()` is unhappy.
219 Class<?> proxyInfoClass = Class.forName("android.net.ProxyInfo");
221 // Get the build direct proxy method from the proxy info class.
222 Method buildDirectProxyMethod = proxyInfoClass.getMethod("buildDirectProxy", String.class, Integer.TYPE);
224 // Populate a proxy info object with the new proxy information.
225 Object proxyInfoObject = buildDirectProxyMethod.invoke(proxyInfoClass, proxyHost, Integer.valueOf(proxyPort));
227 // Add the proxy info object into the proxy change intent.
228 proxyChangeIntent.putExtra("proxy", (Parcelable) proxyInfoObject);
231 // Pass the proxy change intent to the `onReceive` method of the receiver class.
232 onReceiveMethod.invoke(receiver, context, proxyChangeIntent);
237 } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException | NoSuchMethodException | InvocationTargetException exception) {
244 public Proxy getCurrentProxy(Context context) {
245 // Define a proxy variable.
248 // Get the proxy according to the current proxy mode.
249 switch (MainWebViewActivity.proxyMode) {
250 case (ProxyHelper.TOR):
251 if (Build.VERSION.SDK_INT >= 21) {
252 // Use localhost port 9050 as the socket address.
253 SocketAddress torSocketAddress = new InetSocketAddress("localhost", 9050);
255 // Create a SOCKS proxy.
256 proxy = new Proxy(Proxy.Type.SOCKS, torSocketAddress);
258 // Use localhost port 8118 as the socket address.
259 SocketAddress oldTorSocketAddress = new InetSocketAddress("localhost", 8118);
261 // Create an HTTP proxy.
262 proxy = new Proxy(Proxy.Type.HTTP, oldTorSocketAddress);
266 case (ProxyHelper.I2P):
267 // Use localhost port 4444 as the socket address.
268 SocketAddress i2pSocketAddress = new InetSocketAddress("localhost", 4444);
270 // Create an HTTP proxy.
271 proxy = new Proxy(Proxy.Type.HTTP, i2pSocketAddress);
274 case (ProxyHelper.CUSTOM):
275 // Get the shared preferences.
276 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
278 // Get the custom proxy URL string.
279 String customProxyUrlString = sharedPreferences.getString("proxy_custom_url", context.getString(R.string.proxy_custom_url_default_value));
281 // Parse the custom proxy URL.
283 // Convert the custom proxy URL string to a URI.
284 Uri customProxyUri = Uri.parse(customProxyUrlString);
286 // Get the custom socket address.
287 SocketAddress customSocketAddress = new InetSocketAddress(customProxyUri.getHost(), customProxyUri.getPort());
289 // Get the custom proxy scheme.
290 String customProxyScheme = customProxyUri.getScheme();
292 // Create a proxy according to the scheme.
293 if ((customProxyScheme != null) && customProxyScheme.startsWith("socks")) { // A SOCKS proxy is specified.
294 // Create a SOCKS proxy.
295 proxy = new Proxy(Proxy.Type.SOCKS, customSocketAddress);
296 } else { // A SOCKS proxy is not specified.
297 // Create an HTTP proxy.
298 proxy = new Proxy(Proxy.Type.HTTP, customSocketAddress);
300 } catch (Exception exception) { // The custom proxy cannot be parsed.
301 // Disable the proxy.
302 proxy = Proxy.NO_PROXY;
306 default: // No proxy is in use.
307 // Create a direct proxy.
308 proxy = Proxy.NO_PROXY;