Improve per-app proxy

This commit is contained in:
世界
2025-12-18 22:25:15 +08:00
parent 386a401c00
commit 2da0674c33
12 changed files with 322 additions and 203 deletions

View File

@@ -49,6 +49,7 @@ class Application : Application() {
AppChangeReceiver(), AppChangeReceiver(),
IntentFilter().apply { IntentFilter().apply {
addAction(Intent.ACTION_PACKAGE_ADDED) addAction(Intent.ACTION_PACKAGE_ADDED)
addAction(Intent.ACTION_PACKAGE_REPLACED)
addDataScheme("package") addDataScheme("package")
}, },
) )

View File

@@ -25,13 +25,8 @@ class AppChangeReceiver : BroadcastReceiver() {
Log.d(TAG, "per app proxy disabled") Log.d(TAG, "per app proxy disabled")
return return
} }
if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { if (!Settings.perAppProxyManagedMode) {
Log.d(TAG, "skip app update") Log.d(TAG, "managed mode disabled")
return
}
val perAppProxyUpdateOnChange = Settings.perAppProxyUpdateOnChange
if (perAppProxyUpdateOnChange == Settings.PER_APP_PROXY_DISABLED) {
Log.d(TAG, "update on change disabled")
return return
} }
val packageName = intent.dataString?.substringAfter("package:") val packageName = intent.dataString?.substringAfter("package:")
@@ -41,12 +36,12 @@ class AppChangeReceiver : BroadcastReceiver() {
} }
val isChinaApp = PerAppProxyActivity.scanChinaPackage(packageName) val isChinaApp = PerAppProxyActivity.scanChinaPackage(packageName)
Log.d(TAG, "scan china app result for $packageName: $isChinaApp") Log.d(TAG, "scan china app result for $packageName: $isChinaApp")
if ((perAppProxyUpdateOnChange == Settings.PER_APP_PROXY_INCLUDE) xor !isChinaApp) { if (isChinaApp) {
Settings.perAppProxyList += packageName Settings.perAppProxyManagedList += packageName
Log.d(TAG, "added to list") Log.d(TAG, "added to managed list")
} else { } else {
Settings.perAppProxyList -= packageName Settings.perAppProxyManagedList -= packageName
Log.d(TAG, "removed from list") Log.d(TAG, "removed from managed list")
} }
} }
} }

View File

