Add alternative support for QUERY_ALL_PACKAGES in play flavor

This commit is contained in:
世界
2025-12-25 01:49:27 +08:00
parent 104da5d312
commit 08f51d5469
22 changed files with 855 additions and 169 deletions

View File

@@ -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"

View File

@@ -238,4 +238,4 @@
</application>
</manifest>
</manifest>

View File

@@ -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(),

View File

@@ -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 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")
} else {
Settings.perAppProxyManagedList -= packageName
Log.d(TAG, "removed from managed list")
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()
}
} 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 {
@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")
}
}

View File

@@ -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,38 +298,40 @@ fun ProfileOverrideScreen(navController: NavController) {
)
},
trailingContent = {
if (isPerAppProxyAvailable) {
if (isScanning) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
strokeWidth = 2.dp,
)
} else {
Switch(
checked = perAppProxyEnabled,
onCheckedChange = { checked ->
perAppProxyEnabled = checked
scope.launch(Dispatchers.IO) {
Settings.perAppProxyEnabled = checked
}
if (checked && managedModeEnabled) {
isScanning = true
scope.launch {
val chinaApps = scanAllChinaApps()
withContext(Dispatchers.IO) {
Settings.perAppProxyManagedList = chinaApps
}
isScanning = false
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
}
if (checked && managedModeEnabled) {
isScanning = true
scope.launch {
val chinaApps = scanAllChinaApps()
withContext(Dispatchers.IO) {
Settings.perAppProxyManagedList = chinaApps
}
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 },
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(context.getString(R.string.ok))
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)
}

View File

@@ -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"

View File

@@ -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

View File

@@ -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 }

View File

@@ -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,25 +131,31 @@ 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) {
if (packageInfo.packageName == packageName) continue
@@ -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)) {

View File

@@ -180,9 +180,16 @@
<string name="location_permission_background_description">在 Android 10 及更高版本中,需要&lt;strong&gt;后台位置&lt;/strong&gt;权限。选择&lt;strong&gt;始终允许&lt;/strong&gt;以授予权限。</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>

View File

@@ -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, &lt;strong&gt;background location&lt;/strong&gt; permission is required. Select &lt;strong&gt;Allow all the time&lt;/strong&gt; 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>

View 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)
}
}
}

View File

@@ -0,0 +1,7 @@
package io.nekohasekai.sfa.vendor;
import android.content.pm.PackageInfo;
interface IRootPackageManager {
List<PackageInfo> getInstalledPackages(int flags);
}

View 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)
}
}
}

View 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)
}
}

View 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
}
}

View 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()
}
}

View File

@@ -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>

View File

@@ -95,10 +95,6 @@ object Vendor : VendorInterface {
return null
}
override fun isPerAppProxyAvailable(): Boolean {
return true
}
override fun supportsTrackSelection(): Boolean {
return true
}

View File

@@ -95,10 +95,6 @@ object Vendor : VendorInterface {
return null
}
override fun isPerAppProxyAvailable(): Boolean {
return true
}
override fun supportsTrackSelection(): Boolean {
return true
}

View File

@@ -7,4 +7,17 @@
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:node="remove" />
</manifest>
<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>

View File

@@ -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
}
}