Add android per-app proxy
This commit is contained in:
@@ -1,10 +1,21 @@
|
||||
package com.hiddify.hiddify
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.VectorDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Base64
|
||||
import androidx.annotation.NonNull
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.hiddify.hiddify.Application.Companion.packageManager
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||
@@ -12,6 +23,10 @@ import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.PluginRegistry
|
||||
import io.flutter.plugin.common.StandardMethodCodec
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
|
||||
class PlatformSettingsHandler : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware,
|
||||
PluginRegistry.ActivityResultListener {
|
||||
@@ -23,10 +38,13 @@ class PlatformSettingsHandler : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
const val channelName = "com.hiddify.app/platform.settings"
|
||||
|
||||
const val REQUEST_IGNORE_BATTERY_OPTIMIZATIONS = 44
|
||||
val gson = Gson()
|
||||
|
||||
enum class Trigger(val method: String) {
|
||||
IsIgnoringBatteryOptimizations("is_ignoring_battery_optimizations"),
|
||||
RequestIgnoreBatteryOptimizations("request_ignore_battery_optimizations"),
|
||||
GetInstalledPackages("get_installed_packages"),
|
||||
GetPackagesIcon("get_package_icon"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +89,12 @@ class PlatformSettingsHandler : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
return false
|
||||
}
|
||||
|
||||
data class AppItem(
|
||||
@SerializedName("package-name") val packageName: String,
|
||||
@SerializedName("name") val name: String,
|
||||
@SerializedName("is-system-app") val isSystemApp: Boolean
|
||||
)
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
Trigger.IsIgnoringBatteryOptimizations.method -> {
|
||||
@@ -97,6 +121,69 @@ class PlatformSettingsHandler : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
activity?.startActivityForResult(intent, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
|
||||
}
|
||||
|
||||
Trigger.GetInstalledPackages.method -> {
|
||||
GlobalScope.launch {
|
||||
result.runCatching {
|
||||
val flag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
PackageManager.GET_PERMISSIONS or PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
PackageManager.GET_PERMISSIONS or PackageManager.GET_UNINSTALLED_PACKAGES
|
||||
}
|
||||
val installedPackages =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
packageManager.getInstalledPackages(
|
||||
PackageManager.PackageInfoFlags.of(
|
||||
flag.toLong()
|
||||
)
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
packageManager.getInstalledPackages(flag)
|
||||
}
|
||||
val list = mutableListOf<AppItem>()
|
||||
installedPackages.forEach {
|
||||
if (it.packageName != Application.application.packageName &&
|
||||
(it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|
||||
|| it.packageName == "android")
|
||||
) {
|
||||
list.add(
|
||||
AppItem(
|
||||
it.packageName,
|
||||
it.applicationInfo.loadLabel(packageManager).toString(),
|
||||
it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM == 1
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
list.sortBy { it.name }
|
||||
success(gson.toJson(list))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Trigger.GetPackagesIcon.method -> {
|
||||
result.runCatching {
|
||||
val args = call.arguments as Map<*, *>
|
||||
val packageName =
|
||||
args["packageName"] as String? ?: return error("provide packageName")
|
||||
val drawable = packageManager.getApplicationIcon(packageName)
|
||||
val bitmap = Bitmap.createBitmap(
|
||||
drawable.intrinsicWidth,
|
||||
drawable.intrinsicHeight,
|
||||
Bitmap.Config.ARGB_8888
|
||||
)
|
||||
val canvas = Canvas(bitmap)
|
||||
drawable.setBounds(0, 0, canvas.width, canvas.height)
|
||||
drawable.draw(canvas)
|
||||
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
|
||||
val base64: String =
|
||||
Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP)
|
||||
success(base64)
|
||||
}
|
||||
}
|
||||
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,62 @@
|
||||
package com.hiddify.hiddify
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Base64
|
||||
import com.hiddify.hiddify.bg.ProxyService
|
||||
import com.hiddify.hiddify.bg.VPNService
|
||||
import com.hiddify.hiddify.constant.PerAppProxyMode
|
||||
import com.hiddify.hiddify.constant.ServiceMode
|
||||
import com.hiddify.hiddify.constant.SettingsKey
|
||||
import org.json.JSONObject
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.File
|
||||
import java.io.ObjectInputStream
|
||||
|
||||
|
||||
object Settings {
|
||||
|
||||
const val PER_APP_PROXY_DISABLED = 0
|
||||
const val PER_APP_PROXY_EXCLUDE = 1
|
||||
const val PER_APP_PROXY_INCLUDE = 2
|
||||
|
||||
private val preferences by lazy {
|
||||
val context = Application.application.applicationContext
|
||||
context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
var perAppProxyEnabled = preferences.getBoolean(SettingsKey.PER_APP_PROXY_ENABLED, false)
|
||||
var perAppProxyMode = preferences.getInt(SettingsKey.PER_APP_PROXY_MODE, PER_APP_PROXY_EXCLUDE)
|
||||
var perAppProxyList = preferences.getStringSet(SettingsKey.PER_APP_PROXY_LIST, emptySet())!!
|
||||
var perAppProxyUpdateOnChange =
|
||||
preferences.getInt(SettingsKey.PER_APP_PROXY_UPDATE_ON_CHANGE, PER_APP_PROXY_DISABLED)
|
||||
private const val LIST_IDENTIFIER = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu"
|
||||
|
||||
var perAppProxyMode: String
|
||||
get() = preferences.getString(SettingsKey.PER_APP_PROXY_MODE, PerAppProxyMode.OFF)!!
|
||||
set(value) = preferences.edit().putString(SettingsKey.PER_APP_PROXY_MODE, value).apply()
|
||||
|
||||
val perAppProxyEnabled: Boolean
|
||||
get() = perAppProxyMode != PerAppProxyMode.OFF
|
||||
|
||||
val perAppProxyList: List<String>
|
||||
get() {
|
||||
val stringValue = if (perAppProxyMode == PerAppProxyMode.INCLUDE) {
|
||||
preferences.getString(SettingsKey.PER_APP_PROXY_INCLUDE_LIST, "")!!;
|
||||
} else {
|
||||
preferences.getString(SettingsKey.PER_APP_PROXY_EXCLUDE_LIST, "")!!;
|
||||
}
|
||||
if (!stringValue.startsWith(LIST_IDENTIFIER)) {
|
||||
return emptyList()
|
||||
}
|
||||
return decodeListString(stringValue.substring(LIST_IDENTIFIER.length))
|
||||
}
|
||||
|
||||
private fun decodeListString(listString: String): List<String> {
|
||||
val stream = ObjectInputStream(ByteArrayInputStream(Base64.decode(listString, 0)))
|
||||
return stream.readObject() as List<String>
|
||||
}
|
||||
|
||||
var activeConfigPath: String
|
||||
get() = preferences.getString(SettingsKey.ACTIVE_CONFIG_PATH, "") ?: ""
|
||||
get() = preferences.getString(SettingsKey.ACTIVE_CONFIG_PATH, "")!!
|
||||
set(value) = preferences.edit().putString(SettingsKey.ACTIVE_CONFIG_PATH, value).apply()
|
||||
|
||||
var serviceMode: String
|
||||
get() = preferences.getString(SettingsKey.SERVICE_MODE, ServiceMode.NORMAL)
|
||||
?: ServiceMode.NORMAL
|
||||
get() = preferences.getString(SettingsKey.SERVICE_MODE, ServiceMode.NORMAL)!!
|
||||
set(value) = preferences.edit().putString(SettingsKey.SERVICE_MODE, value).apply()
|
||||
|
||||
var configOptions: String
|
||||
get() = preferences.getString(SettingsKey.CONFIG_OPTIONS, "") ?: ""
|
||||
get() = preferences.getString(SettingsKey.CONFIG_OPTIONS, "")!!
|
||||
set(value) = preferences.edit().putString(SettingsKey.CONFIG_OPTIONS, value).apply()
|
||||
|
||||
var debugMode: Boolean
|
||||
@@ -47,11 +68,13 @@ object Settings {
|
||||
|
||||
var disableMemoryLimit: Boolean
|
||||
get() = preferences.getBoolean(SettingsKey.DISABLE_MEMORY_LIMIT, false)
|
||||
set(value) = preferences.edit().putBoolean(SettingsKey.DISABLE_MEMORY_LIMIT, value).apply()
|
||||
set(value) =
|
||||
preferences.edit().putBoolean(SettingsKey.DISABLE_MEMORY_LIMIT, value).apply()
|
||||
|
||||
var systemProxyEnabled: Boolean
|
||||
get() = preferences.getBoolean(SettingsKey.SYSTEM_PROXY_ENABLED, true)
|
||||
set(value) = preferences.edit().putBoolean(SettingsKey.SYSTEM_PROXY_ENABLED, value).apply()
|
||||
set(value) =
|
||||
preferences.edit().putBoolean(SettingsKey.SYSTEM_PROXY_ENABLED, value).apply()
|
||||
|
||||
var startedByUser: Boolean
|
||||
get() = preferences.getBoolean(SettingsKey.STARTED_BY_USER, false)
|
||||
@@ -80,7 +103,7 @@ object Settings {
|
||||
}
|
||||
|
||||
private suspend fun needVPNService(): Boolean {
|
||||
if(enableTun) return true
|
||||
if (enableTun) return true
|
||||
val filePath = activeConfigPath
|
||||
if (filePath.isBlank()) return false
|
||||
val content = JSONObject(File(filePath).readText())
|
||||
|
||||
@@ -16,22 +16,22 @@ class AppChangeReceiver : BroadcastReceiver() {
|
||||
}
|
||||
|
||||
private fun checkUpdate(context: Context, intent: Intent) {
|
||||
if (!Settings.perAppProxyEnabled) {
|
||||
return
|
||||
}
|
||||
val perAppProxyUpdateOnChange = Settings.perAppProxyUpdateOnChange
|
||||
if (perAppProxyUpdateOnChange == Settings.PER_APP_PROXY_DISABLED) {
|
||||
return
|
||||
}
|
||||
val packageName = intent.dataString?.substringAfter("package:")
|
||||
if (packageName.isNullOrBlank()) {
|
||||
return
|
||||
}
|
||||
if ((perAppProxyUpdateOnChange == Settings.PER_APP_PROXY_INCLUDE)) {
|
||||
Settings.perAppProxyList = Settings.perAppProxyList + packageName
|
||||
} else {
|
||||
Settings.perAppProxyList = Settings.perAppProxyList - packageName
|
||||
}
|
||||
// if (!Settings.perAppProxyEnabled) {
|
||||
// return
|
||||
// }
|
||||
// val perAppProxyUpdateOnChange = Settings.perAppProxyUpdateOnChange
|
||||
// if (perAppProxyUpdateOnChange == Settings.PER_APP_PROXY_DISABLED) {
|
||||
// return
|
||||
// }
|
||||
// val packageName = intent.dataString?.substringAfter("package:")
|
||||
// if (packageName.isNullOrBlank()) {
|
||||
// return
|
||||
// }
|
||||
// if ((perAppProxyUpdateOnChange == Settings.PER_APP_PROXY_INCLUDE)) {
|
||||
// Settings.perAppProxyList = Settings.perAppProxyList + packageName
|
||||
// } else {
|
||||
// Settings.perAppProxyList = Settings.perAppProxyList - packageName
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import android.content.pm.PackageManager.NameNotFoundException
|
||||
import android.net.ProxyInfo
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import com.hiddify.hiddify.constant.PerAppProxyMode
|
||||
import io.nekohasekai.libbox.TunOptions
|
||||
|
||||
class VPNService : VpnService(), PlatformInterfaceWrapper {
|
||||
@@ -87,7 +88,7 @@ class VPNService : VpnService(), PlatformInterfaceWrapper {
|
||||
|
||||
if (Settings.perAppProxyEnabled) {
|
||||
val appList = Settings.perAppProxyList
|
||||
if (Settings.perAppProxyMode == Settings.PER_APP_PROXY_INCLUDE) {
|
||||
if (Settings.perAppProxyMode == PerAppProxyMode.INCLUDE) {
|
||||
appList.forEach {
|
||||
try {
|
||||
builder.addAllowedApplication(it)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.hiddify.hiddify.constant
|
||||
|
||||
object PerAppProxyMode {
|
||||
const val OFF = "off"
|
||||
const val INCLUDE = "include"
|
||||
const val EXCLUDE = "exclude"
|
||||
}
|
||||
@@ -8,10 +8,9 @@ object SettingsKey {
|
||||
|
||||
const val CONFIG_OPTIONS = "config_options_json"
|
||||
|
||||
const val PER_APP_PROXY_ENABLED = "per_app_proxy_enabled"
|
||||
const val PER_APP_PROXY_MODE = "per_app_proxy_mode"
|
||||
const val PER_APP_PROXY_LIST = "per_app_proxy_list"
|
||||
const val PER_APP_PROXY_UPDATE_ON_CHANGE = "per_app_proxy_update_on_change"
|
||||
const val PER_APP_PROXY_MODE = "${KEY_PREFIX}per_app_proxy_mode"
|
||||
const val PER_APP_PROXY_INCLUDE_LIST = "${KEY_PREFIX}per_app_proxy_include_list"
|
||||
const val PER_APP_PROXY_EXCLUDE_LIST = "${KEY_PREFIX}per_app_proxy_exclude_list"
|
||||
|
||||
const val DEBUG_MODE = "${KEY_PREFIX}debug_mode"
|
||||
const val ENABLE_TUN = "${KEY_PREFIX}enable-tun"
|
||||
|
||||
Reference in New Issue
Block a user