@@ -145,7 +145,7 @@ class BoxService(
OverrideOptions().apply { OverrideOptions().apply {
autoRedirect = Settings.autoRedirect autoRedirect = Settings.autoRedirect
if (Vendor.isPerAppProxyAvailable() && Settings.perAppProxyEnabled) { if (Vendor.isPerAppProxyAvailable() && Settings.perAppProxyEnabled) {
val appList = Settings.perAppProxyList val appList = Settings.getEffectivePerAppProxyList()
if (Settings.perAppProxyMode == Settings.PER_APP_PROXY_INCLUDE) { if (Settings.perAppProxyMode == Settings.PER_APP_PROXY_INCLUDE) {
includePackage = includePackage =
PlatformInterfaceWrapper.StringArray(appList.iterator()) PlatformInterfaceWrapper.StringArray(appList.iterator())
@@ -228,7 +228,7 @@ class BoxService(
OverrideOptions().apply { OverrideOptions().apply {
autoRedirect = Settings.autoRedirect autoRedirect = Settings.autoRedirect
if (Vendor.isPerAppProxyAvailable() && Settings.perAppProxyEnabled) { if (Vendor.isPerAppProxyAvailable() && Settings.perAppProxyEnabled) {
val appList = Settings.perAppProxyList val appList = Settings.getEffectivePerAppProxyList()
if (Settings.perAppProxyMode == Settings.PER_APP_PROXY_INCLUDE) { if (Settings.perAppProxyMode == Settings.PER_APP_PROXY_INCLUDE) {
includePackage = PlatformInterfaceWrapper.StringArray(appList.iterator()) includePackage = PlatformInterfaceWrapper.StringArray(appList.iterator())
} else { } else {

View File

@@ -1,22 +1,31 @@
package io.nekohasekai.sfa.compose.screen.settings package io.nekohasekai.sfa.compose.screen.settings
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.widget.Toast import android.widget.Toast
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.KeyboardArrowRight
import androidx.compose.material.icons.outlined.AppShortcut
import androidx.compose.material.icons.outlined.FilterList import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.Route import androidx.compose.material.icons.outlined.Route
import androidx.compose.material.icons.outlined.SmartToy
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.ListItemDefaults
@@ -37,11 +46,14 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
import io.nekohasekai.sfa.Application
import io.nekohasekai.sfa.R import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.database.Settings import io.nekohasekai.sfa.database.Settings
import io.nekohasekai.sfa.ui.profileoverride.PerAppProxyActivity import io.nekohasekai.sfa.ui.profileoverride.PerAppProxyActivity
import io.nekohasekai.sfa.vendor.Vendor import io.nekohasekai.sfa.vendor.Vendor
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -52,6 +64,8 @@ fun ProfileOverrideScreen(navController: NavController) {
var autoRedirect by remember { mutableStateOf(Settings.autoRedirect) } var autoRedirect by remember { mutableStateOf(Settings.autoRedirect) }
var perAppProxyEnabled by remember { mutableStateOf(Settings.perAppProxyEnabled) } var perAppProxyEnabled by remember { mutableStateOf(Settings.perAppProxyEnabled) }
var managedModeEnabled by remember { mutableStateOf(Settings.perAppProxyManagedMode) }
var isScanning by remember { mutableStateOf(false) }
var showPerAppProxyDialog by remember { mutableStateOf(false) } var showPerAppProxyDialog by remember { mutableStateOf(false) }
Column( Column(
@@ -62,6 +76,7 @@ fun ProfileOverrideScreen(navController: NavController) {
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(vertical = 8.dp), .padding(vertical = 8.dp),
) { ) {
// Card 1: Auto Redirect
Card( Card(
modifier = modifier =
Modifier Modifier
@@ -72,84 +87,102 @@ fun ProfileOverrideScreen(navController: NavController) {
containerColor = MaterialTheme.colorScheme.surfaceContainer, containerColor = MaterialTheme.colorScheme.surfaceContainer,
), ),
) { ) {
Column { ListItem(
// Auto Redirect headlineContent = {
ListItem( Text(
headlineContent = { stringResource(R.string.auto_redirect),
Text( style = MaterialTheme.typography.bodyLarge,
stringResource(R.string.auto_redirect), )
style = MaterialTheme.typography.bodyLarge, },
) supportingContent = {
}, Text(
supportingContent = { stringResource(R.string.auto_redirect_description),
Text( style = MaterialTheme.typography.bodyMedium,
stringResource(R.string.auto_redirect_description), color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(top = 4.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant, )
modifier = Modifier.padding(top = 4.dp), },
) leadingContent = {
}, Icon(
leadingContent = { imageVector = Icons.Outlined.Route,
Icon( contentDescription = null,
imageVector = Icons.Outlined.Route, tint = MaterialTheme.colorScheme.primary,
contentDescription = null, )
tint = MaterialTheme.colorScheme.primary, },
) trailingContent = {
}, Switch(
trailingContent = { checked = autoRedirect,
Switch( onCheckedChange = { checked ->
checked = autoRedirect, if (checked && !autoRedirect) {
onCheckedChange = { checked -> scope.launch {
if (checked && !autoRedirect) { val hasRoot =
scope.launch { withContext(Dispatchers.IO) {
val hasRoot = try {
withContext(Dispatchers.IO) { val process = Runtime.getRuntime().exec("su -c id")
try { process.inputStream.close()
val process = Runtime.getRuntime().exec("su -c id") process.outputStream.close()
process.inputStream.close() process.errorStream.close()
process.outputStream.close() process.waitFor() == 0
process.errorStream.close() } catch (e: Exception) {
process.waitFor() == 0 false
} catch (e: Exception) {
false
}
} }
if (hasRoot) {
autoRedirect = true
withContext(Dispatchers.IO) {
Settings.autoRedirect = true
}
} else {
Toast.makeText(
context,
context.getString(R.string.root_access_required),
Toast.LENGTH_LONG,
).show()
} }
} if (hasRoot) {
} else if (!checked) { autoRedirect = true
// Disabling doesn't need root check withContext(Dispatchers.IO) {
autoRedirect = false Settings.autoRedirect = true
scope.launch(Dispatchers.IO) { }
Settings.autoRedirect = false } else {
Toast.makeText(
context,
context.getString(R.string.root_access_required),
Toast.LENGTH_LONG,
).show()
} }
} }
}, } else if (!checked) {
) autoRedirect = false
}, scope.launch(Dispatchers.IO) {
modifier = Modifier.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)), Settings.autoRedirect = false
colors = }
ListItemDefaults.colors( }
containerColor = Color.Transparent, },
), )
) },
modifier = Modifier.clip(RoundedCornerShape(12.dp)),
colors =
ListItemDefaults.colors(
containerColor = Color.Transparent,
),
)
}
// Per-App Proxy // Section: Per-App Proxy
val isPerAppProxyAvailable = Vendor.isPerAppProxyAvailable() val isPerAppProxyAvailable = Vendor.isPerAppProxyAvailable()
Text(
text = stringResource(R.string.per_app_proxy),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = 32.dp, top = 16.dp, bottom = 8.dp),
)
Card(
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
colors =
CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
),
) {
Column {
// Enabled toggle
ListItem( ListItem(
headlineContent = { headlineContent = {
Text( Text(
stringResource(R.string.per_app_proxy), stringResource(R.string.enabled),
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
) )
}, },
@@ -187,23 +220,132 @@ fun ProfileOverrideScreen(navController: NavController) {
} }
}, },
modifier = modifier =
Modifier Modifier.clip(
.clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)) if (perAppProxyEnabled && isPerAppProxyAvailable) {
.clickable { RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)
if (isPerAppProxyAvailable) { } else {
// Launch the PerAppProxyActivity RoundedCornerShape(12.dp)
val intent = Intent(context, PerAppProxyActivity::class.java)
context.startActivity(intent)
} else {
// Show dialog explaining why it's disabled
showPerAppProxyDialog = true
}
}, },
),
colors = colors =
ListItemDefaults.colors( ListItemDefaults.colors(
containerColor = Color.Transparent, containerColor = Color.Transparent,
), ),
) )
if (perAppProxyEnabled && isPerAppProxyAvailable) {
// Manage entry
val manageEnabled = !managedModeEnabled
val disabledAlpha = 0.38f
ListItem(
headlineContent = {
Text(
stringResource(R.string.per_app_proxy_manage),
style = MaterialTheme.typography.bodyLarge,
color = if (manageEnabled) {
Color.Unspecified
} else {
MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha)
},
)
},
leadingContent = {
Icon(
imageVector = Icons.Outlined.AppShortcut,
contentDescription = null,
tint = if (manageEnabled) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha)
},
)
},
trailingContent = {
Icon(
imageVector = Icons.AutoMirrored.Outlined.KeyboardArrowRight,
contentDescription = null,
tint = if (manageEnabled) {
MaterialTheme.colorScheme.onSurfaceVariant
} else {
MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha)
},
)
},
modifier =
Modifier.clickable(enabled = manageEnabled) {
val intent = Intent(context, PerAppProxyActivity::class.java)
context.startActivity(intent)
},
colors =
ListItemDefaults.colors(
containerColor = Color.Transparent,
),
)
// Managed Mode toggle
ListItem(
headlineContent = {
Text(
stringResource(R.string.per_app_proxy_managed_mode),
style = MaterialTheme.typography.bodyLarge,
)
},
supportingContent = {
Text(
stringResource(R.string.per_app_proxy_managed_mode_description),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 4.dp),
)
},
leadingContent = {
Icon(
imageVector = Icons.Outlined.SmartToy,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
)
},
trailingContent = {
if (isScanning) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
strokeWidth = 2.dp,
)
} else {
Switch(
checked = managedModeEnabled,
onCheckedChange = { checked ->
if (checked) {
managedModeEnabled = true
isScanning = true
scope.launch {
withContext(Dispatchers.IO) {
Settings.perAppProxyManagedMode = true
Settings.perAppProxyMode = Settings.PER_APP_PROXY_EXCLUDE
}
val chinaApps = scanAllChinaApps()
withContext(Dispatchers.IO) {
Settings.perAppProxyManagedList = chinaApps
}
isScanning = false
}
} else {
managedModeEnabled = false
scope.launch(Dispatchers.IO) {
Settings.perAppProxyManagedMode = false
}
}
},
)
}
},
modifier = Modifier.clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)),
colors =
ListItemDefaults.colors(
containerColor = Color.Transparent,
),
)
}
} }
} }
@@ -228,3 +370,34 @@ fun ProfileOverrideScreen(navController: NavController) {
} }
} }
} }
private suspend fun scanAllChinaApps(): Set<String> = withContext(Dispatchers.Default) {
val packageManagerFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
PackageManager.MATCH_UNINSTALLED_PACKAGES
} else {
@Suppress("DEPRECATION")
PackageManager.GET_UNINSTALLED_PACKAGES
}
val installedPackages = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Application.packageManager.getInstalledPackages(
PackageManager.PackageInfoFlags.of(packageManagerFlags.toLong())
)
} else {
@Suppress("DEPRECATION")
Application.packageManager.getInstalledPackages(packageManagerFlags)
}
val chinaApps = mutableSetOf<String>()
installedPackages.map { packageInfo ->
async {
if (PerAppProxyActivity.scanChinaPackage(packageInfo.packageName)) {
synchronized(chinaApps) {
chinaApps.add(packageInfo.packageName)
}
}
}
}.awaitAll()
chinaApps.toSet()
}

