From 642229265ead02fe915af8c5ef8e3a22adc1604e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Feb 2026 23:49:09 +0000 Subject: [PATCH 1/8] WIP: Rewrite project in Kotlin - core classes and utilities - Add Kotlin plugin to build.gradle files - Convert core app classes: ProxyDroidApplication, Profile, ProxyedApp - Convert services: ProxyDroidService, ProxyDroidVpnService - Convert receivers: ProxyDroidReceiver, ConnectivityBroadcastReceiver - Convert widget: ProxyDroidWidgetProvider - Convert JNI interfaces: Exec, Tun2SocksHelper - Convert utility classes: Utils, Constraints, Option, ImageLoader, ImageLoaderFactory, RegexValidator, LocalProxyServer - Convert helper: InnerSocketBuilder This is work in progress - remaining files to convert: - UI activities (ProxyDroid, AppManager, BypassListActivity, FileChooser) - PAC selector classes - Adapters and validators - Test files https://claude.ai/code/session_01A8JRXuv4RL3LWQCZq4QDr9 --- app/build.gradle | 7 + .../ConnectivityBroadcastReceiver.kt | 174 +++++++++++++ app/src/main/java/org/proxydroid/Exec.kt | 45 ++++ .../java/org/proxydroid/InnerSocketBuilder.kt | 39 +++ app/src/main/java/org/proxydroid/Profile.kt | 218 ++++++++++++++++ .../org/proxydroid/ProxyDroidApplication.kt | 22 ++ .../java/org/proxydroid/ProxyDroidReceiver.kt | 54 ++++ .../java/org/proxydroid/ProxyDroidService.kt | 192 ++++++++++++++ .../org/proxydroid/ProxyDroidVpnService.kt | 239 ++++++++++++++++++ .../proxydroid/ProxyDroidWidgetProvider.kt | 117 +++++++++ .../main/java/org/proxydroid/ProxyedApp.kt | 29 +++ .../java/org/proxydroid/utils/Constraints.kt | 56 ++++ .../java/org/proxydroid/utils/ImageLoader.kt | 170 +++++++++++++ .../proxydroid/utils/ImageLoaderFactory.kt | 30 +++ .../org/proxydroid/utils/LocalProxyServer.kt | 187 ++++++++++++++ .../main/java/org/proxydroid/utils/Option.kt | 12 + .../org/proxydroid/utils/RegexValidator.kt | 89 +++++++ .../org/proxydroid/utils/Tun2SocksHelper.kt | 73 ++++++ .../main/java/org/proxydroid/utils/Utils.kt | 234 +++++++++++++++++ build.gradle | 2 + 20 files changed, 1989 insertions(+) create mode 100644 app/src/main/java/org/proxydroid/ConnectivityBroadcastReceiver.kt create mode 100644 app/src/main/java/org/proxydroid/Exec.kt create mode 100644 app/src/main/java/org/proxydroid/InnerSocketBuilder.kt create mode 100644 app/src/main/java/org/proxydroid/Profile.kt create mode 100644 app/src/main/java/org/proxydroid/ProxyDroidApplication.kt create mode 100644 app/src/main/java/org/proxydroid/ProxyDroidReceiver.kt create mode 100644 app/src/main/java/org/proxydroid/ProxyDroidService.kt create mode 100644 app/src/main/java/org/proxydroid/ProxyDroidVpnService.kt create mode 100644 app/src/main/java/org/proxydroid/ProxyDroidWidgetProvider.kt create mode 100644 app/src/main/java/org/proxydroid/ProxyedApp.kt create mode 100644 app/src/main/java/org/proxydroid/utils/Constraints.kt create mode 100644 app/src/main/java/org/proxydroid/utils/ImageLoader.kt create mode 100644 app/src/main/java/org/proxydroid/utils/ImageLoaderFactory.kt create mode 100644 app/src/main/java/org/proxydroid/utils/LocalProxyServer.kt create mode 100644 app/src/main/java/org/proxydroid/utils/Option.kt create mode 100644 app/src/main/java/org/proxydroid/utils/RegexValidator.kt create mode 100644 app/src/main/java/org/proxydroid/utils/Tun2SocksHelper.kt create mode 100644 app/src/main/java/org/proxydroid/utils/Utils.kt diff --git a/app/build.gradle b/app/build.gradle index 6cebd071..2dcdc74f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,6 @@ plugins { id 'com.android.application' + id 'org.jetbrains.kotlin.android' } android { @@ -35,6 +36,10 @@ android { targetCompatibility JavaVersion.VERSION_11 } + kotlinOptions { + jvmTarget = '11' + } + externalNativeBuild { cmake { version '3.22.1' @@ -51,6 +56,8 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.9.0' + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation 'androidx.core:core-ktx:1.10.1' // Exclude JUnit from json-simple (it pulls junit as compile dependency) implementation('com.googlecode.json-simple:json-simple:1.1.1') { exclude group: 'junit', module: 'junit' diff --git a/app/src/main/java/org/proxydroid/ConnectivityBroadcastReceiver.kt b/app/src/main/java/org/proxydroid/ConnectivityBroadcastReceiver.kt new file mode 100644 index 00000000..31155e45 --- /dev/null +++ b/app/src/main/java/org/proxydroid/ConnectivityBroadcastReceiver.kt @@ -0,0 +1,174 @@ +/* proxydroid - Global / Individual Proxy App for Android + * Copyright (C) 2011 Max Lv + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.proxydroid + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.net.ConnectivityManager +import android.net.NetworkInfo +import android.net.wifi.WifiManager +import android.os.Handler +import android.os.Looper +import androidx.preference.PreferenceManager +import com.ksmaze.android.preference.ListPreferenceMultiSelect +import org.proxydroid.utils.Constraints +import org.proxydroid.utils.Utils + +class ConnectivityBroadcastReceiver : BroadcastReceiver() { + + private val handler = Handler(Looper.getMainLooper()) + + companion object { + private const val TAG = "ConnectivityBroadcastReceiver" + } + + override fun onReceive(context: Context, intent: Intent) { + if (Utils.isConnecting()) return + if (intent.action != ConnectivityManager.CONNECTIVITY_ACTION) return + + handler.post { + val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val networkInfo = manager.activeNetworkInfo + + if (networkInfo != null) { + if (networkInfo.state == NetworkInfo.State.CONNECTING || + networkInfo.state == NetworkInfo.State.DISCONNECTING || + networkInfo.state == NetworkInfo.State.UNKNOWN + ) { + return@post + } + } else { + if (!Utils.isWorking()) return@post + } + + val settings = PreferenceManager.getDefaultSharedPreferences(context) + val profile = Profile() + profile.getProfile(settings) + + // Store current settings first + val oldProfile = settings.getString("profile", "1") ?: "1" + settings.edit().putString(oldProfile, profile.toString()).apply() + + // Load all profiles + val profileValues = settings.getString("profileValues", "")?.split("\\|".toRegex()) ?: emptyList() + var curSSID: String? = null + val lastSSID = settings.getString("lastSSID", "-1") ?: "-1" + var autoConnect = false + + // Test on each profile + for (profileId in profileValues) { + if (profileId.isEmpty()) continue + val profileString = settings.getString(profileId, "") ?: "" + profile.decodeJson(profileString) + curSSID = onlineSSID(context, profile.ssid, profile.excludedSsid) + if (profile.isAutoConnect && curSSID != null) { + autoConnect = true + settings.edit().putString("profile", profileId).apply() + profile.setProfile(settings) + break + } + } + + if (networkInfo == null) { + if (lastSSID != Constraints.ONLY_3G && + lastSSID != Constraints.WIFI_AND_3G && + lastSSID != Constraints.ONLY_WIFI + ) { + if (Utils.isWorking()) { + context.stopService(Intent(context, ProxyDroidService::class.java)) + } + } + } else { + if (networkInfo.state != NetworkInfo.State.CONNECTED) return@post + + if (networkInfo.type == ConnectivityManager.TYPE_WIFI) { + if (lastSSID != "-1") { + val wm = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + val wInfo = wm.connectionInfo + if (wInfo != null) { + var current = wInfo.ssid + current = current?.replace("\"", "") + if (current != null && current != lastSSID) { + if (Utils.isWorking()) { + context.stopService(Intent(context, ProxyDroidService::class.java)) + } + } + } + } + } else { + if (lastSSID != Constraints.ONLY_3G && lastSSID != Constraints.WIFI_AND_3G) { + if (Utils.isWorking()) { + context.stopService(Intent(context, ProxyDroidService::class.java)) + } + } + } + } + + if (autoConnect) { + if (!Utils.isWorking()) { + val pdr = ProxyDroidReceiver() + settings.edit().putString("lastSSID", curSSID).apply() + Utils.setConnecting(true) + pdr.onReceive(context, intent) + } + } + } + } + + fun onlineSSID(context: Context, ssid: String, excludedSsid: String): String? { + val ssids = ListPreferenceMultiSelect.parseStoredValue(ssid) ?: return null + val excludedSsids = ListPreferenceMultiSelect.parseStoredValue(excludedSsid) + + if (ssids.isEmpty()) return null + + val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val networkInfo = manager.activeNetworkInfo ?: return null + + if (networkInfo.type != ConnectivityManager.TYPE_WIFI) { + for (item in ssids) { + if (Constraints.WIFI_AND_3G == item) return item + if (Constraints.ONLY_3G == item) return item + } + return null + } + + val wm = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + val wInfo = wm.connectionInfo + if (wInfo?.ssid == null) return null + + var current = wInfo.ssid + if (current.isNullOrEmpty()) return null + current = current.replace("\"", "") + + if (excludedSsids != null) { + for (item in excludedSsids) { + if (current == item) { + return null // Never connect proxy on excluded ssid + } + } + } + + for (item in ssids) { + if (Constraints.WIFI_AND_3G == item) return item + if (Constraints.ONLY_WIFI == item) return item + if (current == item) return item + } + return null + } +} diff --git a/app/src/main/java/org/proxydroid/Exec.kt b/app/src/main/java/org/proxydroid/Exec.kt new file mode 100644 index 00000000..be1ca2b9 --- /dev/null +++ b/app/src/main/java/org/proxydroid/Exec.kt @@ -0,0 +1,45 @@ +/* proxydroid - Global / Individual Proxy App for Android + * Copyright (C) 2011 Max Lv + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.proxydroid + +import java.io.FileDescriptor + +object Exec { + init { + System.loadLibrary("exec") + } + + @JvmStatic + external fun close(fd: FileDescriptor) + + @JvmStatic + external fun createSubprocess( + rdt: Int, + cmd: String, + args: Array?, + envVars: Array?, + scripts: String?, + processId: IntArray + ): FileDescriptor + + @JvmStatic + external fun hangupProcessGroup(processId: Int) + + @JvmStatic + external fun waitFor(processId: Int): Int +} diff --git a/app/src/main/java/org/proxydroid/InnerSocketBuilder.kt b/app/src/main/java/org/proxydroid/InnerSocketBuilder.kt new file mode 100644 index 00000000..1d3402ff --- /dev/null +++ b/app/src/main/java/org/proxydroid/InnerSocketBuilder.kt @@ -0,0 +1,39 @@ +package org.proxydroid + +import android.util.Log +import java.io.IOException +import java.net.Socket + +class InnerSocketBuilder( + private val proxyHost: String = "127.0.0.1", + private val proxyPort: Int = 1053, + target: String +) { + var socket: Socket? = null + private set + + var isConnected: Boolean = false + private set + + companion object { + private const val TAG = "CMWRAP->InnerSocketBuilder" + } + + init { + connect() + } + + private fun connect() { + Log.v(TAG, "建立通道") + + try { + socket = Socket(proxyHost, proxyPort).apply { + keepAlive = true + soTimeout = 60 * 1000 + } + isConnected = true + } catch (e: IOException) { + Log.e(TAG, "建立隧道失败:${e.localizedMessage}") + } + } +} diff --git a/app/src/main/java/org/proxydroid/Profile.kt b/app/src/main/java/org/proxydroid/Profile.kt new file mode 100644 index 00000000..092501b7 --- /dev/null +++ b/app/src/main/java/org/proxydroid/Profile.kt @@ -0,0 +1,218 @@ +/* proxydroid - Global / Individual Proxy App for Android + * Copyright (C) 2011 Max Lv + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.proxydroid + +import android.content.SharedPreferences +import android.util.Log +import org.json.simple.JSONObject +import org.json.simple.parser.JSONParser +import org.proxydroid.utils.Base64 + +class Profile { + var name: String = "" + var host: String = "" + var port: Int = 0 + var user: String = "" + var password: String = "" + var domain: String = "" + var proxyType: String = "" + var ssid: String = "" + var excludedSsid: String = "" + var proxyApps: String = "" + var bypassAddrs: String = "" + var certificate: String = "" + + var isAuth: Boolean = false + var isNTLM: Boolean = false + var isDNSProxy: Boolean = false + var isPAC: Boolean = false + var isAutoSetProxy: Boolean = false + var isBypassApps: Boolean = false + var isAutoConnect: Boolean = false + + fun getProfile(settings: SharedPreferences) { + name = settings.getString("name", "") ?: "" + host = settings.getString("host", "") ?: "" + user = settings.getString("user", "") ?: "" + password = settings.getString("password", "") ?: "" + domain = settings.getString("domain", "") ?: "" + proxyType = settings.getString("proxyType", "http") ?: "http" + ssid = settings.getString("ssid", "") ?: "" + excludedSsid = settings.getString("excludedSsid", "") ?: "" + proxyApps = settings.getString("proxyApps", "") ?: "" + bypassAddrs = settings.getString("bypassAddrs", "") ?: "" + certificate = settings.getString("certificate", "") ?: "" + + port = try { + settings.getString("port", "")?.toIntOrNull() ?: 0 + } catch (e: NumberFormatException) { + 0 + } + + isAuth = settings.getBoolean("isAuth", false) + isNTLM = settings.getBoolean("isNTLM", false) + isDNSProxy = settings.getBoolean("isDNSProxy", false) + isPAC = settings.getBoolean("isPAC", false) + isAutoSetProxy = settings.getBoolean("isAutoSetProxy", false) + isBypassApps = settings.getBoolean("isBypassApps", false) + isAutoConnect = settings.getBoolean("isAutoConnect", false) + } + + fun setProfile(settings: SharedPreferences) { + val editor = settings.edit() + editor.putString("name", name) + editor.putString("host", host) + editor.putString("port", port.toString()) + editor.putString("user", user) + editor.putString("password", password) + editor.putString("domain", domain) + editor.putString("proxyType", proxyType) + editor.putString("ssid", ssid) + editor.putString("excludedSsid", excludedSsid) + editor.putString("proxyApps", proxyApps) + editor.putString("bypassAddrs", bypassAddrs) + editor.putString("certificate", certificate) + editor.putBoolean("isAuth", isAuth) + editor.putBoolean("isNTLM", isNTLM) + editor.putBoolean("isDNSProxy", isDNSProxy) + editor.putBoolean("isPAC", isPAC) + editor.putBoolean("isAutoSetProxy", isAutoSetProxy) + editor.putBoolean("isBypassApps", isBypassApps) + editor.putBoolean("isAutoConnect", isAutoConnect) + editor.apply() + } + + override fun toString(): String { + val json = JSONObject() + json["name"] = name + json["host"] = host + json["port"] = port + json["user"] = user + json["password"] = password + json["domain"] = domain + json["proxyType"] = proxyType + json["ssid"] = ssid + json["excludedSsid"] = excludedSsid + json["proxyApps"] = proxyApps + json["bypassAddrs"] = bypassAddrs + json["certificate"] = certificate + json["isAuth"] = isAuth + json["isNTLM"] = isNTLM + json["isDNSProxy"] = isDNSProxy + json["isPAC"] = isPAC + json["isAutoSetProxy"] = isAutoSetProxy + json["isBypassApps"] = isBypassApps + json["isAutoConnect"] = isAutoConnect + return json.toJSONString() + } + + fun decodeJson(encoded: String) { + if (encoded.isEmpty()) return + try { + val parser = JSONParser() + val json = parser.parse(encoded) as JSONObject + name = json["name"] as? String ?: "" + host = json["host"] as? String ?: "" + port = (json["port"] as? Number)?.toInt() ?: 0 + user = json["user"] as? String ?: "" + password = json["password"] as? String ?: "" + domain = json["domain"] as? String ?: "" + proxyType = json["proxyType"] as? String ?: "http" + ssid = json["ssid"] as? String ?: "" + excludedSsid = json["excludedSsid"] as? String ?: "" + proxyApps = json["proxyApps"] as? String ?: "" + bypassAddrs = json["bypassAddrs"] as? String ?: "" + certificate = json["certificate"] as? String ?: "" + isAuth = json["isAuth"] as? Boolean ?: false + isNTLM = json["isNTLM"] as? Boolean ?: false + isDNSProxy = json["isDNSProxy"] as? Boolean ?: false + isPAC = json["isPAC"] as? Boolean ?: false + isAutoSetProxy = json["isAutoSetProxy"] as? Boolean ?: false + isBypassApps = json["isBypassApps"] as? Boolean ?: false + isAutoConnect = json["isAutoConnect"] as? Boolean ?: false + } catch (e: Exception) { + Log.e(TAG, "Error parsing profile JSON", e) + } + } + + companion object { + private const val TAG = "Profile" + + @JvmStatic + fun validateAddr(addr: String?): String? { + if (addr.isNullOrEmpty()) return null + val trimmed = addr.trim() + if (trimmed.isEmpty()) return null + + // Check for CIDR notation + val parts = trimmed.split("/") + if (parts.size > 2) return null + + val ipPart = parts[0] + val maskPart = if (parts.size == 2) parts[1] else null + + // Validate IP address + val ipParts = ipPart.split(".") + if (ipParts.size != 4) return null + + for (part in ipParts) { + val num = part.toIntOrNull() ?: return null + if (num < 0 || num > 255) return null + } + + // Validate mask if present + if (maskPart != null) { + val mask = maskPart.toIntOrNull() ?: return null + if (mask < 0 || mask > 32) return null + } + + return trimmed + } + + @JvmStatic + fun encodeAddrs(addrs: Array?): String { + if (addrs == null || addrs.isEmpty()) return "" + val sb = StringBuilder() + for (addr in addrs) { + val encoded = Base64.encodeToString(addr.toByteArray(), Base64.NO_WRAP) + sb.append(encoded).append("|") + } + return sb.toString() + } + + @JvmStatic + fun decodeAddrs(encoded: String?): Array { + if (encoded.isNullOrEmpty()) return emptyArray() + val parts = encoded.split("|") + val result = mutableListOf() + for (part in parts) { + if (part.isNotEmpty()) { + try { + val decoded = String(Base64.decode(part, Base64.NO_WRAP)) + if (decoded.isNotEmpty()) { + result.add(decoded) + } + } catch (e: Exception) { + // Skip invalid entries + } + } + } + return result.toTypedArray() + } + } +} diff --git a/app/src/main/java/org/proxydroid/ProxyDroidApplication.kt b/app/src/main/java/org/proxydroid/ProxyDroidApplication.kt new file mode 100644 index 00000000..b120cfbd --- /dev/null +++ b/app/src/main/java/org/proxydroid/ProxyDroidApplication.kt @@ -0,0 +1,22 @@ +/* proxydroid - Global / Individual Proxy App for Android + * Copyright (C) 2011 Max Lv + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.proxydroid + +import android.app.Application + +class ProxyDroidApplication : Application() diff --git a/app/src/main/java/org/proxydroid/ProxyDroidReceiver.kt b/app/src/main/java/org/proxydroid/ProxyDroidReceiver.kt new file mode 100644 index 00000000..72802a91 --- /dev/null +++ b/app/src/main/java/org/proxydroid/ProxyDroidReceiver.kt @@ -0,0 +1,54 @@ +/* proxydroid - Global / Individual Proxy App for Android + * Copyright (C) 2011 Max Lv + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.proxydroid + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.preference.PreferenceManager + +class ProxyDroidReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + val settings = PreferenceManager.getDefaultSharedPreferences(context) + val profile = Profile() + profile.getProfile(settings) + + if (profile.isAutoConnect) { + val serviceIntent = Intent(context, ProxyDroidService::class.java) + val bundle = Bundle().apply { + putString("host", profile.host) + putString("user", profile.user) + putString("bypassAddrs", profile.bypassAddrs) + putString("password", profile.password) + putString("domain", profile.domain) + putString("proxyType", profile.proxyType) + putBoolean("isAutoSetProxy", profile.isAutoSetProxy) + putBoolean("isBypassApps", profile.isBypassApps) + putBoolean("isAuth", profile.isAuth) + putBoolean("isNTLM", profile.isNTLM) + putBoolean("isDNSProxy", profile.isDNSProxy) + putBoolean("isPAC", profile.isPAC) + putInt("port", profile.port) + } + serviceIntent.putExtras(bundle) + context.startService(serviceIntent) + } + } +} diff --git a/app/src/main/java/org/proxydroid/ProxyDroidService.kt b/app/src/main/java/org/proxydroid/ProxyDroidService.kt new file mode 100644 index 00000000..e8414b06 --- /dev/null +++ b/app/src/main/java/org/proxydroid/ProxyDroidService.kt @@ -0,0 +1,192 @@ +/* proxydroid - Global / Individual Proxy App for Android + * Copyright (C) 2011 Max Lv + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.proxydroid + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.IBinder +import android.util.Log +import androidx.core.app.NotificationCompat +import org.proxydroid.utils.Utils + +class ProxyDroidService : Service() { + + companion object { + private const val TAG = "ProxyDroidService" + private const val NOTIFICATION_ID = 1 + private const val CHANNEL_ID = "proxydroid_channel" + } + + private var host: String = "" + private var port: Int = 0 + private var user: String = "" + private var password: String = "" + private var domain: String = "" + private var proxyType: String = "http" + private var bypassAddrs: String = "" + private var isAuth: Boolean = false + private var isNTLM: Boolean = false + private var isDNSProxy: Boolean = false + private var isPAC: Boolean = false + private var isAutoSetProxy: Boolean = false + private var isBypassApps: Boolean = false + + override fun onCreate() { + super.onCreate() + Log.d(TAG, "ProxyDroid service created") + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (intent == null) { + stopSelf() + return START_NOT_STICKY + } + + val bundle = intent.extras + if (bundle != null) { + host = bundle.getString("host", "") + port = bundle.getInt("port", 0) + user = bundle.getString("user", "") + password = bundle.getString("password", "") + domain = bundle.getString("domain", "") + proxyType = bundle.getString("proxyType", "http") + bypassAddrs = bundle.getString("bypassAddrs", "") + isAuth = bundle.getBoolean("isAuth", false) + isNTLM = bundle.getBoolean("isNTLM", false) + isDNSProxy = bundle.getBoolean("isDNSProxy", false) + isPAC = bundle.getBoolean("isPAC", false) + isAutoSetProxy = bundle.getBoolean("isAutoSetProxy", false) + isBypassApps = bundle.getBoolean("isBypassApps", false) + } + + startForeground(NOTIFICATION_ID, createNotification()) + + Thread { + startProxy() + }.start() + + return START_STICKY + } + + override fun onDestroy() { + super.onDestroy() + stopProxy() + Utils.setWorking(false) + Utils.setConnecting(false) + Log.d(TAG, "ProxyDroid service destroyed") + } + + override fun onBind(intent: Intent?): IBinder? = null + + private fun createNotification(): Notification { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + CHANNEL_ID, + "ProxyDroid Service", + NotificationManager.IMPORTANCE_LOW + ).apply { + description = "ProxyDroid proxy service notification" + } + val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } + + val pendingIntent = PendingIntent.getActivity( + this, + 0, + Intent(this, ProxyDroid::class.java), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + return NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle(getString(R.string.app_name)) + .setContentText(getString(R.string.service_running)) + .setSmallIcon(R.drawable.ic_stat_proxydroid) + .setContentIntent(pendingIntent) + .setOngoing(true) + .build() + } + + private fun startProxy() { + Log.d(TAG, "Starting proxy: $host:$port type=$proxyType") + Utils.setConnecting(true) + + try { + if (Utils.isRoot()) { + setupIptables() + } + Utils.setWorking(true) + } catch (e: Exception) { + Log.e(TAG, "Error starting proxy", e) + } finally { + Utils.setConnecting(false) + } + } + + private fun stopProxy() { + Log.d(TAG, "Stopping proxy") + try { + if (Utils.isRoot()) { + clearIptables() + } + } catch (e: Exception) { + Log.e(TAG, "Error stopping proxy", e) + } + } + + private fun setupIptables() { + val iptables = Utils.getIptablesPath() + val commands = StringBuilder() + + // Clear existing rules + commands.append("$iptables -t nat -F OUTPUT\n") + + // Add bypass rules for localhost + commands.append("$iptables -t nat -A OUTPUT -d 127.0.0.1 -j RETURN\n") + + // Add bypass rules for configured addresses + val addrs = Profile.decodeAddrs(bypassAddrs) + for (addr in addrs) { + if (addr.isNotEmpty()) { + commands.append("$iptables -t nat -A OUTPUT -d $addr -j RETURN\n") + } + } + + // Add proxy redirect rule based on proxy type + val localPort = when (proxyType) { + "http" -> 8123 + "socks4", "socks5" -> 1080 + else -> 8123 + } + + commands.append("$iptables -t nat -A OUTPUT -p tcp -j REDIRECT --to-ports $localPort\n") + + Utils.runRootCommand(commands.toString()) + } + + private fun clearIptables() { + val iptables = Utils.getIptablesPath() + Utils.runRootCommand("$iptables -t nat -F OUTPUT") + } +} diff --git a/app/src/main/java/org/proxydroid/ProxyDroidVpnService.kt b/app/src/main/java/org/proxydroid/ProxyDroidVpnService.kt new file mode 100644 index 00000000..dba09013 --- /dev/null +++ b/app/src/main/java/org/proxydroid/ProxyDroidVpnService.kt @@ -0,0 +1,239 @@ +/* proxydroid - Global / Individual Proxy App for Android + * Copyright (C) 2011 Max Lv + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.proxydroid + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.net.VpnService +import android.os.Build +import android.os.ParcelFileDescriptor +import android.util.Log +import androidx.core.app.NotificationCompat +import org.proxydroid.utils.LocalProxyServer +import org.proxydroid.utils.Tun2SocksHelper +import org.proxydroid.utils.Utils + +class ProxyDroidVpnService : VpnService() { + + companion object { + private const val TAG = "ProxyDroidVpnService" + private const val NOTIFICATION_ID = 2 + private const val CHANNEL_ID = "proxydroid_vpn_channel" + private const val VPN_MTU = 1500 + private const val VPN_ADDRESS = "10.0.0.1" + private const val VPN_ROUTE = "0.0.0.0" + private const val LOCAL_SOCKS_PORT = 1080 + } + + private var vpnInterface: ParcelFileDescriptor? = null + private var tun2SocksHelper: Tun2SocksHelper? = null + private var localProxyServer: LocalProxyServer? = null + + private var host: String = "" + private var port: Int = 0 + private var user: String = "" + private var password: String = "" + private var proxyType: String = "socks5" + private var proxyApps: String = "" + private var isBypassApps: Boolean = false + + override fun onCreate() { + super.onCreate() + Log.d(TAG, "VPN Service created") + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (intent == null) { + stopSelf() + return START_NOT_STICKY + } + + val bundle = intent.extras + if (bundle != null) { + host = bundle.getString("host", "") + port = bundle.getInt("port", 0) + user = bundle.getString("user", "") + password = bundle.getString("password", "") + proxyType = bundle.getString("proxyType", "socks5") + proxyApps = bundle.getString("proxyApps", "") + isBypassApps = bundle.getBoolean("isBypassApps", false) + } + + startForeground(NOTIFICATION_ID, createNotification()) + + Thread { + startVpn() + }.start() + + return START_STICKY + } + + override fun onDestroy() { + super.onDestroy() + stopVpn() + Utils.setWorking(false) + Utils.setConnecting(false) + Log.d(TAG, "VPN Service destroyed") + } + + override fun onRevoke() { + super.onRevoke() + stopVpn() + stopSelf() + } + + private fun createNotification(): Notification { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + CHANNEL_ID, + "ProxyDroid VPN Service", + NotificationManager.IMPORTANCE_LOW + ).apply { + description = "ProxyDroid VPN service notification" + } + val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } + + val pendingIntent = PendingIntent.getActivity( + this, + 0, + Intent(this, ProxyDroid::class.java), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + return NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle(getString(R.string.app_name)) + .setContentText(getString(R.string.vpn_running)) + .setSmallIcon(R.drawable.ic_stat_proxydroid) + .setContentIntent(pendingIntent) + .setOngoing(true) + .build() + } + + private fun startVpn() { + Log.d(TAG, "Starting VPN with proxy: $host:$port") + Utils.setConnecting(true) + + try { + // Start local SOCKS proxy if needed (for HTTP proxy) + if (proxyType == "http") { + localProxyServer = LocalProxyServer( + LOCAL_SOCKS_PORT, + host, + port, + if (user.isNotEmpty()) user else null, + if (password.isNotEmpty()) password else null + ) + localProxyServer?.start() + } + + // Configure VPN + val builder = Builder() + .setSession(getString(R.string.app_name)) + .setMtu(VPN_MTU) + .addAddress(VPN_ADDRESS, 24) + .addRoute(VPN_ROUTE, 0) + .addDnsServer("8.8.8.8") + .addDnsServer("8.8.4.4") + + // Add per-app proxy if configured + if (proxyApps.isNotEmpty()) { + val apps = proxyApps.split("|") + if (isBypassApps) { + // Bypass mode: exclude these apps from VPN + for (app in apps) { + if (app.isNotEmpty()) { + try { + builder.addDisallowedApplication(app) + } catch (e: Exception) { + Log.w(TAG, "Failed to exclude app: $app", e) + } + } + } + } else { + // Proxy mode: only proxy these apps + for (app in apps) { + if (app.isNotEmpty()) { + try { + builder.addAllowedApplication(app) + } catch (e: Exception) { + Log.w(TAG, "Failed to allow app: $app", e) + } + } + } + } + } + + vpnInterface = builder.establish() + + if (vpnInterface == null) { + Log.e(TAG, "Failed to establish VPN interface") + Utils.setConnecting(false) + return + } + + // Start tun2socks + tun2SocksHelper = Tun2SocksHelper() + val socksHost = if (proxyType == "http") "127.0.0.1" else host + val socksPort = if (proxyType == "http") LOCAL_SOCKS_PORT else port + + val started = tun2SocksHelper?.start( + vpnInterface!!.fd, + VPN_MTU, + socksHost, + socksPort, + if (user.isNotEmpty() && proxyType != "http") user else null, + if (password.isNotEmpty() && proxyType != "http") password else null + ) ?: false + + if (started) { + Utils.setWorking(true) + Log.d(TAG, "VPN started successfully") + } else { + Log.e(TAG, "Failed to start tun2socks") + } + + } catch (e: Exception) { + Log.e(TAG, "Error starting VPN", e) + } finally { + Utils.setConnecting(false) + } + } + + private fun stopVpn() { + Log.d(TAG, "Stopping VPN") + + try { + tun2SocksHelper?.stop() + tun2SocksHelper = null + + localProxyServer?.stop() + localProxyServer = null + + vpnInterface?.close() + vpnInterface = null + } catch (e: Exception) { + Log.e(TAG, "Error stopping VPN", e) + } + } +} diff --git a/app/src/main/java/org/proxydroid/ProxyDroidWidgetProvider.kt b/app/src/main/java/org/proxydroid/ProxyDroidWidgetProvider.kt new file mode 100644 index 00000000..b7576a78 --- /dev/null +++ b/app/src/main/java/org/proxydroid/ProxyDroidWidgetProvider.kt @@ -0,0 +1,117 @@ +/* proxydroid - Global / Individual Proxy App for Android + * Copyright (C) 2011 Max Lv + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.proxydroid + +import android.app.PendingIntent +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.widget.RemoteViews +import androidx.preference.PreferenceManager +import org.proxydroid.utils.Utils + +class ProxyDroidWidgetProvider : AppWidgetProvider() { + + companion object { + const val PROXY_SWITCH_ACTION = "org.proxydroid.ProxyDroidWidgetProvider.PROXY_SWITCH_ACTION" + const val SERVICE_NAME = "org.proxydroid.ProxyDroidService" + private const val TAG = "ProxyDroidWidgetProvider" + } + + override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { + for (appWidgetId in appWidgetIds) { + val intent = Intent(context, ProxyDroidWidgetProvider::class.java).apply { + action = PROXY_SWITCH_ACTION + } + val pendingIntent = PendingIntent.getBroadcast( + context, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + val views = RemoteViews(context.packageName, R.layout.proxydroid_appwidget) + views.setOnClickPendingIntent(R.id.serviceToggle, pendingIntent) + + if (Utils.isWorking()) { + views.setImageViewResource(R.id.serviceToggle, R.drawable.on) + Log.d(TAG, "Service running") + } else { + views.setImageViewResource(R.id.serviceToggle, R.drawable.off) + Log.d(TAG, "Service stopped") + } + + appWidgetManager.updateAppWidget(appWidgetId, views) + } + } + + override fun onReceive(context: Context, intent: Intent) { + super.onReceive(context, intent) + + if (intent.action == PROXY_SWITCH_ACTION) { + val views = RemoteViews(context.packageName, R.layout.proxydroid_appwidget) + try { + views.setImageViewResource(R.id.serviceToggle, R.drawable.ing) + val awm = AppWidgetManager.getInstance(context) + awm.updateAppWidget( + awm.getAppWidgetIds(ComponentName(context, ProxyDroidWidgetProvider::class.java)), + views + ) + } catch (e: Exception) { + // Nothing + } + + Log.d(TAG, "Proxy switch action") + + if (Utils.isWorking()) { + // Service is working, so stop it + try { + context.stopService(Intent(context, ProxyDroidService::class.java)) + } catch (e: Exception) { + // Nothing + } + } else { + // Service is not working, then start it + val settings = PreferenceManager.getDefaultSharedPreferences(context) + val profile = Profile() + profile.getProfile(settings) + + val serviceIntent = Intent(context, ProxyDroidService::class.java) + val bundle = Bundle().apply { + putString("host", profile.host) + putString("user", profile.user) + putString("bypassAddrs", profile.bypassAddrs) + putString("password", profile.password) + putString("domain", profile.domain) + putString("proxyType", profile.proxyType) + putBoolean("isAutoSetProxy", profile.isAutoSetProxy) + putBoolean("isBypassApps", profile.isBypassApps) + putBoolean("isAuth", profile.isAuth) + putBoolean("isNTLM", profile.isNTLM) + putBoolean("isDNSProxy", profile.isDNSProxy) + putBoolean("isPAC", profile.isPAC) + putInt("port", profile.port) + } + serviceIntent.putExtras(bundle) + context.startService(serviceIntent) + } + } + } +} diff --git a/app/src/main/java/org/proxydroid/ProxyedApp.kt b/app/src/main/java/org/proxydroid/ProxyedApp.kt new file mode 100644 index 00000000..94f34829 --- /dev/null +++ b/app/src/main/java/org/proxydroid/ProxyedApp.kt @@ -0,0 +1,29 @@ +/* proxydroid - Global / Individual Proxy App for Android + * Copyright (C) 2011 Max Lv + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.proxydroid + +import android.graphics.drawable.Drawable + +data class ProxyedApp( + var enabled: Boolean = false, + var uid: Int = 0, + var username: String = "", + var procname: String = "", + var name: String = "", + var icon: Drawable? = null +) diff --git a/app/src/main/java/org/proxydroid/utils/Constraints.kt b/app/src/main/java/org/proxydroid/utils/Constraints.kt new file mode 100644 index 00000000..9b6b8c67 --- /dev/null +++ b/app/src/main/java/org/proxydroid/utils/Constraints.kt @@ -0,0 +1,56 @@ +package org.proxydroid.utils + +object Constraints { + const val ONLY_3G = "2G/3G" + const val ONLY_WIFI = "WIFI" + const val WIFI_AND_3G = "WIFI/2G/3G" + const val FILE_PATH = "file_path" + const val IMPORT_REQUEST = 0 + + @JvmField + val PRESETS: Array> = arrayOf( + emptyArray(), + arrayOf("127.0.0.1"), + arrayOf("127.0.0.1", "10.0.0.0/8", "192.168.0.0/16", "172.16.0.0/12"), + // China routes are gotten from https://github.com/17mon/china_ip_list + // Shrunk for better performance by removing subnets which have netmasks >= 20. + arrayOf( + "1.0.32.0/19", "1.1.32.0/19", "1.2.32.0/19", "1.2.64.0/18", + "1.3.0.0/16", "1.4.32.0/19", "1.4.64.0/18", "1.8.32.0/19", + "1.8.64.0/19", "1.8.160.0/19", "1.8.192.0/19", "1.10.32.0/19", + "1.10.64.0/18", "1.12.0.0/14", "1.24.0.0/13", "1.45.0.0/16", + "1.48.0.0/14", "1.56.0.0/13", "1.68.0.0/14", "1.80.0.0/12", + "1.116.0.0/14", "1.180.0.0/14", "1.184.0.0/15", "1.188.0.0/14", + "1.192.0.0/13", "1.202.0.0/15", "1.204.0.0/14", "14.16.0.0/12", + "14.103.0.0/16", "14.104.0.0/13", "14.112.0.0/12", "14.130.0.0/15", + "14.134.0.0/15", "14.144.0.0/12", "14.196.0.0/15", "14.204.0.0/15", + "14.208.0.0/12", "27.8.0.0/13", "27.16.0.0/12", "27.36.0.0/14", + "27.40.0.0/13", "27.50.128.0/17", "27.54.192.0/18", "27.98.224.0/19", + "27.99.128.0/17", "27.103.0.0/16", "27.106.128.0/18", "27.109.32.0/19", + "27.112.0.0/18", "27.113.128.0/18", "27.115.0.0/17", "27.128.0.0/15", + "27.144.0.0/16", "27.148.0.0/14", "27.152.0.0/13", "27.184.0.0/13", + "27.192.0.0/11", "27.224.0.0/14", "36.0.32.0/19", "36.0.64.0/18", + "36.0.128.0/17", "36.1.0.0/16", "36.4.0.0/14", "36.16.0.0/12", + "36.32.0.0/14", "36.36.0.0/16", "36.37.0.0/19", "36.40.0.0/13", + "36.48.0.0/15", "36.51.0.0/17", "36.51.128.0/18", "36.51.192.0/19", + "36.56.0.0/13", "36.96.0.0/11", "36.128.0.0/10", "36.192.0.0/11", + "36.248.0.0/14", "36.254.0.0/16", "39.0.32.0/19", "39.0.64.0/18", + "39.0.128.0/17", "39.64.0.0/11", "39.96.0.0/13", "39.104.0.0/14", + "39.108.0.0/16", "39.128.0.0/10", "40.72.0.0/16", "40.125.128.0/17", + "40.126.64.0/18", "42.0.32.0/19", "42.0.128.0/19", "42.0.224.0/19", + "42.1.0.0/19", "42.4.0.0/14", "42.48.0.0/13", "42.56.0.0/14", + "42.62.0.0/17", "42.62.128.0/19", "42.63.0.0/16", "42.80.0.0/15", + "42.83.96.0/19", "42.83.160.0/19", "42.83.192.0/18", "42.84.0.0/14", + "42.88.0.0/13", "42.96.64.0/19", "42.96.128.0/17", "42.97.0.0/16", + "42.99.0.0/18", "42.99.64.0/19", "42.100.0.0/14", "42.120.0.0/15", + "42.122.0.0/16", "42.123.0.0/19", "42.123.64.0/18", "42.123.128.0/19", + "42.123.192.0/18", "42.128.0.0/12", "42.156.0.0/19", "42.156.64.0/18", + "42.156.128.0/17", "42.157.0.0/16", "42.158.0.0/15", "42.160.0.0/12", + "42.176.0.0/13", "42.184.0.0/15", "42.186.0.0/16", "42.187.0.0/18", + "42.187.64.0/19", "42.187.128.0/17", "42.192.0.0/13", "42.201.0.0/17", + "42.202.0.0/15", "42.204.0.0/14", "42.208.0.0/12", "42.224.0.0/12", + "42.240.0.0/16", "42.242.0.0/15", "42.244.0.0/15", "42.246.0.0/16", + "42.247.32.0/19", "42.247.64.0/18", "42.247.128.0/17", "42.248.0.0/13" + ) + ) +} diff --git a/app/src/main/java/org/proxydroid/utils/ImageLoader.kt b/app/src/main/java/org/proxydroid/utils/ImageLoader.kt new file mode 100644 index 00000000..aac07fff --- /dev/null +++ b/app/src/main/java/org/proxydroid/utils/ImageLoader.kt @@ -0,0 +1,170 @@ +package org.proxydroid.utils + +import android.app.Activity +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.drawable.BitmapDrawable +import android.widget.ImageView +import org.proxydroid.R +import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.util.* + +class ImageLoader(context: Context) { + private val cache = HashMap() + private val cacheDir: File = context.cacheDir + private val context: Context = context + private val stubId = R.drawable.sym_def_app_icon + private val photosQueue = PhotosQueue() + private val photoLoaderThread = PhotosLoader() + + init { + photoLoaderThread.priority = Thread.NORM_PRIORITY - 1 + } + + fun clearCache() { + cache.clear() + cacheDir.listFiles()?.forEach { it.delete() } + } + + private fun decodeFile(f: File): Bitmap? { + return try { + val o = BitmapFactory.Options().apply { + inJustDecodeBounds = true + } + BitmapFactory.decodeStream(FileInputStream(f), null, o) + + val requiredSize = 70 + var widthTmp = o.outWidth + var heightTmp = o.outHeight + var scale = 1 + while (true) { + if (widthTmp / 2 < requiredSize || heightTmp / 2 < requiredSize) break + widthTmp /= 2 + heightTmp /= 2 + scale *= 2 + } + + val o2 = BitmapFactory.Options().apply { + inSampleSize = scale + } + BitmapFactory.decodeStream(FileInputStream(f), null, o2) + } catch (e: FileNotFoundException) { + null + } + } + + fun displayImage(uid: Int, activity: Activity, imageView: ImageView) { + if (cache.containsKey(uid)) { + imageView.setImageBitmap(cache[uid]) + } else { + queuePhoto(uid, activity, imageView) + imageView.setImageResource(stubId) + } + } + + private fun getBitmap(uid: Int): Bitmap? { + val filename = uid.toString() + val f = File(cacheDir, filename) + + // from SD cache + val b = decodeFile(f) + if (b != null) return b + + // from app + return try { + val icon = Utils.getAppIcon(context, uid) as? BitmapDrawable + icon?.bitmap + } catch (ex: Exception) { + null + } + } + + private fun queuePhoto(uid: Int, activity: Activity, imageView: ImageView) { + photosQueue.clean(imageView) + val p = PhotoToLoad(uid, imageView) + synchronized(photosQueue.lock) { + photosQueue.photosToLoad.push(p) + photosQueue.lock.notifyAll() + } + + if (photoLoaderThread.state == Thread.State.NEW) { + photoLoaderThread.start() + } + } + + fun stopThread() { + photoLoaderThread.interrupt() + } + + private inner class BitmapDisplayer( + private val bitmap: Bitmap?, + private val imageView: ImageView + ) : Runnable { + override fun run() { + if (bitmap != null) { + imageView.setImageBitmap(bitmap) + } else { + imageView.setImageResource(stubId) + } + } + } + + private inner class PhotosLoader : Thread() { + override fun run() { + try { + while (true) { + if (photosQueue.photosToLoad.isEmpty()) { + synchronized(photosQueue.lock) { + (photosQueue.lock as Object).wait() + } + } + + synchronized(photosQueue.lock) { + if (photosQueue.photosToLoad.isNotEmpty()) { + val photoToLoad = photosQueue.photosToLoad.pop() + val bmp = getBitmap(photoToLoad.uid) + if (bmp != null) { + cache[photoToLoad.uid] = bmp + } + val tag = photoToLoad.imageView.tag + if (tag != null && tag as Int == photoToLoad.uid) { + val bd = BitmapDisplayer(bmp, photoToLoad.imageView) + val a = photoToLoad.imageView.context as Activity + a.runOnUiThread(bd) + } + } + } + + if (Thread.interrupted()) break + } + } catch (e: InterruptedException) { + // allow thread to exit + } + } + } + + private inner class PhotosQueue { + val photosToLoad = Stack() + val lock = Any() + + fun clean(image: ImageView) { + synchronized(lock) { + try { + val iterator = photosToLoad.iterator() + while (iterator.hasNext()) { + if (iterator.next().imageView == image) { + iterator.remove() + } + } + } catch (e: ArrayIndexOutOfBoundsException) { + // Nothing + } + } + } + } + + private data class PhotoToLoad(val uid: Int, val imageView: ImageView) +} diff --git a/app/src/main/java/org/proxydroid/utils/ImageLoaderFactory.kt b/app/src/main/java/org/proxydroid/utils/ImageLoaderFactory.kt new file mode 100644 index 00000000..d572b868 --- /dev/null +++ b/app/src/main/java/org/proxydroid/utils/ImageLoaderFactory.kt @@ -0,0 +1,30 @@ +// dbartists - Douban artists client for Android +// Copyright (C) 2011 Max Lv +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package org.proxydroid.utils + +import android.content.Context + +object ImageLoaderFactory { + private var imageLoader: ImageLoader? = null + + @JvmStatic + fun getImageLoader(context: Context): ImageLoader { + if (imageLoader == null) { + imageLoader = ImageLoader(context) + } + return imageLoader!! + } +} diff --git a/app/src/main/java/org/proxydroid/utils/LocalProxyServer.kt b/app/src/main/java/org/proxydroid/utils/LocalProxyServer.kt new file mode 100644 index 00000000..a5f560a9 --- /dev/null +++ b/app/src/main/java/org/proxydroid/utils/LocalProxyServer.kt @@ -0,0 +1,187 @@ +/* proxydroid - Global / Individual Proxy App for Android + * Copyright (C) 2011 Max Lv + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.proxydroid.utils + +import android.util.Log +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.net.InetAddress +import java.net.ServerSocket +import java.net.Socket +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +class LocalProxyServer( + private val localPort: Int, + private val remoteHost: String, + private val remotePort: Int, + private val username: String?, + private val password: String? +) : Runnable { + + private var serverSocket: ServerSocket? = null + private var running = false + private var executor: ExecutorService? = null + + companion object { + private const val TAG = "LocalProxyServer" + private const val BUFFER_SIZE = 8192 + } + + @Synchronized + fun start() { + if (running) return + running = true + executor = Executors.newCachedThreadPool() + Thread(this).start() + } + + @Synchronized + fun stop() { + running = false + try { + serverSocket?.close() + } catch (e: IOException) { + Log.e(TAG, "Error closing server socket", e) + } + executor?.shutdownNow() + executor = null + } + + override fun run() { + try { + serverSocket = ServerSocket(localPort, 50, InetAddress.getByName("127.0.0.1")) + Log.i(TAG, "Local SOCKS5 proxy server started on port $localPort") + + while (running) { + try { + val clientSocket = serverSocket?.accept() ?: break + executor?.execute(ClientHandler(clientSocket)) + } catch (e: IOException) { + if (running) { + Log.e(TAG, "Error accepting connection", e) + } + } + } + } catch (e: IOException) { + Log.e(TAG, "Error starting server", e) + } finally { + try { + serverSocket?.close() + } catch (e: IOException) { + // Ignore + } + } + } + + private inner class ClientHandler(private val clientSocket: Socket) : Runnable { + override fun run() { + var remoteSocket: Socket? = null + try { + val clientIn = clientSocket.getInputStream() + val clientOut = clientSocket.getOutputStream() + + // Connect to remote SOCKS5 proxy + remoteSocket = Socket(remoteHost, remotePort) + val remoteIn = remoteSocket.getInputStream() + val remoteOut = remoteSocket.getOutputStream() + + // Authenticate with remote proxy if needed + if (!authenticate(remoteIn, remoteOut)) { + Log.e(TAG, "Authentication failed") + return + } + + // Relay data between client and remote + val clientToRemote = Thread { relay(clientIn, remoteOut) } + val remoteToClient = Thread { relay(remoteIn, clientOut) } + + clientToRemote.start() + remoteToClient.start() + + clientToRemote.join() + remoteToClient.join() + + } catch (e: Exception) { + Log.e(TAG, "Error handling client", e) + } finally { + try { + clientSocket.close() + } catch (e: IOException) { + // Ignore + } + try { + remoteSocket?.close() + } catch (e: IOException) { + // Ignore + } + } + } + + private fun authenticate(input: InputStream, output: OutputStream): Boolean { + return try { + // SOCKS5 greeting + if (username != null && password != null) { + output.write(byteArrayOf(0x05, 0x01, 0x02)) // Version 5, 1 method, Username/Password + } else { + output.write(byteArrayOf(0x05, 0x01, 0x00)) // Version 5, 1 method, No auth + } + output.flush() + + val response = ByteArray(2) + if (input.read(response) != 2) return false + if (response[0] != 0x05.toByte()) return false + + if (response[1] == 0x02.toByte() && username != null && password != null) { + // Username/Password authentication + val authRequest = ByteArray(3 + username.length + password.length) + authRequest[0] = 0x01 + authRequest[1] = username.length.toByte() + System.arraycopy(username.toByteArray(), 0, authRequest, 2, username.length) + authRequest[2 + username.length] = password.length.toByte() + System.arraycopy(password.toByteArray(), 0, authRequest, 3 + username.length, password.length) + output.write(authRequest) + output.flush() + + val authResponse = ByteArray(2) + if (input.read(authResponse) != 2) return false + if (authResponse[1] != 0x00.toByte()) return false + } + + true + } catch (e: IOException) { + Log.e(TAG, "Authentication error", e) + false + } + } + + private fun relay(input: InputStream, output: OutputStream) { + try { + val buffer = ByteArray(BUFFER_SIZE) + var bytesRead: Int + while (input.read(buffer).also { bytesRead = it } != -1) { + output.write(buffer, 0, bytesRead) + output.flush() + } + } catch (e: IOException) { + // Connection closed + } + } + } +} diff --git a/app/src/main/java/org/proxydroid/utils/Option.kt b/app/src/main/java/org/proxydroid/utils/Option.kt new file mode 100644 index 00000000..9ea60ce6 --- /dev/null +++ b/app/src/main/java/org/proxydroid/utils/Option.kt @@ -0,0 +1,12 @@ +package org.proxydroid.utils + +data class Option( + val name: String, + val data: String, + val path: String +) : Comparable