Improve per-app proxy
This commit is contained in:
@@ -49,6 +49,7 @@ class Application : Application() {
|
||||
AppChangeReceiver(),
|
||||
IntentFilter().apply {
|
||||
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||
addAction(Intent.ACTION_PACKAGE_REPLACED)
|
||||
addDataScheme("package")
|
||||
},
|
||||
)
|
||||
|
||||
@@ -25,13 +25,8 @@ class AppChangeReceiver : BroadcastReceiver() {
|
||||
Log.d(TAG, "per app proxy disabled")
|
||||
return
|
||||
}
|
||||
if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
|
||||
Log.d(TAG, "skip app update")
|
||||
return
|
||||
}
|
||||
val perAppProxyUpdateOnChange = Settings.perAppProxyUpdateOnChange
|
||||
if (perAppProxyUpdateOnChange == Settings.PER_APP_PROXY_DISABLED) {
|
||||
Log.d(TAG, "update on change disabled")
|
||||
if (!Settings.perAppProxyManagedMode) {
|
||||
Log.d(TAG, "managed mode disabled")
|
||||
return
|
||||
}
|
||||
val packageName = intent.dataString?.substringAfter("package:")
|
||||
@@ -41,12 +36,12 @@ class AppChangeReceiver : BroadcastReceiver() {
|
||||
}
|
||||
val isChinaApp = PerAppProxyActivity.scanChinaPackage(packageName)
|
||||
Log.d(TAG, "scan china app result for $packageName: $isChinaApp")
|
||||
if ((perAppProxyUpdateOnChange == Settings.PER_APP_PROXY_INCLUDE) xor !isChinaApp) {
|
||||
Settings.perAppProxyList += packageName
|
||||
Log.d(TAG, "added to list")
|
||||
if (isChinaApp) {
|
||||
Settings.perAppProxyManagedList += packageName
|
||||
Log.d(TAG, "added to managed list")
|
||||
} else {
|
||||
Settings.perAppProxyList -= packageName
|
||||
Log.d(TAG, "removed from list")
|
||||
Settings.perAppProxyManagedList -= packageName
|
||||
Log.d(TAG, "removed from managed list")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ class BoxService(
|
||||
OverrideOptions().apply {
|
||||
autoRedirect = Settings.autoRedirect
|
||||
if (Vendor.isPerAppProxyAvailable() && Settings.perAppProxyEnabled) {
|
||||
val appList = Settings.perAppProxyList
|
||||
val appList = Settings.getEffectivePerAppProxyList()
|
||||
if (Settings.perAppProxyMode == Settings.PER_APP_PROXY_INCLUDE) {
|
||||
includePackage =
|
||||
PlatformInterfaceWrapper.StringArray(appList.iterator())
|
||||
@@ -228,7 +228,7 @@ class BoxService(
|
||||
OverrideOptions().apply {
|
||||
autoRedirect = Settings.autoRedirect
|
||||
if (Vendor.isPerAppProxyAvailable() && Settings.perAppProxyEnabled) {
|
||||
val appList = Settings.perAppProxyList
|
||||
val appList = Settings.getEffectivePerAppProxyList()
|
||||
if (Settings.perAppProxyMode == Settings.PER_APP_PROXY_INCLUDE) {
|
||||
includePackage = PlatformInterfaceWrapper.StringArray(appList.iterator())
|
||||
} else {
|
||||
|
||||
@@ -1,22 +1,31 @@
|
||||
package io.nekohasekai.sfa.compose.screen.settings
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
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.Route
|
||||
import androidx.compose.material.icons.outlined.SmartToy
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
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.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import io.nekohasekai.sfa.Application
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.database.Settings
|
||||
import io.nekohasekai.sfa.ui.profileoverride.PerAppProxyActivity
|
||||
import io.nekohasekai.sfa.vendor.Vendor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@@ -52,6 +64,8 @@ fun ProfileOverrideScreen(navController: NavController) {
|
||||
|
||||
var autoRedirect by remember { mutableStateOf(Settings.autoRedirect) }
|
||||
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) }
|
||||
|
||||
Column(
|
||||
@@ -62,6 +76,7 @@ fun ProfileOverrideScreen(navController: NavController) {
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(vertical = 8.dp),
|
||||
) {
|
||||
// Card 1: Auto Redirect
|
||||
Card(
|
||||
modifier =
|
||||
Modifier
|
||||
@@ -72,84 +87,102 @@ fun ProfileOverrideScreen(navController: NavController) {
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
),
|
||||
) {
|
||||
Column {
|
||||
// Auto Redirect
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
stringResource(R.string.auto_redirect),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
stringResource(R.string.auto_redirect_description),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
)
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Route,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = autoRedirect,
|
||||
onCheckedChange = { checked ->
|
||||
if (checked && !autoRedirect) {
|
||||
scope.launch {
|
||||
val hasRoot =
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val process = Runtime.getRuntime().exec("su -c id")
|
||||
process.inputStream.close()
|
||||
process.outputStream.close()
|
||||
process.errorStream.close()
|
||||
process.waitFor() == 0
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
stringResource(R.string.auto_redirect),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
stringResource(R.string.auto_redirect_description),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
)
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Route,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = autoRedirect,
|
||||
onCheckedChange = { checked ->
|
||||
if (checked && !autoRedirect) {
|
||||
scope.launch {
|
||||
val hasRoot =
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val process = Runtime.getRuntime().exec("su -c id")
|
||||
process.inputStream.close()
|
||||
process.outputStream.close()
|
||||
process.errorStream.close()
|
||||
process.waitFor() == 0
|
||||
} 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()
|
||||
}
|
||||
}
|
||||
} else if (!checked) {
|
||||
// Disabling doesn't need root check
|
||||
autoRedirect = false
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Settings.autoRedirect = 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()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)),
|
||||
colors =
|
||||
ListItemDefaults.colors(
|
||||
containerColor = Color.Transparent,
|
||||
),
|
||||
)
|
||||
} else if (!checked) {
|
||||
autoRedirect = false
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Settings.autoRedirect = false
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clip(RoundedCornerShape(12.dp)),
|
||||
colors =
|
||||
ListItemDefaults.colors(
|
||||
containerColor = Color.Transparent,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Per-App Proxy
|
||||
val isPerAppProxyAvailable = Vendor.isPerAppProxyAvailable()
|
||||
// Section: Per-App Proxy
|
||||
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(
|
||||
headlineContent = {
|
||||
Text(
|
||||
stringResource(R.string.per_app_proxy),
|
||||
stringResource(R.string.enabled),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
},
|
||||
@@ -187,23 +220,132 @@ fun ProfileOverrideScreen(navController: NavController) {
|
||||
}
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp))
|
||||
.clickable {
|
||||
if (isPerAppProxyAvailable) {
|
||||
// Launch the PerAppProxyActivity
|
||||
val intent = Intent(context, PerAppProxyActivity::class.java)
|
||||
context.startActivity(intent)
|
||||
} else {
|
||||
// Show dialog explaining why it's disabled
|
||||
showPerAppProxyDialog = true
|
||||
}
|
||||
Modifier.clip(
|
||||
if (perAppProxyEnabled && isPerAppProxyAvailable) {
|
||||
RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)
|
||||
} else {
|
||||
RoundedCornerShape(12.dp)
|
||||
},
|
||||
),
|
||||
colors =
|
||||
ListItemDefaults.colors(
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,11 @@ object SettingsKey {
|
||||
const val SELECTED_PROFILE = "selected_profile"
|
||||
const val SERVICE_MODE = "service_mode"
|
||||
const val CHECK_UPDATE_ENABLED = "check_update_enabled"
|
||||
const val UPDATE_CHECK_PROMPTED = "update_check_prompted"
|
||||
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 DYNAMIC_NOTIFICATION = "dynamic_notification"
|
||||
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_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_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"
|
||||
|
||||
@@ -23,6 +28,8 @@ object SettingsKey {
|
||||
const val DASHBOARD_DISABLED_ITEMS = "dashboard_disabled_items"
|
||||
|
||||
// cache
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
@@ -40,7 +40,8 @@ object Settings {
|
||||
var serviceMode by dataStore.string(SettingsKey.SERVICE_MODE) { ServiceMode.NORMAL }
|
||||
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) {
|
||||
val versionName = BuildConfig.VERSION_NAME.lowercase()
|
||||
if (versionName.contains("-alpha") ||
|
||||
@@ -52,6 +53,9 @@ object Settings {
|
||||
"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 dynamicNotification by dataStore.boolean(SettingsKey.DYNAMIC_NOTIFICATION) { 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 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 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 dashboardItemOrder by dataStore.string(SettingsKey.DASHBOARD_ITEM_ORDER) { "" }
|
||||
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<*> {
|
||||
return when (serviceMode) {
|
||||
ServiceMode.VPN -> VPNService::class.java
|
||||
|
||||
@@ -2,18 +2,10 @@ package io.nekohasekai.sfa.ui.profileoverride
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.constant.PerAppProxyUpdateType
|
||||
import io.nekohasekai.sfa.database.Settings
|
||||
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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ProfileOverrideActivity :
|
||||
AbstractActivity<ActivityConfigOverrideBinding>() {
|
||||
@@ -24,34 +16,12 @@ class ProfileOverrideActivity :
|
||||
binding.switchPerAppProxy.isChecked = Settings.perAppProxyEnabled
|
||||
binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked ->
|
||||
Settings.perAppProxyEnabled = isChecked
|
||||
binding.perAppProxyUpdateOnChange.isEnabled = binding.switchPerAppProxy.isChecked
|
||||
binding.configureAppListButton.isEnabled = isChecked
|
||||
}
|
||||
binding.perAppProxyUpdateOnChange.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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,24 +54,6 @@
|
||||
android:layout_marginTop="8dp"
|
||||
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
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -144,7 +144,9 @@
|
||||
<string name="message_scan_app_found">找到以下应用程序,请选择您想要的操作。</string>
|
||||
<string name="title_scan_result">扫描结果</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_message">您确定要导入配置文件 %s 吗?</string>
|
||||
<string name="icloud_profile_unsupported">当前平台不支持 iCloud 配置文件</string>
|
||||
|
||||
@@ -12,9 +12,4 @@
|
||||
<item>@string/enabled</item>
|
||||
<item>@string/disabled</item>
|
||||
</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>
|
||||
@@ -126,6 +126,8 @@
|
||||
<string name="per_app_proxy">Per-App Proxy</string>
|
||||
<string name="unavailable">Unavailable</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="no_updates_available">No updates available</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_beta">Beta</string>
|
||||
<string name="update_track_not_supported">Current track does not support update checking yet</string>
|
||||
<string name="download_and_install">Download & 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_title">App version</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="title_scan_result">Scan Result</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_message">Are you sure to import profile %s?</string>
|
||||
<string name="icloud_profile_unsupported">iCloud profile is not support on current platform</string>
|
||||
|
||||
Reference in New Issue
Block a user