Add alternative support for QUERY_ALL_PACKAGES in play flavor
This commit is contained in:
@@ -79,9 +79,11 @@ android {
|
||||
sourceSets {
|
||||
play {
|
||||
java.srcDirs += ['src/minApi23/java']
|
||||
aidl.srcDirs += ['src/minApi23/aidl']
|
||||
}
|
||||
other {
|
||||
java.srcDirs += ['src/minApi23/java', 'src/github/java']
|
||||
aidl.srcDirs += ['src/minApi23/aidl']
|
||||
}
|
||||
otherLegacy {
|
||||
java.srcDirs += ['src/minApi21/java', 'src/github/java']
|
||||
@@ -202,6 +204,15 @@ dependencies {
|
||||
otherImplementation "dev.rikka.shizuku:provider:$shizukuVersion"
|
||||
otherImplementation 'org.lsposed.hiddenapibypass:hiddenapibypass:4.3'
|
||||
|
||||
// libsu for ROOT package query (all flavors)
|
||||
def libsuVersion = '6.0.0'
|
||||
playImplementation "com.github.topjohnwu.libsu:core:$libsuVersion"
|
||||
playImplementation "com.github.topjohnwu.libsu:service:$libsuVersion"
|
||||
otherImplementation "com.github.topjohnwu.libsu:core:$libsuVersion"
|
||||
otherImplementation "com.github.topjohnwu.libsu:service:$libsuVersion"
|
||||
otherLegacyImplementation "com.github.topjohnwu.libsu:core:$libsuVersion"
|
||||
otherLegacyImplementation "com.github.topjohnwu.libsu:service:$libsuVersion"
|
||||
|
||||
// Compose dependencies - API 23+ (play/other)
|
||||
def composeBom23 = platform('androidx.compose:compose-bom:2025.12.01')
|
||||
def activityVersion23 = "1.12.2"
|
||||
|
||||
@@ -42,8 +42,6 @@ class Application : Application() {
|
||||
UpdateProfileWork.reconfigureUpdater()
|
||||
}
|
||||
|
||||
// Only register AppChangeReceiver if Per-app Proxy is available
|
||||
// This receiver needs QUERY_ALL_PACKAGES permission to function
|
||||
if (Vendor.isPerAppProxyAvailable()) {
|
||||
registerReceiver(
|
||||
AppChangeReceiver(),
|
||||
|
||||
@@ -3,9 +3,18 @@ package io.nekohasekai.sfa.bg
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.database.Settings
|
||||
import io.nekohasekai.sfa.ui.profileoverride.PerAppProxyActivity
|
||||
import io.nekohasekai.sfa.vendor.PackageQueryManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class AppChangeReceiver : BroadcastReceiver() {
|
||||
companion object {
|
||||
@@ -17,10 +26,6 @@ class AppChangeReceiver : BroadcastReceiver() {
|
||||
intent: Intent,
|
||||
) {
|
||||
Log.d(TAG, "onReceive: ${intent.action}")
|
||||
checkUpdate(intent)
|
||||
}
|
||||
|
||||
private fun checkUpdate(intent: Intent) {
|
||||
if (!Settings.perAppProxyEnabled) {
|
||||
Log.d(TAG, "per app proxy disabled")
|
||||
return
|
||||
@@ -29,19 +34,41 @@ class AppChangeReceiver : BroadcastReceiver() {
|
||||
Log.d(TAG, "managed mode disabled")
|
||||
return
|
||||
}
|
||||
val packageName = intent.dataString?.substringAfter("package:")
|
||||
if (packageName.isNullOrBlank()) {
|
||||
Log.d(TAG, "missing package name in intent")
|
||||
return
|
||||
val pendingResult = goAsync()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
rescanAllApps()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to rescan apps", e)
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(context, R.string.error_title, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
val isChinaApp = PerAppProxyActivity.scanChinaPackage(packageName)
|
||||
Log.d(TAG, "scan china app result for $packageName: $isChinaApp")
|
||||
if (isChinaApp) {
|
||||
Settings.perAppProxyManagedList += packageName
|
||||
Log.d(TAG, "added to managed list")
|
||||
} finally {
|
||||
pendingResult.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun rescanAllApps() {
|
||||
Log.d(TAG, "rescanning all apps")
|
||||
val packageManagerFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
PackageManager.MATCH_UNINSTALLED_PACKAGES or
|
||||
PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or
|
||||
PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
|
||||
} else {
|
||||
Settings.perAppProxyManagedList -= packageName
|
||||
Log.d(TAG, "removed from managed list")
|
||||
@Suppress("DEPRECATION")
|
||||
PackageManager.GET_UNINSTALLED_PACKAGES or
|
||||
PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or
|
||||
PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
|
||||
}
|
||||
val installedPackages = PackageQueryManager.getInstalledPackages(packageManagerFlags)
|
||||
val chinaApps = mutableSetOf<String>()
|
||||
for (packageInfo in installedPackages) {
|
||||
if (PerAppProxyActivity.scanChinaPackage(packageInfo)) {
|
||||
chinaApps.add(packageInfo.packageName)
|
||||
}
|
||||
}
|
||||
Settings.perAppProxyManagedList = chinaApps
|
||||
Log.d(TAG, "rescan complete, found ${chinaApps.size} china apps")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,14 @@ package io.nekohasekai.sfa.compose.screen.settings
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
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
|
||||
@@ -22,6 +21,7 @@ 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.material.icons.outlined.Tune
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
@@ -30,10 +30,14 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -46,11 +50,10 @@ 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 io.nekohasekai.sfa.vendor.PackageQueryManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
@@ -66,7 +69,58 @@ fun ProfileOverrideScreen(navController: NavController) {
|
||||
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 showShizukuDialog by remember { mutableStateOf(false) }
|
||||
var showRootDialog by remember { mutableStateOf(false) }
|
||||
var showModeDialog by remember { mutableStateOf(false) }
|
||||
|
||||
val needsPrivilegedQuery = PackageQueryManager.needsPrivilegedQuery
|
||||
var packageQueryMode by remember { mutableStateOf(Settings.perAppProxyPackageQueryMode) }
|
||||
val useRootMode = packageQueryMode == Settings.PACKAGE_QUERY_MODE_ROOT
|
||||
|
||||
val isShizukuInstalled by PackageQueryManager.shizukuInstalled.collectAsState()
|
||||
val isShizukuBinderReady by PackageQueryManager.shizukuBinderReady.collectAsState()
|
||||
val isShizukuPermissionGranted by PackageQueryManager.shizukuPermissionGranted.collectAsState()
|
||||
val isShizukuAvailable = isShizukuBinderReady && isShizukuPermissionGranted
|
||||
|
||||
DisposableEffect(needsPrivilegedQuery) {
|
||||
if (needsPrivilegedQuery) {
|
||||
PackageQueryManager.registerListeners()
|
||||
}
|
||||
onDispose {
|
||||
if (needsPrivilegedQuery) {
|
||||
PackageQueryManager.unregisterListeners()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-disable per-app proxy if Shizuku authorization is revoked (only when using Shizuku mode)
|
||||
LaunchedEffect(isShizukuAvailable, useRootMode) {
|
||||
if (needsPrivilegedQuery && !useRootMode && !isShizukuAvailable && perAppProxyEnabled) {
|
||||
perAppProxyEnabled = false
|
||||
withContext(Dispatchers.IO) {
|
||||
Settings.perAppProxyEnabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-close dialog and enable feature when Shizuku becomes available
|
||||
LaunchedEffect(isShizukuAvailable) {
|
||||
if (needsPrivilegedQuery && isShizukuAvailable && showShizukuDialog) {
|
||||
showShizukuDialog = false
|
||||
perAppProxyEnabled = true
|
||||
withContext(Dispatchers.IO) {
|
||||
Settings.perAppProxyEnabled = true
|
||||
}
|
||||
if (managedModeEnabled) {
|
||||
isScanning = true
|
||||
val chinaApps = scanAllChinaApps()
|
||||
withContext(Dispatchers.IO) {
|
||||
Settings.perAppProxyManagedList = chinaApps
|
||||
}
|
||||
isScanning = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
@@ -158,7 +212,11 @@ fun ProfileOverrideScreen(navController: NavController) {
|
||||
}
|
||||
|
||||
// Section: Per-App Proxy
|
||||
val isPerAppProxyAvailable = Vendor.isPerAppProxyAvailable()
|
||||
val canUsePerAppProxy = if (needsPrivilegedQuery) {
|
||||
if (useRootMode) true else isShizukuAvailable
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.per_app_proxy),
|
||||
@@ -178,6 +236,52 @@ fun ProfileOverrideScreen(navController: NavController) {
|
||||
),
|
||||
) {
|
||||
Column {
|
||||
// Mode selector (only when privileged query is needed)
|
||||
if (needsPrivilegedQuery) {
|
||||
val modeEnabled = !perAppProxyEnabled
|
||||
val disabledAlpha = 0.38f
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
stringResource(R.string.per_app_proxy_package_query_mode),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = if (modeEnabled) Color.Unspecified
|
||||
else MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha),
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
if (useRootMode) "ROOT" else "Shizuku",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (modeEnabled) MaterialTheme.colorScheme.onSurfaceVariant
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = disabledAlpha),
|
||||
)
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Tune,
|
||||
contentDescription = null,
|
||||
tint = if (modeEnabled) MaterialTheme.colorScheme.primary
|
||||
else MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha),
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Outlined.KeyboardArrowRight,
|
||||
contentDescription = null,
|
||||
tint = if (modeEnabled) MaterialTheme.colorScheme.onSurfaceVariant
|
||||
else MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha),
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp))
|
||||
.clickable(enabled = modeEnabled) { showModeDialog = true },
|
||||
colors = ListItemDefaults.colors(
|
||||
containerColor = Color.Transparent,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Enabled toggle
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
@@ -186,19 +290,6 @@ fun ProfileOverrideScreen(navController: NavController) {
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
},
|
||||
supportingContent =
|
||||
if (!isPerAppProxyAvailable) {
|
||||
{
|
||||
Text(
|
||||
text = context.getString(R.string.per_app_proxy_disabled_play_store),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.FilterList,
|
||||
@@ -207,16 +298,16 @@ fun ProfileOverrideScreen(navController: NavController) {
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
if (isPerAppProxyAvailable) {
|
||||
if (isScanning) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
strokeWidth = 2.dp,
|
||||
)
|
||||
} else {
|
||||
Switch(
|
||||
checked = perAppProxyEnabled,
|
||||
onCheckedChange = { checked ->
|
||||
if (checked && needsPrivilegedQuery) {
|
||||
if (useRootMode) {
|
||||
showRootDialog = true
|
||||
} else {
|
||||
showShizukuDialog = true
|
||||
}
|
||||
} else {
|
||||
perAppProxyEnabled = checked
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Settings.perAppProxyEnabled = checked
|
||||
@@ -231,14 +322,16 @@ fun ProfileOverrideScreen(navController: NavController) {
|
||||
isScanning = false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = !isScanning,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier =
|
||||
Modifier.clip(
|
||||
if (perAppProxyEnabled && isPerAppProxyAvailable) {
|
||||
if (needsPrivilegedQuery) {
|
||||
RoundedCornerShape(0.dp)
|
||||
} else if (perAppProxyEnabled && canUsePerAppProxy) {
|
||||
RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)
|
||||
} else {
|
||||
RoundedCornerShape(12.dp)
|
||||
@@ -250,7 +343,7 @@ fun ProfileOverrideScreen(navController: NavController) {
|
||||
),
|
||||
)
|
||||
|
||||
if (perAppProxyEnabled && isPerAppProxyAvailable) {
|
||||
if (perAppProxyEnabled && canUsePerAppProxy) {
|
||||
// Manage entry
|
||||
val manageEnabled = !managedModeEnabled
|
||||
val disabledAlpha = 0.38f
|
||||
@@ -366,23 +459,199 @@ fun ProfileOverrideScreen(navController: NavController) {
|
||||
}
|
||||
}
|
||||
|
||||
// Dialog for Per-app Proxy disabled message
|
||||
if (showPerAppProxyDialog) {
|
||||
// Shizuku dialog
|
||||
if (showShizukuDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showPerAppProxyDialog = false },
|
||||
onDismissRequest = { showShizukuDialog = false },
|
||||
title = {
|
||||
Text(stringResource(R.string.unavailable))
|
||||
Text(stringResource(R.string.per_app_proxy))
|
||||
},
|
||||
text = {
|
||||
Text(context.getString(R.string.per_app_proxy_disabled_message))
|
||||
Text(stringResource(R.string.per_app_proxy_shizuku_required))
|
||||
},
|
||||
confirmButton = {
|
||||
when {
|
||||
isShizukuAvailable -> {
|
||||
TextButton(
|
||||
onClick = {
|
||||
showShizukuDialog = false
|
||||
perAppProxyEnabled = true
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Settings.perAppProxyEnabled = true
|
||||
}
|
||||
if (managedModeEnabled) {
|
||||
isScanning = true
|
||||
scope.launch {
|
||||
val chinaApps = scanAllChinaApps()
|
||||
withContext(Dispatchers.IO) {
|
||||
Settings.perAppProxyManagedList = chinaApps
|
||||
}
|
||||
isScanning = false
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(stringResource(R.string.ok))
|
||||
}
|
||||
}
|
||||
isShizukuBinderReady -> {
|
||||
TextButton(
|
||||
onClick = {
|
||||
PackageQueryManager.requestShizukuPermission()
|
||||
},
|
||||
) {
|
||||
Text(stringResource(R.string.request_shizuku))
|
||||
}
|
||||
}
|
||||
isShizukuInstalled -> {
|
||||
TextButton(
|
||||
onClick = {
|
||||
showShizukuDialog = false
|
||||
val intent = context.packageManager.getLaunchIntentForPackage("moe.shizuku.privileged.api")
|
||||
if (intent != null) {
|
||||
context.startActivity(intent)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(stringResource(R.string.start_shizuku))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
TextButton(
|
||||
onClick = {
|
||||
showShizukuDialog = false
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://shizuku.rikka.app/"))
|
||||
context.startActivity(intent)
|
||||
},
|
||||
) {
|
||||
Text(stringResource(R.string.get_shizuku))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
if (!isShizukuAvailable) {
|
||||
TextButton(
|
||||
onClick = { showShizukuDialog = false },
|
||||
) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// ROOT dialog
|
||||
if (showRootDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showRootDialog = false },
|
||||
title = {
|
||||
Text(stringResource(R.string.per_app_proxy))
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(R.string.per_app_proxy_root_required))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = { showPerAppProxyDialog = false },
|
||||
) {
|
||||
Text(context.getString(R.string.ok))
|
||||
onClick = {
|
||||
scope.launch {
|
||||
val hasRoot = PackageQueryManager.checkRootAvailable()
|
||||
if (hasRoot) {
|
||||
showRootDialog = false
|
||||
perAppProxyEnabled = true
|
||||
withContext(Dispatchers.IO) {
|
||||
Settings.perAppProxyEnabled = true
|
||||
}
|
||||
if (managedModeEnabled) {
|
||||
isScanning = true
|
||||
val chinaApps = scanAllChinaApps()
|
||||
withContext(Dispatchers.IO) {
|
||||
Settings.perAppProxyManagedList = chinaApps
|
||||
}
|
||||
isScanning = false
|
||||
}
|
||||
} else {
|
||||
showRootDialog = false
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.root_access_denied,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(stringResource(R.string.ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = { showRootDialog = false },
|
||||
) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Mode selection dialog
|
||||
if (showModeDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showModeDialog = false },
|
||||
title = {
|
||||
Text(stringResource(R.string.per_app_proxy_package_query_mode))
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
ListItem(
|
||||
headlineContent = { Text("Shizuku") },
|
||||
leadingContent = {
|
||||
RadioButton(
|
||||
selected = packageQueryMode == Settings.PACKAGE_QUERY_MODE_SHIZUKU,
|
||||
onClick = null,
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable {
|
||||
packageQueryMode = Settings.PACKAGE_QUERY_MODE_SHIZUKU
|
||||
PackageQueryManager.setQueryMode(Settings.PACKAGE_QUERY_MODE_SHIZUKU)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Settings.perAppProxyPackageQueryMode = Settings.PACKAGE_QUERY_MODE_SHIZUKU
|
||||
}
|
||||
if (perAppProxyEnabled && !isShizukuAvailable) {
|
||||
perAppProxyEnabled = false
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Settings.perAppProxyEnabled = false
|
||||
}
|
||||
}
|
||||
showModeDialog = false
|
||||
},
|
||||
colors = ListItemDefaults.colors(
|
||||
containerColor = Color.Transparent,
|
||||
),
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text("ROOT") },
|
||||
leadingContent = {
|
||||
RadioButton(
|
||||
selected = packageQueryMode == Settings.PACKAGE_QUERY_MODE_ROOT,
|
||||
onClick = null,
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable {
|
||||
packageQueryMode = Settings.PACKAGE_QUERY_MODE_ROOT
|
||||
PackageQueryManager.setQueryMode(Settings.PACKAGE_QUERY_MODE_ROOT)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Settings.perAppProxyPackageQueryMode = Settings.PACKAGE_QUERY_MODE_ROOT
|
||||
}
|
||||
showModeDialog = false
|
||||
},
|
||||
colors = ListItemDefaults.colors(
|
||||
containerColor = Color.Transparent,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -390,25 +659,22 @@ 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
|
||||
PackageManager.MATCH_UNINSTALLED_PACKAGES or
|
||||
PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or
|
||||
PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
PackageManager.GET_UNINSTALLED_PACKAGES
|
||||
PackageManager.GET_UNINSTALLED_PACKAGES or
|
||||
PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or
|
||||
PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
|
||||
}
|
||||
|
||||
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 installedPackages = PackageQueryManager.getInstalledPackages(packageManagerFlags)
|
||||
|
||||
val chinaApps = mutableSetOf<String>()
|
||||
installedPackages.map { packageInfo ->
|
||||
async {
|
||||
if (PerAppProxyActivity.scanChinaPackage(packageInfo.packageName)) {
|
||||
if (PerAppProxyActivity.scanChinaPackage(packageInfo)) {
|
||||
synchronized(chinaApps) {
|
||||
chinaApps.add(packageInfo.packageName)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ object SettingsKey {
|
||||
const val PER_APP_PROXY_LIST = "per_app_proxy_list"
|
||||
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 PER_APP_PROXY_PACKAGE_QUERY_MODE = "per_app_proxy_package_query_mode"
|
||||
|
||||
const val SYSTEM_PROXY_ENABLED = "system_proxy_enabled"
|
||||
|
||||
|
||||
@@ -79,6 +79,10 @@ object Settings {
|
||||
var perAppProxyManagedMode by dataStore.boolean(SettingsKey.PER_APP_PROXY_MANAGED_MODE) { false }
|
||||
var perAppProxyManagedList by dataStore.stringSet(SettingsKey.PER_APP_PROXY_MANAGED_LIST) { emptySet() }
|
||||
|
||||
const val PACKAGE_QUERY_MODE_SHIZUKU = "SHIZUKU"
|
||||
const val PACKAGE_QUERY_MODE_ROOT = "ROOT"
|
||||
var perAppProxyPackageQueryMode by dataStore.string(SettingsKey.PER_APP_PROXY_PACKAGE_QUERY_MODE) { PACKAGE_QUERY_MODE_SHIZUKU }
|
||||
|
||||
fun getEffectivePerAppProxyList(): Set<String> {
|
||||
return if (perAppProxyManagedMode) {
|
||||
perAppProxyList union perAppProxyManagedList
|
||||
|
||||
@@ -22,6 +22,7 @@ import io.nekohasekai.sfa.databinding.ViewVpnAppItemBinding
|
||||
import io.nekohasekai.sfa.ktx.dp2px
|
||||
import io.nekohasekai.sfa.ktx.toStringIterator
|
||||
import io.nekohasekai.sfa.ui.shared.AbstractActivity
|
||||
import io.nekohasekai.sfa.vendor.PackageQueryManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -139,13 +140,7 @@ class VPNScanActivity : AbstractActivity<ActivityVpnScanBinding>() {
|
||||
@Suppress("DEPRECATION")
|
||||
PackageManager.GET_UNINSTALLED_PACKAGES
|
||||
}
|
||||
val installedPackages =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
packageManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(flag.toLong()))
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
packageManager.getInstalledPackages(flag)
|
||||
}
|
||||
val installedPackages = PackageQueryManager.getInstalledPackages(flag)
|
||||
val vpnAppList =
|
||||
installedPackages.filter {
|
||||
it.services?.any { it.permission == Manifest.permission.BIND_VPN_SERVICE && it.applicationInfo != null }
|
||||
|
||||
@@ -32,7 +32,8 @@ import io.nekohasekai.sfa.databinding.DialogProgressbarBinding
|
||||
import io.nekohasekai.sfa.databinding.ViewAppListItemBinding
|
||||
import io.nekohasekai.sfa.ktx.clipboardText
|
||||
import io.nekohasekai.sfa.ui.shared.AbstractActivity
|
||||
import io.nekohasekai.sfa.vendor.Vendor
|
||||
import io.nekohasekai.sfa.vendor.PackageQueryManager
|
||||
import io.nekohasekai.sfa.vendor.PrivilegedAccessRequiredException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
@@ -80,6 +81,8 @@ class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
|
||||
val applicationLabel by lazy {
|
||||
appInfo.loadLabel(packageManager).toString()
|
||||
}
|
||||
|
||||
val info: PackageInfo get() = packageInfo
|
||||
}
|
||||
|
||||
private lateinit var adapter: ApplicationAdapter
|
||||
@@ -91,19 +94,6 @@ class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Check if Per-app Proxy is available
|
||||
if (!Vendor.isPerAppProxyAvailable()) {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle("Unavailable")
|
||||
.setMessage(getString(R.string.per_app_proxy_disabled_message))
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
finish()
|
||||
}
|
||||
.setCancelable(false)
|
||||
.show()
|
||||
return
|
||||
}
|
||||
|
||||
setTitle(R.string.per_app_proxy)
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.appList) { view, windowInsets ->
|
||||
@@ -127,7 +117,9 @@ class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
|
||||
binding.perAppProxyMode.setText(R.string.per_app_proxy_mode_exclude_description)
|
||||
}
|
||||
}
|
||||
reloadApplicationList()
|
||||
if (!reloadApplicationList()) {
|
||||
return@withContext
|
||||
}
|
||||
filterApplicationList()
|
||||
withContext(Dispatchers.Main) {
|
||||
adapter = ApplicationAdapter(displayPackages)
|
||||
@@ -139,24 +131,30 @@ class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun reloadApplicationList() {
|
||||
private suspend fun reloadApplicationList(): Boolean {
|
||||
val packageManagerFlags =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
PackageManager.GET_PERMISSIONS or PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||
PackageManager.GET_PERMISSIONS or PackageManager.MATCH_UNINSTALLED_PACKAGES or
|
||||
PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or
|
||||
PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
PackageManager.GET_PERMISSIONS or PackageManager.GET_UNINSTALLED_PACKAGES
|
||||
PackageManager.GET_PERMISSIONS or PackageManager.GET_UNINSTALLED_PACKAGES or
|
||||
PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or
|
||||
PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
|
||||
}
|
||||
val installedPackages =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
packageManager.getInstalledPackages(
|
||||
PackageManager.PackageInfoFlags.of(
|
||||
packageManagerFlags.toLong(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
packageManager.getInstalledPackages(packageManagerFlags)
|
||||
val installedPackages = try {
|
||||
PackageQueryManager.getInstalledPackages(packageManagerFlags)
|
||||
} catch (e: PrivilegedAccessRequiredException) {
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
this@PerAppProxyActivity,
|
||||
R.string.privileged_access_required,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
finish()
|
||||
}
|
||||
return false
|
||||
}
|
||||
val packages = mutableListOf<PackageCache>()
|
||||
for (packageInfo in installedPackages) {
|
||||
@@ -173,6 +171,7 @@ class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
|
||||
}
|
||||
this.packages = packages
|
||||
this.selectedUIDs = selectedUIDs
|
||||
return true
|
||||
}
|
||||
|
||||
private fun filterApplicationList(selectedUIDs: Set<Int> = this.selectedUIDs) {
|
||||
@@ -592,7 +591,7 @@ class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
|
||||
val progressInt = AtomicInteger()
|
||||
currentPackages.map { it ->
|
||||
async {
|
||||
if (scanChinaPackage(it.packageName)) {
|
||||
if (scanChinaPackage(it.info)) {
|
||||
foundApps[it.packageName] = it
|
||||
}
|
||||
runOnUiThread {
|
||||
@@ -729,36 +728,17 @@ class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
|
||||
("(" + chinaAppPrefixList.joinToString("|").replace(".", "\\.") + ").*").toRegex()
|
||||
}
|
||||
|
||||
fun scanChinaPackage(packageName: String): Boolean {
|
||||
fun scanChinaPackage(packageInfo: PackageInfo): Boolean {
|
||||
val packageName = packageInfo.packageName
|
||||
skipPrefixList.forEach {
|
||||
if (packageName == it || packageName.startsWith("$it.")) return false
|
||||
}
|
||||
|
||||
val packageManagerFlags =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
PackageManager.MATCH_UNINSTALLED_PACKAGES or PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
PackageManager.GET_UNINSTALLED_PACKAGES or PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
|
||||
}
|
||||
if (packageName.matches(chinaAppRegex)) {
|
||||
Log.d("PerAppProxyActivity", "Match package name: $packageName")
|
||||
return true
|
||||
}
|
||||
try {
|
||||
val packageInfo =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
Application.packageManager.getPackageInfo(
|
||||
packageName,
|
||||
PackageManager.PackageInfoFlags.of(packageManagerFlags.toLong()),
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
Application.packageManager.getPackageInfo(
|
||||
packageName,
|
||||
packageManagerFlags,
|
||||
)
|
||||
}
|
||||
val appInfo = packageInfo.applicationInfo ?: return false
|
||||
packageInfo.services?.forEach {
|
||||
if (it.name.matches(chinaAppRegex)) {
|
||||
|
||||
@@ -180,9 +180,16 @@
|
||||
<string name="location_permission_background_description">在 Android 10 及更高版本中,需要<strong>后台位置</strong>权限。选择<strong>始终允许</strong>以授予权限。</string>
|
||||
<string name="notification_permission_title">通知权限</string>
|
||||
<string name="notification_permission_required_description">sing-box 无法在没有发送通知权限的情况下显示实时网速。请授予权限或禁用实时网速通知后再启动服务。</string>
|
||||
<string name="per_app_proxy_disabled_play_store">Play 商店版本中不可用</string>
|
||||
<string name="per_app_proxy_disabled_message">Google Play 拒绝允许我们使用 QUERY_ALL_PACKAGES 权限(同时不禁止其他类似应用这样做),而这是列出应用程序所必需的。</string>
|
||||
<string name="per_app_proxy_package_query_mode">模式</string>
|
||||
<string name="per_app_proxy_shizuku_required">通过 Play 商店安装时,分应用代理需要 Shizuku。Google Play 拒绝允许我们使用 QUERY_ALL_PACKAGES 权限(同时不禁止其他类似应用这样做),而这是列出应用程序所必需的。</string>
|
||||
<string name="per_app_proxy_root_required">通过 Play 商店安装时,分应用代理需要 ROOT。Google Play 拒绝允许我们使用 QUERY_ALL_PACKAGES 权限(同时不禁止其他类似应用这样做),而这是列出应用程序所必需的。</string>
|
||||
<string name="per_app_proxy_shizuku_mode">分应用代理(Shizuku 模式)</string>
|
||||
<string name="start_shizuku">启动 Shizuku</string>
|
||||
<string name="request_shizuku">授权 Shizuku</string>
|
||||
<string name="get_shizuku">获取 Shizuku</string>
|
||||
<string name="root_access_required">需要 Root 权限</string>
|
||||
<string name="root_access_denied">Root 权限被拒绝</string>
|
||||
<string name="privileged_access_required">需要 Root 或 Shizuku 权限来获取完整应用列表</string>
|
||||
<string name="title_connections">连接</string>
|
||||
<string name="title_others">其他</string>
|
||||
<string name="title_experimental_features">实验性功能</string>
|
||||
|
||||
@@ -154,6 +154,8 @@
|
||||
<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="start_shizuku">Start Shizuku</string>
|
||||
<string name="request_shizuku">Request 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>
|
||||
@@ -247,9 +249,13 @@
|
||||
<string name="location_permission_background_description">On Android 10 and up, <strong>background location</strong> permission is required. Select <strong>Allow all the time</strong> to grant the permission.</string>
|
||||
<string name="notification_permission_title">Notification permission</string>
|
||||
<string name="notification_permission_required_description">sing-box is unable to show real-time network speeds without the permission to send notifications. Please grant the permission or disable real-time network speeds notification before starting the service.</string>
|
||||
<string name="per_app_proxy_disabled_play_store">Unavailable in the Play Store version</string>
|
||||
<string name="per_app_proxy_disabled_message">Google Play refuses to allow us to use the QUERY_ALL_PACKAGES permission (while not prohibiting other similar apps from doing so), which is required for listing apps.</string>
|
||||
<string name="per_app_proxy_package_query_mode">Mode</string>
|
||||
<string name="per_app_proxy_shizuku_required">When installed from the Play Store, per-app proxy requires Shizuku. Google Play refuses to allow us to use the QUERY_ALL_PACKAGES permission (while not prohibiting other similar apps from doing so), which is required for listing apps.</string>
|
||||
<string name="per_app_proxy_root_required">When installed from the Play Store, per-app proxy requires ROOT. Google Play refuses to allow us to use the QUERY_ALL_PACKAGES permission (while not prohibiting other similar apps from doing so), which is required for listing apps.</string>
|
||||
<string name="per_app_proxy_shizuku_mode">Per-App Proxy (Shizuku mode)</string>
|
||||
<string name="root_access_required">Root access required</string>
|
||||
<string name="root_access_denied">Root access denied</string>
|
||||
<string name="privileged_access_required">Root or Shizuku access required to get the complete app list</string>
|
||||
<string name="dashboard_items">Dashboard Items</string>
|
||||
<string name="reset_order">Reset order</string>
|
||||
<string name="reset">Reset</string>
|
||||
|
||||
47
app/src/minApi21/java/io/nekohasekai/sfa/vendor/PackageQueryManager.kt
vendored
Normal file
47
app/src/minApi21/java/io/nekohasekai/sfa/vendor/PackageQueryManager.kt
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
package io.nekohasekai.sfa.vendor
|
||||
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import io.nekohasekai.sfa.Application
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
object PackageQueryManager {
|
||||
|
||||
val needsPrivilegedQuery: Boolean = false
|
||||
|
||||
private val _queryMode = MutableStateFlow("")
|
||||
val queryMode: StateFlow<String> = _queryMode
|
||||
|
||||
val shizukuInstalled: StateFlow<Boolean> = MutableStateFlow(false)
|
||||
val shizukuBinderReady: StateFlow<Boolean> = MutableStateFlow(false)
|
||||
val shizukuPermissionGranted: StateFlow<Boolean> = MutableStateFlow(false)
|
||||
val rootAvailable: StateFlow<Boolean?> = MutableStateFlow(null)
|
||||
val rootServiceConnected: StateFlow<Boolean> = MutableStateFlow(false)
|
||||
|
||||
fun isShizukuAvailable(): Boolean = false
|
||||
|
||||
fun registerListeners() {}
|
||||
|
||||
fun unregisterListeners() {}
|
||||
|
||||
fun requestShizukuPermission() {}
|
||||
|
||||
suspend fun checkRootAvailable(): Boolean = false
|
||||
|
||||
fun setQueryMode(mode: String) {
|
||||
_queryMode.value = mode
|
||||
}
|
||||
|
||||
suspend fun getInstalledPackages(flags: Int): List<PackageInfo> {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
Application.packageManager.getInstalledPackages(
|
||||
PackageManager.PackageInfoFlags.of(flags.toLong())
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
Application.packageManager.getInstalledPackages(flags)
|
||||
}
|
||||
}
|
||||
}
|
||||
7
app/src/minApi23/aidl/io/nekohasekai/sfa/vendor/IRootPackageManager.aidl
vendored
Normal file
7
app/src/minApi23/aidl/io/nekohasekai/sfa/vendor/IRootPackageManager.aidl
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
package io.nekohasekai.sfa.vendor;
|
||||
|
||||
import android.content.pm.PackageInfo;
|
||||
|
||||
interface IRootPackageManager {
|
||||
List<PackageInfo> getInstalledPackages(int flags);
|
||||
}
|
||||
101
app/src/minApi23/java/io/nekohasekai/sfa/vendor/PackageQueryManager.kt
vendored
Normal file
101
app/src/minApi23/java/io/nekohasekai/sfa/vendor/PackageQueryManager.kt
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
package io.nekohasekai.sfa.vendor
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import io.nekohasekai.sfa.Application
|
||||
import io.nekohasekai.sfa.database.Settings
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
class PrivilegedAccessRequiredException(message: String) : Exception(message)
|
||||
|
||||
object PackageQueryManager {
|
||||
|
||||
private const val TAG = "PackageQueryManager"
|
||||
|
||||
val needsPrivilegedQuery: Boolean by lazy {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
// Check if QUERY_ALL_PACKAGES is declared in manifest
|
||||
val packageInfo = Application.packageManager.getPackageInfo(
|
||||
Application.application.packageName,
|
||||
PackageManager.GET_PERMISSIONS
|
||||
)
|
||||
val hasPermission = packageInfo.requestedPermissions?.contains(
|
||||
Manifest.permission.QUERY_ALL_PACKAGES
|
||||
) == true
|
||||
!hasPermission
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private val _queryMode = MutableStateFlow(Settings.perAppProxyPackageQueryMode)
|
||||
val queryMode: StateFlow<String> = _queryMode
|
||||
|
||||
val shizukuInstalled: StateFlow<Boolean> get() = ShizukuPackageManager.shizukuInstalled
|
||||
val shizukuBinderReady: StateFlow<Boolean> get() = ShizukuPackageManager.binderReady
|
||||
val shizukuPermissionGranted: StateFlow<Boolean> get() = ShizukuPackageManager.permissionGranted
|
||||
val rootAvailable: StateFlow<Boolean?> get() = RootPackageManager.rootAvailable
|
||||
val rootServiceConnected: StateFlow<Boolean> get() = RootPackageManager.serviceConnected
|
||||
|
||||
fun isShizukuAvailable(): Boolean =
|
||||
ShizukuPackageManager.isAvailable() && ShizukuPackageManager.checkPermission()
|
||||
|
||||
fun registerListeners() {
|
||||
ShizukuPackageManager.registerListeners()
|
||||
_queryMode.value = Settings.perAppProxyPackageQueryMode
|
||||
}
|
||||
|
||||
fun unregisterListeners() {
|
||||
ShizukuPackageManager.unregisterListeners()
|
||||
}
|
||||
|
||||
fun requestShizukuPermission() {
|
||||
ShizukuPackageManager.requestPermission()
|
||||
}
|
||||
|
||||
suspend fun checkRootAvailable(): Boolean {
|
||||
return RootPackageManager.checkRootAvailable()
|
||||
}
|
||||
|
||||
fun setQueryMode(mode: String) {
|
||||
_queryMode.value = mode
|
||||
}
|
||||
|
||||
suspend fun getInstalledPackages(flags: Int): List<PackageInfo> {
|
||||
if (!needsPrivilegedQuery) {
|
||||
return getPackagesViaPackageManager(flags)
|
||||
}
|
||||
|
||||
val mode = _queryMode.value
|
||||
|
||||
if (mode == Settings.PACKAGE_QUERY_MODE_ROOT) {
|
||||
if (rootAvailable.value != true) {
|
||||
val isAvailable = RootPackageManager.checkRootAvailable()
|
||||
if (!isAvailable) {
|
||||
throw PrivilegedAccessRequiredException("ROOT access required")
|
||||
}
|
||||
}
|
||||
return RootPackageManager.getInstalledPackages(flags)
|
||||
}
|
||||
|
||||
if (!isShizukuAvailable()) {
|
||||
throw PrivilegedAccessRequiredException("Shizuku access required")
|
||||
}
|
||||
return ShizukuPackageManager.getInstalledPackages(flags)
|
||||
}
|
||||
|
||||
private fun getPackagesViaPackageManager(flags: Int): List<PackageInfo> {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
Application.packageManager.getInstalledPackages(
|
||||
PackageManager.PackageInfoFlags.of(flags.toLong())
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
Application.packageManager.getInstalledPackages(flags)
|
||||
}
|
||||
}
|
||||
}
|
||||
98
app/src/minApi23/java/io/nekohasekai/sfa/vendor/RootPackageManager.kt
vendored
Normal file
98
app/src/minApi23/java/io/nekohasekai/sfa/vendor/RootPackageManager.kt
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
package io.nekohasekai.sfa.vendor
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.content.pm.PackageInfo
|
||||
import android.os.IBinder
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ipc.RootService
|
||||
import io.nekohasekai.sfa.Application
|
||||
import io.nekohasekai.sfa.BuildConfig
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
object RootPackageManager {
|
||||
|
||||
init {
|
||||
Shell.enableVerboseLogging = BuildConfig.DEBUG
|
||||
Shell.setDefaultBuilder(
|
||||
Shell.Builder.create()
|
||||
.setFlags(Shell.FLAG_MOUNT_MASTER)
|
||||
.setTimeout(10)
|
||||
)
|
||||
}
|
||||
|
||||
private val _rootAvailable = MutableStateFlow<Boolean?>(null)
|
||||
val rootAvailable: StateFlow<Boolean?> = _rootAvailable
|
||||
|
||||
private val _serviceConnected = MutableStateFlow(false)
|
||||
val serviceConnected: StateFlow<Boolean> = _serviceConnected
|
||||
|
||||
private var service: IRootPackageManager? = null
|
||||
private var connection: ServiceConnection? = null
|
||||
private val connectionMutex = Mutex()
|
||||
|
||||
suspend fun checkRootAvailable(): Boolean {
|
||||
Shell.getCachedShell()?.close()
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
Shell.getShell { shell ->
|
||||
val available = shell.isRoot
|
||||
_rootAvailable.value = available
|
||||
continuation.resume(available)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun bindService(): IRootPackageManager = connectionMutex.withLock {
|
||||
service?.let { return it }
|
||||
|
||||
return withContext(Dispatchers.Main) {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
val conn = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
|
||||
val svc = IRootPackageManager.Stub.asInterface(binder)
|
||||
service = svc
|
||||
connection = this
|
||||
_serviceConnected.value = true
|
||||
continuation.resume(svc)
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
service = null
|
||||
connection = null
|
||||
_serviceConnected.value = false
|
||||
}
|
||||
}
|
||||
|
||||
val intent = Intent(Application.application, RootPackageManagerService::class.java)
|
||||
RootService.bind(intent, conn)
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
RootService.unbind(conn)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun unbindService() {
|
||||
connection?.let {
|
||||
RootService.unbind(it)
|
||||
connection = null
|
||||
service = null
|
||||
_serviceConnected.value = false
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getInstalledPackages(flags: Int): List<PackageInfo> {
|
||||
val svc = bindService()
|
||||
return svc.getInstalledPackages(flags)
|
||||
}
|
||||
}
|
||||
28
app/src/minApi23/java/io/nekohasekai/sfa/vendor/RootPackageManagerService.kt
vendored
Normal file
28
app/src/minApi23/java/io/nekohasekai/sfa/vendor/RootPackageManagerService.kt
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
package io.nekohasekai.sfa.vendor
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import com.topjohnwu.superuser.ipc.RootService
|
||||
|
||||
class RootPackageManagerService : RootService() {
|
||||
|
||||
private val binder = object : IRootPackageManager.Stub() {
|
||||
override fun getInstalledPackages(flags: Int): List<PackageInfo> {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
packageManager.getInstalledPackages(
|
||||
PackageManager.PackageInfoFlags.of(flags.toLong())
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
packageManager.getInstalledPackages(flags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
return binder
|
||||
}
|
||||
}
|
||||
114
app/src/minApi23/java/io/nekohasekai/sfa/vendor/ShizukuPackageManager.kt
vendored
Normal file
114
app/src/minApi23/java/io/nekohasekai/sfa/vendor/ShizukuPackageManager.kt
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
package io.nekohasekai.sfa.vendor
|
||||
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import io.nekohasekai.sfa.Application
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
||||
import rikka.shizuku.Shizuku
|
||||
import rikka.shizuku.ShizukuBinderWrapper
|
||||
import rikka.shizuku.SystemServiceHelper
|
||||
|
||||
object ShizukuPackageManager {
|
||||
|
||||
private const val SHIZUKU_PACKAGE = "moe.shizuku.privileged.api"
|
||||
|
||||
private val _shizukuInstalled = MutableStateFlow(false)
|
||||
val shizukuInstalled: StateFlow<Boolean> = _shizukuInstalled
|
||||
|
||||
private val _binderReady = MutableStateFlow(false)
|
||||
val binderReady: StateFlow<Boolean> = _binderReady
|
||||
|
||||
private val _permissionGranted = MutableStateFlow(false)
|
||||
val permissionGranted: StateFlow<Boolean> = _permissionGranted
|
||||
|
||||
private val binderReceivedListener = Shizuku.OnBinderReceivedListener {
|
||||
_binderReady.value = true
|
||||
_permissionGranted.value = checkPermission()
|
||||
}
|
||||
|
||||
private val binderDeadListener = Shizuku.OnBinderDeadListener {
|
||||
_binderReady.value = false
|
||||
_permissionGranted.value = false
|
||||
}
|
||||
|
||||
private val permissionResultListener = Shizuku.OnRequestPermissionResultListener { _, grantResult ->
|
||||
_permissionGranted.value = grantResult == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
fun registerListeners() {
|
||||
Shizuku.addBinderReceivedListenerSticky(binderReceivedListener)
|
||||
Shizuku.addBinderDeadListener(binderDeadListener)
|
||||
Shizuku.addRequestPermissionResultListener(permissionResultListener)
|
||||
_shizukuInstalled.value = isShizukuInstalled()
|
||||
_binderReady.value = isAvailable()
|
||||
_permissionGranted.value = checkPermission()
|
||||
}
|
||||
|
||||
fun isShizukuInstalled(): Boolean {
|
||||
return try {
|
||||
Application.packageManager.getPackageInfo(SHIZUKU_PACKAGE, 0)
|
||||
true
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun unregisterListeners() {
|
||||
Shizuku.removeBinderReceivedListener(binderReceivedListener)
|
||||
Shizuku.removeBinderDeadListener(binderDeadListener)
|
||||
Shizuku.removeRequestPermissionResultListener(permissionResultListener)
|
||||
}
|
||||
|
||||
fun isAvailable(): Boolean = ShizukuInstaller.isAvailable()
|
||||
|
||||
fun checkPermission(): Boolean = ShizukuInstaller.checkPermission()
|
||||
|
||||
fun requestPermission() = ShizukuInstaller.requestPermission()
|
||||
|
||||
fun getInstalledPackages(flags: Int): List<PackageInfo> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
HiddenApiBypass.addHiddenApiExemptions("")
|
||||
}
|
||||
|
||||
val packageManagerBinder = SystemServiceHelper.getSystemService("package")
|
||||
val wrappedBinder = ShizukuBinderWrapper(packageManagerBinder)
|
||||
|
||||
val iPackageManagerClass = Class.forName("android.content.pm.IPackageManager")
|
||||
val stubClass = Class.forName("android.content.pm.IPackageManager\$Stub")
|
||||
val asInterfaceMethod = stubClass.getMethod("asInterface", IBinder::class.java)
|
||||
val iPackageManager = asInterfaceMethod.invoke(null, wrappedBinder)
|
||||
|
||||
val userId = android.os.Process.myUserHandle().hashCode()
|
||||
|
||||
val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val method = iPackageManagerClass.getMethod(
|
||||
"getInstalledPackages",
|
||||
Long::class.javaPrimitiveType,
|
||||
Int::class.javaPrimitiveType
|
||||
)
|
||||
method.invoke(iPackageManager, flags.toLong(), userId)
|
||||
} else {
|
||||
val method = iPackageManagerClass.getMethod(
|
||||
"getInstalledPackages",
|
||||
Int::class.javaPrimitiveType,
|
||||
Int::class.javaPrimitiveType
|
||||
)
|
||||
method.invoke(iPackageManager, flags, userId)
|
||||
}
|
||||
|
||||
return extractPackageList(result)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun extractPackageList(parceledListSlice: Any?): List<PackageInfo> {
|
||||
if (parceledListSlice == null) return emptyList()
|
||||
|
||||
val getListMethod = parceledListSlice.javaClass.getMethod("getList")
|
||||
val list = getListMethod.invoke(parceledListSlice) as? List<*>
|
||||
return list?.filterIsInstance<PackageInfo>() ?: emptyList()
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="moe.shizuku.manager.permission.API_V23" />
|
||||
|
||||
<uses-sdk tools:overrideLibrary="rikka.shizuku.provider,rikka.shizuku.api,rikka.shizuku.aidl,rikka.shizuku.shared" />
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<provider
|
||||
@@ -13,6 +8,10 @@
|
||||
android:exported="true"
|
||||
android:multiprocess="false"
|
||||
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||
|
||||
<service
|
||||
android:name=".vendor.RootPackageManagerService"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -95,10 +95,6 @@ object Vendor : VendorInterface {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun isPerAppProxyAvailable(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun supportsTrackSelection(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -95,10 +95,6 @@ object Vendor : VendorInterface {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun isPerAppProxyAvailable(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun supportsTrackSelection(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -7,4 +7,17 @@
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:node="remove" />
|
||||
|
||||
<application>
|
||||
<provider
|
||||
android:name="rikka.shizuku.ShizukuProvider"
|
||||
android:authorities="${applicationId}.shizuku"
|
||||
android:exported="true"
|
||||
android:multiprocess="false"
|
||||
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||
|
||||
<service
|
||||
android:name=".vendor.RootPackageManagerService"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -93,19 +93,11 @@ object Vendor : VendorInterface {
|
||||
}
|
||||
}
|
||||
|
||||
override fun isPerAppProxyAvailable(): Boolean {
|
||||
// Per-app Proxy is disabled for Play Store builds due to QUERY_ALL_PACKAGES permission restriction
|
||||
return false
|
||||
}
|
||||
|
||||
override fun supportsTrackSelection(): Boolean {
|
||||
// Play Store doesn't support track selection
|
||||
return false
|
||||
}
|
||||
|
||||
override fun checkUpdateAsync(): UpdateInfo? {
|
||||
// Play Store updates are handled by the Play Core library
|
||||
// We can't get version info in the same way as GitHub
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user