View File

@@ -1,49 +0,0 @@
package io.nekohasekai.sfa.constant
import android.content.Context
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.database.Settings
enum class PerAppProxyUpdateType {
Disabled,
Select,
Deselect,
;
fun value() =
when (this) {
Disabled -> Settings.PER_APP_PROXY_DISABLED
Select -> Settings.PER_APP_PROXY_INCLUDE
Deselect -> Settings.PER_APP_PROXY_EXCLUDE
}
fun getString(context: Context): String {
return when (this) {
Disabled -> context.getString(R.string.disabled)
Select -> context.getString(R.string.per_app_proxy_select)
Deselect -> context.getString(R.string.action_deselect)
}
}
companion object {
fun valueOf(value: Int): PerAppProxyUpdateType =
when (value) {
Settings.PER_APP_PROXY_DISABLED -> Disabled
Settings.PER_APP_PROXY_INCLUDE -> Select
Settings.PER_APP_PROXY_EXCLUDE -> Deselect
else -> throw IllegalArgumentException()
}
fun valueOf(
context: Context,
value: String,
): PerAppProxyUpdateType {
return when (value) {
context.getString(R.string.disabled) -> Disabled
context.getString(R.string.per_app_proxy_select) -> Select
context.getString(R.string.action_deselect) -> Deselect
else -> Disabled
}
}
}
}

