Improve per-app proxy
This commit is contained in:
@@ -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")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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 & 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>
|
||||||
|
|||||||
Reference in New Issue
Block a user