2 * Copyright © 2016-2021 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.net.Uri
26 import android.os.Build
27 import android.os.Parcelable
28 import android.util.ArrayMap
29 import android.view.View
31 import androidx.preference.PreferenceManager
32 import androidx.webkit.ProxyConfig
33 import androidx.webkit.ProxyController
34 import androidx.webkit.WebViewFeature
36 import com.stoutner.privacybrowser.R
37 import com.stoutner.privacybrowser.activities.MainWebViewActivity
39 import com.google.android.material.snackbar.Snackbar
41 import java.lang.Exception
42 import java.lang.IllegalArgumentException
43 import java.lang.reflect.InvocationTargetException
44 import java.net.InetSocketAddress
46 import java.net.SocketAddress
50 // Define the public companion object constants. These can be moved to public class constants once the entire project has migrated to Kotlin.
51 const val NONE = "None"
54 const val CUSTOM = "Custom"
55 const val ORBOT_STATUS_ON = "ON"
58 fun setProxy(context: Context, activityView: View, proxyMode: String) {
59 // Initialize the proxy host and port strings.
63 // Create a proxy config builder.
64 val proxyConfigBuilder = ProxyConfig.Builder()
66 // Run the commands that correlate to the proxy mode.
69 // Clear the proxy values.
70 System.clearProperty("proxyHost")
71 System.clearProperty("proxyPort")
75 // Update the proxy host and port strings. These can be removed once the minimum API >= 21.
76 proxyHost = "localhost"
79 // Set the proxy values. These can be removed once the minimum API >= 21.
80 System.setProperty("proxyHost", proxyHost)
81 System.setProperty("proxyPort", proxyPort)
83 // Add the proxy to the builder. The proxy config builder can use a SOCKS proxy.
84 proxyConfigBuilder.addProxyRule("socks://localhost:9050")
86 // Ask Orbot to connect if its current status is not `"ON"`.
87 if (MainWebViewActivity.orbotStatus != ORBOT_STATUS_ON) {
88 // Create an intent to request Orbot to start.
89 val orbotIntent = Intent("org.torproject.android.intent.action.START")
91 // Send the intent to the Orbot package.
92 orbotIntent.setPackage("org.torproject.android")
94 // Request a status response be sent back to this package.
95 orbotIntent.putExtra("org.torproject.android.intent.extra.PACKAGE_NAME", context.packageName)
98 context.sendBroadcast(orbotIntent)
103 // Update the proxy host and port strings. These can be removed once the minimum API >= 21.
104 proxyHost = "localhost"
107 // Set the proxy values. These can be removed once the minimum API >= 21.
108 System.setProperty("proxyHost", proxyHost)
109 System.setProperty("proxyPort", proxyPort)
111 // Add the proxy to the builder.
112 proxyConfigBuilder.addProxyRule("http://localhost:4444")
116 // Get a handle for the shared preferences.
117 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
119 // Get the custom proxy URL string.
120 val customProxyUrlString = sharedPreferences.getString(context.getString(R.string.proxy_custom_url_key), context.getString(R.string.proxy_custom_url_default_value))
122 // Parse the custom proxy URL.
124 // Convert the custom proxy URL string to a URI.
125 val customProxyUri = Uri.parse(customProxyUrlString)
127 // Get the proxy host and port strings from the shared preferences. These can be removed once the minimum API >= 21.
128 proxyHost = customProxyUri.host!!
129 proxyPort = customProxyUri.port.toString()
131 // Set the proxy values. These can be removed once the minimum API >= 21.
132 System.setProperty("proxyHost", proxyHost)
133 System.setProperty("proxyPort", proxyPort)
135 // Add the proxy to the builder.
136 proxyConfigBuilder.addProxyRule(customProxyUrlString!!)
137 } catch (exception: Exception) { // The custom proxy URL is invalid.
138 // Display a Snackbar.
139 Snackbar.make(activityView, R.string.custom_proxy_invalid, Snackbar.LENGTH_LONG).show()
144 // Apply the proxy settings
145 if (WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) { // The fancy new proxy config can be used because the API >= 21.
146 // Convert the proxy config builder into a proxy config.
147 val proxyConfig = proxyConfigBuilder.build()
149 // Get the proxy controller.
150 val proxyController = ProxyController.getInstance()
152 // Apply the proxy settings.
153 if (proxyMode == NONE) { // Remove the proxy. A default executor and runnable are used.
154 proxyController.clearProxyOverride({}, {})
155 } else { // Apply the proxy.
157 // Apply the proxy. A default executor and runnable are used.
158 proxyController.setProxyOverride(proxyConfig, {}, {})
159 } catch (exception: IllegalArgumentException) { // The proxy config is invalid.
160 // Display a Snackbar.
161 Snackbar.make(activityView, R.string.custom_proxy_invalid, Snackbar.LENGTH_LONG).show()
164 } else { // The old proxy method must be used, either because an old WebView is installed or because the API == 19;
165 // Get a handle for the shared preferences.
166 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
168 // Check to make sure a SOCKS proxy is not selected.
169 if ((proxyMode == CUSTOM) &&
170 sharedPreferences.getString(context.getString(R.string.proxy_custom_url_key), context.getString(R.string.proxy_custom_url_default_value))!!.startsWith("socks://")) {
171 // Display a Snackbar.
172 Snackbar.make(activityView, R.string.socks_proxies_do_not_work_on_kitkat, Snackbar.LENGTH_LONG).show()
173 } else { // Use reflection to apply the new proxy values.
175 // Get the application and APK classes.
176 val applicationClass = Class.forName("android.app.Application")
178 // Suppress the lint warning that reflection may not always work in the future and on all devices.
179 @SuppressLint("PrivateApi") val loadedApkClass = Class.forName("android.app.LoadedApk")
181 // Get the declared fields. Suppress the lint that it is discouraged to access private APIs.
182 @SuppressLint("DiscouragedPrivateApi") val methodLoadedApkField = applicationClass.getDeclaredField("mLoadedApk")
183 @SuppressLint("DiscouragedPrivateApi") val methodReceiversField = loadedApkClass.getDeclaredField("mReceivers")
185 // Allow the values to be changed.
186 methodLoadedApkField.isAccessible = true
187 methodReceiversField.isAccessible = true
189 // Get the APK object.
190 val methodLoadedApkObject = methodLoadedApkField[context]
192 // Get an array map of the receivers.
193 val receivers = methodReceiversField[methodLoadedApkObject] as ArrayMap<*, *>
196 for (receiverMap in receivers.values) {
197 for (receiver in (receiverMap as ArrayMap<*, *>).keys) {
198 // Get the receiver class.
199 // `Class<*>`, which is an `unbounded wildcard parameterized type`, must be used instead of `Class`, which is a `raw type`. Otherwise, `receiveClass.getDeclaredMethod()` is unhappy.
200 val receiverClass: Class<*> = receiver.javaClass
202 // Apply the new proxy settings to any classes whose names contain `ProxyChangeListener`.
203 if (receiverClass.name.contains("ProxyChangeListener")) {
204 // Get the `onReceive` method from the class.
205 val onReceiveMethod = receiverClass.getDeclaredMethod("onReceive", Context::class.java, Intent::class.java)
207 // Create a proxy change intent.
208 val proxyChangeIntent = Intent(android.net.Proxy.PROXY_CHANGE_ACTION)
210 // Set the proxy for API >= 21.
211 if (Build.VERSION.SDK_INT >= 21) {
212 // Get the proxy info class.
213 val proxyInfoClass = Class.forName("android.net.ProxyInfo")
215 // Get the build direct proxy method from the proxy info class.
216 val buildDirectProxyMethod = proxyInfoClass.getMethod("buildDirectProxy", String::class.java, Integer.TYPE)
218 // Populate a proxy info object with the new proxy information.
219 val proxyInfoObject = buildDirectProxyMethod.invoke(proxyInfoClass, proxyHost, Integer.valueOf(proxyPort))
221 // Add the proxy info object into the proxy change intent.
222 proxyChangeIntent.putExtra("proxy", proxyInfoObject as Parcelable)
225 // Pass the proxy change intent to the `onReceive` method of the receiver class.
226 onReceiveMethod.invoke(receiver, context, proxyChangeIntent)
230 } catch (exception: ClassNotFoundException) {
232 } catch (exception: NoSuchFieldException) {
234 } catch (exception: IllegalAccessException) {
236 } catch (exception: NoSuchMethodException) {
238 } catch (exception: InvocationTargetException) {
245 fun getCurrentProxy(context: Context): Proxy {
246 // Get the proxy according to the current proxy mode.
247 val proxy = when (MainWebViewActivity.proxyMode) {
248 TOR -> if (Build.VERSION.SDK_INT >= 21) {
249 // Use localhost port 9050 as the socket address.
250 val torSocketAddress: SocketAddress = InetSocketAddress.createUnresolved("localhost", 9050)
252 // Create a SOCKS proxy.
253 Proxy(Proxy.Type.SOCKS, torSocketAddress)
255 // Use localhost port 8118 as the socket address.
256 val oldTorSocketAddress: SocketAddress = InetSocketAddress.createUnresolved("localhost", 8118)
258 // Create an HTTP proxy.
259 Proxy(Proxy.Type.HTTP, oldTorSocketAddress)
263 // Use localhost port 4444 as the socket address.
264 val i2pSocketAddress: SocketAddress = InetSocketAddress.createUnresolved("localhost", 4444)
266 // Create an HTTP proxy.
267 Proxy(Proxy.Type.HTTP, i2pSocketAddress)
271 // Get the shared preferences.
272 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
274 // Get the custom proxy URL string.
275 val customProxyUrlString = sharedPreferences.getString(context.getString(R.string.proxy_custom_url_key), context.getString(R.string.proxy_custom_url_default_value))
277 // Parse the custom proxy URL.
279 // Convert the custom proxy URL string to a URI.
280 val customProxyUri = Uri.parse(customProxyUrlString)
282 // Get the custom socket address.
283 val customSocketAddress: SocketAddress = InetSocketAddress.createUnresolved(customProxyUri.host, customProxyUri.port)
285 // Get the custom proxy scheme.
286 val customProxyScheme = customProxyUri.scheme
288 // Create a proxy according to the scheme.
289 if (customProxyScheme != null && customProxyScheme.startsWith("socks")) { // A SOCKS proxy is specified.
290 // Create a SOCKS proxy.
291 Proxy(Proxy.Type.SOCKS, customSocketAddress)
292 } else { // A SOCKS proxy is not specified.
293 // Create an HTTP proxy.
294 Proxy(Proxy.Type.HTTP, customSocketAddress)
296 } catch (exception: Exception) { // The custom proxy cannot be parsed.
297 // Disable the proxy.
303 // Create a direct proxy.