View File

@@ -4,7 +4,11 @@ object SettingsKey {
const val SELECTED_PROFILE = "selected_profile" const val SELECTED_PROFILE = "selected_profile"
const val SERVICE_MODE = "service_mode" const val SERVICE_MODE = "service_mode"
const val CHECK_UPDATE_ENABLED = "check_update_enabled" const val CHECK_UPDATE_ENABLED = "check_update_enabled"
const val UPDATE_CHECK_PROMPTED = "update_check_prompted"
const val UPDATE_TRACK = "update_track" const val UPDATE_TRACK = "update_track"
const val SILENT_INSTALL_ENABLED = "silent_install_enabled"
const val SILENT_INSTALL_METHOD = "silent_install_method"
const val AUTO_UPDATE_ENABLED = "auto_update_enabled"
const val DISABLE_MEMORY_LIMIT = "disable_memory_limit" const val DISABLE_MEMORY_LIMIT = "disable_memory_limit"
const val DYNAMIC_NOTIFICATION = "dynamic_notification" const val DYNAMIC_NOTIFICATION = "dynamic_notification"
const val USE_COMPOSE_UI = "use_compose_ui" const val USE_COMPOSE_UI = "use_compose_ui"
@@ -14,7 +18,8 @@ object SettingsKey {
const val PER_APP_PROXY_ENABLED = "per_app_proxy_enabled" 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_MODE = "per_app_proxy_mode"
const val PER_APP_PROXY_LIST = "per_app_proxy_list" 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_MANAGED_MODE = "per_app_proxy_managed_mode"
const val PER_APP_PROXY_MANAGED_LIST = "per_app_proxy_managed_list"
const val SYSTEM_PROXY_ENABLED = "system_proxy_enabled" const val SYSTEM_PROXY_ENABLED = "system_proxy_enabled"
@@ -23,6 +28,8 @@ object SettingsKey {
const val DASHBOARD_DISABLED_ITEMS = "dashboard_disabled_items" const val DASHBOARD_DISABLED_ITEMS = "dashboard_disabled_items"
// cache // cache
const val STARTED_BY_USER = "started_by_user" const val STARTED_BY_USER = "started_by_user"
const val CACHED_UPDATE_INFO = "cached_update_info"
const val CACHED_APK_PATH = "cached_apk_path"
const val LAST_SHOWN_UPDATE_VERSION = "last_shown_update_version"
} }

View File

@@ -40,7 +40,8 @@ object Settings {
var serviceMode by dataStore.string(SettingsKey.SERVICE_MODE) { ServiceMode.NORMAL } var serviceMode by dataStore.string(SettingsKey.SERVICE_MODE) { ServiceMode.NORMAL }
var startedByUser by dataStore.boolean(SettingsKey.STARTED_BY_USER) var startedByUser by dataStore.boolean(SettingsKey.STARTED_BY_USER)
var checkUpdateEnabled by dataStore.boolean(SettingsKey.CHECK_UPDATE_ENABLED) { true } var checkUpdateEnabled by dataStore.boolean(SettingsKey.CHECK_UPDATE_ENABLED) { false }
var updateCheckPrompted by dataStore.boolean(SettingsKey.UPDATE_CHECK_PROMPTED) { false }
var updateTrack by dataStore.string(SettingsKey.UPDATE_TRACK) { var updateTrack by dataStore.string(SettingsKey.UPDATE_TRACK) {
val versionName = BuildConfig.VERSION_NAME.lowercase() val versionName = BuildConfig.VERSION_NAME.lowercase()
if (versionName.contains("-alpha") || if (versionName.contains("-alpha") ||
@@ -52,6 +53,9 @@ object Settings {
"stable" "stable"
} }
} }
var silentInstallEnabled by dataStore.boolean(SettingsKey.SILENT_INSTALL_ENABLED) { false }
var silentInstallMethod by dataStore.string(SettingsKey.SILENT_INSTALL_METHOD) { "PACKAGE_INSTALLER" }
var autoUpdateEnabled by dataStore.boolean(SettingsKey.AUTO_UPDATE_ENABLED) { false }
var disableMemoryLimit by dataStore.boolean(SettingsKey.DISABLE_MEMORY_LIMIT) var disableMemoryLimit by dataStore.boolean(SettingsKey.DISABLE_MEMORY_LIMIT)
var dynamicNotification by dataStore.boolean(SettingsKey.DYNAMIC_NOTIFICATION) { true } var dynamicNotification by dataStore.boolean(SettingsKey.DYNAMIC_NOTIFICATION) { true }
var useComposeUI by dataStore.boolean(SettingsKey.USE_COMPOSE_UI) { true } var useComposeUI by dataStore.boolean(SettingsKey.USE_COMPOSE_UI) { true }
@@ -65,13 +69,26 @@ object Settings {
var perAppProxyEnabled by dataStore.boolean(SettingsKey.PER_APP_PROXY_ENABLED) { false } var perAppProxyEnabled by dataStore.boolean(SettingsKey.PER_APP_PROXY_ENABLED) { false }
var perAppProxyMode by dataStore.int(SettingsKey.PER_APP_PROXY_MODE) { PER_APP_PROXY_EXCLUDE } var perAppProxyMode by dataStore.int(SettingsKey.PER_APP_PROXY_MODE) { PER_APP_PROXY_EXCLUDE }
var perAppProxyList by dataStore.stringSet(SettingsKey.PER_APP_PROXY_LIST) { emptySet() } var perAppProxyList by dataStore.stringSet(SettingsKey.PER_APP_PROXY_LIST) { emptySet() }
var perAppProxyUpdateOnChange by dataStore.int(SettingsKey.PER_APP_PROXY_UPDATE_ON_CHANGE) { PER_APP_PROXY_DISABLED } var perAppProxyManagedMode by dataStore.boolean(SettingsKey.PER_APP_PROXY_MANAGED_MODE) { false }
var perAppProxyManagedList by dataStore.stringSet(SettingsKey.PER_APP_PROXY_MANAGED_LIST) { emptySet() }
fun getEffectivePerAppProxyList(): Set<String> {
return if (perAppProxyManagedMode) {
perAppProxyList union perAppProxyManagedList
} else {
perAppProxyList
}
}
var systemProxyEnabled by dataStore.boolean(SettingsKey.SYSTEM_PROXY_ENABLED) { true } var systemProxyEnabled by dataStore.boolean(SettingsKey.SYSTEM_PROXY_ENABLED) { true }
var dashboardItemOrder by dataStore.string(SettingsKey.DASHBOARD_ITEM_ORDER) { "" } var dashboardItemOrder by dataStore.string(SettingsKey.DASHBOARD_ITEM_ORDER) { "" }
var dashboardDisabledItems by dataStore.stringSet(SettingsKey.DASHBOARD_DISABLED_ITEMS) { emptySet() } var dashboardDisabledItems by dataStore.stringSet(SettingsKey.DASHBOARD_DISABLED_ITEMS) { emptySet() }
var cachedUpdateInfo by dataStore.string(SettingsKey.CACHED_UPDATE_INFO) { "" }
var cachedApkPath by dataStore.string(SettingsKey.CACHED_APK_PATH) { "" }
var lastShownUpdateVersion by dataStore.int(SettingsKey.LAST_SHOWN_UPDATE_VERSION) { 0 }
fun serviceClass(): Class<*> { fun serviceClass(): Class<*> {
return when (serviceMode) { return when (serviceMode) {
ServiceMode.VPN -> VPNService::class.java ServiceMode.VPN -> VPNService::class.java

View File

@@ -2,18 +2,10 @@ package io.nekohasekai.sfa.ui.profileoverride
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import io.nekohasekai.sfa.R import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.constant.PerAppProxyUpdateType
import io.nekohasekai.sfa.database.Settings import io.nekohasekai.sfa.database.Settings
import io.nekohasekai.sfa.databinding.ActivityConfigOverrideBinding import io.nekohasekai.sfa.databinding.ActivityConfigOverrideBinding
import io.nekohasekai.sfa.ktx.addTextChangedListener
import io.nekohasekai.sfa.ktx.setSimpleItems
import io.nekohasekai.sfa.ktx.text
import io.nekohasekai.sfa.ui.shared.AbstractActivity import io.nekohasekai.sfa.ui.shared.AbstractActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ProfileOverrideActivity : class ProfileOverrideActivity :
AbstractActivity<ActivityConfigOverrideBinding>() { AbstractActivity<ActivityConfigOverrideBinding>() {
@@ -24,34 +16,12 @@ class ProfileOverrideActivity :
binding.switchPerAppProxy.isChecked = Settings.perAppProxyEnabled binding.switchPerAppProxy.isChecked = Settings.perAppProxyEnabled
binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked -> binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked ->
Settings.perAppProxyEnabled = isChecked Settings.perAppProxyEnabled = isChecked
binding.perAppProxyUpdateOnChange.isEnabled = binding.switchPerAppProxy.isChecked
binding.configureAppListButton.isEnabled = isChecked binding.configureAppListButton.isEnabled = isChecked
} }
binding.perAppProxyUpdateOnChange.isEnabled = binding.switchPerAppProxy.isChecked
binding.configureAppListButton.isEnabled = binding.switchPerAppProxy.isChecked binding.configureAppListButton.isEnabled = binding.switchPerAppProxy.isChecked
binding.perAppProxyUpdateOnChange.addTextChangedListener {
lifecycleScope.launch(Dispatchers.IO) {
Settings.perAppProxyUpdateOnChange =
PerAppProxyUpdateType.valueOf(this@ProfileOverrideActivity, it).value()
}
}
binding.configureAppListButton.setOnClickListener { binding.configureAppListButton.setOnClickListener {
startActivity(Intent(this, PerAppProxyActivity::class.java)) startActivity(Intent(this, PerAppProxyActivity::class.java))
} }
lifecycleScope.launch(Dispatchers.IO) {
reloadSettings()
}
}
private suspend fun reloadSettings() {
val perAppUpdateOnChange = Settings.perAppProxyUpdateOnChange
withContext(Dispatchers.Main) {
binding.perAppProxyUpdateOnChange.text =
PerAppProxyUpdateType.valueOf(perAppUpdateOnChange)
.getString(this@ProfileOverrideActivity)
binding.perAppProxyUpdateOnChange.setSimpleItems(R.array.per_app_proxy_update_on_change_value)
}
} }
} }

View File

@@ -54,24 +54,6 @@
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:text="@string/per_app_proxy_description" /> android:text="@string/per_app_proxy_description" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/perAppProxyUpdateOnChange"
style="@style/Widget.Material3.TextInputLayout.FilledBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/per_app_proxy_update_on_change">
<AutoCompleteTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:text="@string/disabled"
app:simpleItems="@array/per_app_proxy_update_on_change_value" />
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@@ -144,7 +144,9 @@
<string name="message_scan_app_found">找到以下应用程序,请选择您想要的操作。</string> <string name="message_scan_app_found">找到以下应用程序,请选择您想要的操作。</string>
<string name="title_scan_result">扫描结果</string> <string name="title_scan_result">扫描结果</string>
<string name="action_deselect">取消选择</string> <string name="action_deselect">取消选择</string>
<string name="per_app_proxy_update_on_change">新中国应用安装时更新</string> <string name="per_app_proxy_manage">管理</string>
<string name="per_app_proxy_managed_mode">托管模式</string>
<string name="per_app_proxy_managed_mode_description">自动排除中国应用</string>
<string name="import_profile">导入配置</string> <string name="import_profile">导入配置</string>
<string name="import_profile_message">您确定要导入配置文件 %s 吗?</string> <string name="import_profile_message">您确定要导入配置文件 %s 吗?</string>
<string name="icloud_profile_unsupported">当前平台不支持 iCloud 配置文件</string> <string name="icloud_profile_unsupported">当前平台不支持 iCloud 配置文件</string>

View File

@@ -12,9 +12,4 @@
<item>@string/enabled</item> <item>@string/enabled</item>
<item>@string/disabled</item> <item>@string/disabled</item>
</array> </array>
<array name="per_app_proxy_update_on_change_value">
<item>@string/disabled</item>
<item>@string/per_app_proxy_select</item>
<item>@string/action_deselect</item>
</array>
</resources> </resources>

View File

@@ -126,6 +126,8 @@
<string name="per_app_proxy">Per-App Proxy</string> <string name="per_app_proxy">Per-App Proxy</string>
<string name="unavailable">Unavailable</string> <string name="unavailable">Unavailable</string>
<string name="check_update_automatic">Automatic Update Check</string> <string name="check_update_automatic">Automatic Update Check</string>
<string name="check_update_prompt_play">Would you like to enable automatic update checking from **Play Store**?</string>
<string name="check_update_prompt_github">Would you like to enable automatic update checking from **GitHub**?</string>
<string name="check_update">Check Update</string> <string name="check_update">Check Update</string>
<string name="no_updates_available">No updates available</string> <string name="no_updates_available">No updates available</string>
<string name="new_version_available">New version available: %s</string> <string name="new_version_available">New version available: %s</string>
@@ -134,6 +136,28 @@
<string name="update_track_stable">Stable</string> <string name="update_track_stable">Stable</string>
<string name="update_track_beta">Beta</string> <string name="update_track_beta">Beta</string>
<string name="update_track_not_supported">Current track does not support update checking yet</string> <string name="update_track_not_supported">Current track does not support update checking yet</string>
<string name="download_and_install">Download &amp; Install</string>
<string name="view_release">View Release</string>
<string name="downloading">Downloading…</string>
<string name="download_size">Download size: %s</string>
<string name="silent_install">Silent Install</string>
<string name="silent_install_title">Silent Install</string>
<string name="silent_install_description">Install updates without interaction</string>
<string name="silent_install_method">Install Method</string>
<string name="silent_install_method_description">Select an install method. The permission will be verified immediately after selection.</string>
<string name="install_method_package_installer">PackageInstaller</string>
<string name="install_method_shizuku">Shizuku</string>
<string name="install_method_root">ROOT</string>
<string name="package_installer_not_available">Install permission not granted</string>
<string name="grant_install_permission">Grant Install Permission</string>
<string name="grant_install_permission_description">Allow installing apps from this source</string>
<string name="shizuku_not_available">Shizuku is not installed or not running</string>
<string name="shizuku_description">Shizuku allows apps to use system APIs directly with higher privileges</string>
<string name="get_shizuku">Get Shizuku</string>
<string name="silent_install_not_available">ROOT and Shizuku are not available</string>
<string name="silent_install_verify_failed">%s is not available or permission denied</string>
<string name="auto_update">Auto Update</string>
<string name="auto_update_description">Automatically download and install updates in background</string>
<string name="app_version">Version %s</string> <string name="app_version">Version %s</string>
<string name="app_version_title">App version</string> <string name="app_version_title">App version</string>
<string name="action">Action</string> <string name="action">Action</string>
@@ -186,7 +210,9 @@
<string name="message_scan_app_found">Found the following apps, please choose the action you want.</string> <string name="message_scan_app_found">Found the following apps, please choose the action you want.</string>
<string name="title_scan_result">Scan Result</string> <string name="title_scan_result">Scan Result</string>
<string name="action_deselect">Deselect</string> <string name="action_deselect">Deselect</string>
<string name="per_app_proxy_update_on_change">Update on new China App Installed</string> <string name="per_app_proxy_manage">Manage</string>
<string name="per_app_proxy_managed_mode">Managed Mode</string>
<string name="per_app_proxy_managed_mode_description">Automatically Exclude China apps</string>
<string name="import_profile">Import profile</string> <string name="import_profile">Import profile</string>
<string name="import_profile_message">Are you sure to import profile %s?</string> <string name="import_profile_message">Are you sure to import profile %s?</string>
<string name="icloud_profile_unsupported">iCloud profile is not support on current platform</string> <string name="icloud_profile_unsupported">iCloud profile is not support on current platform</string>