Add alternative support for QUERY_ALL_PACKAGES in play flavor
This commit is contained in:
@@ -79,9 +79,11 @@ android {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
play {
|
play {
|
||||||
java.srcDirs += ['src/minApi23/java']
|
java.srcDirs += ['src/minApi23/java']
|
||||||
|
aidl.srcDirs += ['src/minApi23/aidl']
|
||||||
}
|
}
|
||||||
other {
|
other {
|
||||||
java.srcDirs += ['src/minApi23/java', 'src/github/java']
|
java.srcDirs += ['src/minApi23/java', 'src/github/java']
|
||||||
|
aidl.srcDirs += ['src/minApi23/aidl']
|
||||||
}
|
}
|
||||||
otherLegacy {
|
otherLegacy {
|
||||||
java.srcDirs += ['src/minApi21/java', 'src/github/java']
|
java.srcDirs += ['src/minApi21/java', 'src/github/java']
|
||||||
@@ -202,6 +204,15 @@ dependencies {
|
|||||||
otherImplementation "dev.rikka.shizuku:provider:$shizukuVersion"
|
otherImplementation "dev.rikka.shizuku:provider:$shizukuVersion"
|
||||||
otherImplementation 'org.lsposed.hiddenapibypass:hiddenapibypass:4.3'
|
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)
|
// Compose dependencies - API 23+ (play/other)
|
||||||
def composeBom23 = platform('androidx.compose:compose-bom:2025.12.01')
|
def composeBom23 = platform('androidx.compose:compose-bom:2025.12.01')
|
||||||
def activityVersion23 = "1.12.2"
|
def activityVersion23 = "1.12.2"
|
||||||
|
|||||||
@@ -238,4 +238,4 @@
|
|||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -42,8 +42,6 @@ class Application : Application() {
|
|||||||
UpdateProfileWork.reconfigureUpdater()
|
UpdateProfileWork.reconfigureUpdater()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only register AppChangeReceiver if Per-app Proxy is available
|
|
||||||
// This receiver needs QUERY_ALL_PACKAGES permission to function
|
|
||||||
if (Vendor.isPerAppProxyAvailable()) {
|
if (Vendor.isPerAppProxyAvailable()) {
|
||||||
registerReceiver(
|
registerReceiver(
|
||||||
AppChangeReceiver(),
|
AppChangeReceiver(),
|
||||||
|
|||||||
@@ -3,9 +3,18 @@ package io.nekohasekai.sfa.bg
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import io.nekohasekai.sfa.R
|
||||||
import io.nekohasekai.sfa.database.Settings
|
import io.nekohasekai.sfa.database.Settings
|
||||||
import io.nekohasekai.sfa.ui.profileoverride.PerAppProxyActivity
|
import io.nekohasekai.sfa.ui.profileoverride.PerAppProxyActivity
|
||||||
|
import io.nekohasekai.sfa.vendor.PackageQueryManager
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class AppChangeReceiver : BroadcastReceiver() {
|
class AppChangeReceiver : BroadcastReceiver() {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -17,10 +26,6 @@ class AppChangeReceiver : BroadcastReceiver() {
|
|||||||
intent: Intent,
|
intent: Intent,
|
||||||
) {
|
) {
|
||||||
Log.d(TAG, "onReceive: ${intent.action}")
|
Log.d(TAG, "onReceive: ${intent.action}")
|
||||||
checkUpdate(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkUpdate(intent: Intent) {
|
|
||||||
if (!Settings.perAppProxyEnabled) {
|
if (!Settings.perAppProxyEnabled) {
|
||||||
Log.d(TAG, "per app proxy disabled")
|
Log.d(TAG, "per app proxy disabled")
|
||||||
return
|
return
|
||||||
@@ -29,19 +34,41 @@ class AppChangeReceiver : BroadcastReceiver() {
|
|||||||
Log.d(TAG, "managed mode disabled")
|
Log.d(TAG, "managed mode disabled")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val packageName = intent.dataString?.substringAfter("package:")
|
val pendingResult = goAsync()
|
||||||
if (packageName.isNullOrBlank()) {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
Log.d(TAG, "missing package name in intent")
|
try {
|
||||||
return
|
rescanAllApps()
|
||||||
}
|
} catch (e: Exception) {
|
||||||
val isChinaApp = PerAppProxyActivity.scanChinaPackage(packageName)
|
Log.e(TAG, "Failed to rescan apps", e)
|
||||||
Log.d(TAG, "scan china app result for $packageName: $isChinaApp")
|
withContext(Dispatchers.Main) {
|
||||||
if (isChinaApp) {
|
Toast.makeText(context, R.string.error_title, Toast.LENGTH_SHORT).show()
|
||||||
Settings.perAppProxyManagedList += packageName
|
}
|
||||||
Log.d(TAG, "added to managed list")
|
} finally {
|
||||||
} else {
|
pendingResult.finish()
|
||||||
Settings.perAppProxyManagedList -= packageName
|
}
|
||||||
Log.d(TAG, "removed from managed list")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ package io.nekohasekai.sfa.compose.screen.settings
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.rememberScrollState
|
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.FilterList
|
||||||
import androidx.compose.material.icons.outlined.Route
|
import androidx.compose.material.icons.outlined.Route
|
||||||
import androidx.compose.material.icons.outlined.SmartToy
|
import androidx.compose.material.icons.outlined.SmartToy
|
||||||
|
import androidx.compose.material.icons.outlined.Tune
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
@@ -30,10 +30,14 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.ListItemDefaults
|
import androidx.compose.material3.ListItemDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.RadioButton
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
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.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
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.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import io.nekohasekai.sfa.Application
|
|
||||||
import io.nekohasekai.sfa.R
|
import io.nekohasekai.sfa.R
|
||||||
import io.nekohasekai.sfa.database.Settings
|
import io.nekohasekai.sfa.database.Settings
|
||||||
import io.nekohasekai.sfa.ui.profileoverride.PerAppProxyActivity
|
import io.nekohasekai.sfa.ui.profileoverride.PerAppProxyActivity
|
||||||
import io.nekohasekai.sfa.vendor.Vendor
|
import io.nekohasekai.sfa.vendor.PackageQueryManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
@@ -66,7 +69,58 @@ fun ProfileOverrideScreen(navController: NavController) {
|
|||||||
var perAppProxyEnabled by remember { mutableStateOf(Settings.perAppProxyEnabled) }
|
var perAppProxyEnabled by remember { mutableStateOf(Settings.perAppProxyEnabled) }
|
||||||
var managedModeEnabled by remember { mutableStateOf(Settings.perAppProxyManagedMode) }
|
var managedModeEnabled by remember { mutableStateOf(Settings.perAppProxyManagedMode) }
|
||||||
var isScanning by remember { mutableStateOf(false) }
|
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(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
@@ -158,7 +212,11 @@ fun ProfileOverrideScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Section: Per-App Proxy
|
// Section: Per-App Proxy
|
||||||
val isPerAppProxyAvailable = Vendor.isPerAppProxyAvailable()
|
val canUsePerAppProxy = if (needsPrivilegedQuery) {
|
||||||
|
if (useRootMode) true else isShizukuAvailable
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.per_app_proxy),
|
text = stringResource(R.string.per_app_proxy),
|
||||||
@@ -178,6 +236,52 @@ fun ProfileOverrideScreen(navController: NavController) {
|
|||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Column {
|
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
|
// Enabled toggle
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
@@ -186,19 +290,6 @@ fun ProfileOverrideScreen(navController: NavController) {
|
|||||||
style = MaterialTheme.typography.bodyLarge,
|
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 = {
|
leadingContent = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.FilterList,
|
imageVector = Icons.Outlined.FilterList,
|
||||||
@@ -207,38 +298,40 @@ fun ProfileOverrideScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
if (isPerAppProxyAvailable) {
|
Switch(
|
||||||
if (isScanning) {
|
checked = perAppProxyEnabled,
|
||||||
CircularProgressIndicator(
|
onCheckedChange = { checked ->
|
||||||
modifier = Modifier.size(24.dp),
|
if (checked && needsPrivilegedQuery) {
|
||||||
strokeWidth = 2.dp,
|
if (useRootMode) {
|
||||||
)
|
showRootDialog = true
|
||||||
} else {
|
} else {
|
||||||
Switch(
|
showShizukuDialog = true
|
||||||
checked = perAppProxyEnabled,
|
}
|
||||||
onCheckedChange = { checked ->
|
} else {
|
||||||
perAppProxyEnabled = checked
|
perAppProxyEnabled = checked
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
Settings.perAppProxyEnabled = checked
|
Settings.perAppProxyEnabled = checked
|
||||||
}
|
}
|
||||||
if (checked && managedModeEnabled) {
|
if (checked && managedModeEnabled) {
|
||||||
isScanning = true
|
isScanning = true
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val chinaApps = scanAllChinaApps()
|
val chinaApps = scanAllChinaApps()
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
Settings.perAppProxyManagedList = chinaApps
|
Settings.perAppProxyManagedList = chinaApps
|
||||||
}
|
|
||||||
isScanning = false
|
|
||||||
}
|
}
|
||||||
|
isScanning = false
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
}
|
||||||
}
|
},
|
||||||
}
|
enabled = !isScanning,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.clip(
|
Modifier.clip(
|
||||||
if (perAppProxyEnabled && isPerAppProxyAvailable) {
|
if (needsPrivilegedQuery) {
|
||||||
|
RoundedCornerShape(0.dp)
|
||||||
|
} else if (perAppProxyEnabled && canUsePerAppProxy) {
|
||||||
RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)
|
RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)
|
||||||
} else {
|
} else {
|
||||||
RoundedCornerShape(12.dp)
|
RoundedCornerShape(12.dp)
|
||||||
@@ -250,7 +343,7 @@ fun ProfileOverrideScreen(navController: NavController) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (perAppProxyEnabled && isPerAppProxyAvailable) {
|
if (perAppProxyEnabled && canUsePerAppProxy) {
|
||||||
// Manage entry
|
// Manage entry
|
||||||
val manageEnabled = !managedModeEnabled
|
val manageEnabled = !managedModeEnabled
|
||||||
val disabledAlpha = 0.38f
|
val disabledAlpha = 0.38f
|
||||||
@@ -366,23 +459,199 @@ fun ProfileOverrideScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dialog for Per-app Proxy disabled message
|
// Shizuku dialog
|
||||||
if (showPerAppProxyDialog) {
|
if (showShizukuDialog) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { showPerAppProxyDialog = false },
|
onDismissRequest = { showShizukuDialog = false },
|
||||||
title = {
|
title = {
|
||||||
Text(stringResource(R.string.unavailable))
|
Text(stringResource(R.string.per_app_proxy))
|
||||||
},
|
},
|
||||||
text = {
|
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 = {
|
confirmButton = {
|
||||||
TextButton(
|
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) {
|
private suspend fun scanAllChinaApps(): Set<String> = withContext(Dispatchers.Default) {
|
||||||
val packageManagerFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
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 {
|
} else {
|
||||||
@Suppress("DEPRECATION")
|
@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) {
|
val installedPackages = PackageQueryManager.getInstalledPackages(packageManagerFlags)
|
||||||
Application.packageManager.getInstalledPackages(
|
|
||||||
PackageManager.PackageInfoFlags.of(packageManagerFlags.toLong())
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
Application.packageManager.getInstalledPackages(packageManagerFlags)
|
|
||||||
}
|
|
||||||
|
|
||||||
val chinaApps = mutableSetOf<String>()
|
val chinaApps = mutableSetOf<String>()
|
||||||
installedPackages.map { packageInfo ->
|
installedPackages.map { packageInfo ->
|
||||||
async {
|
async {
|
||||||
if (PerAppProxyActivity.scanChinaPackage(packageInfo.packageName)) {
|
if (PerAppProxyActivity.scanChinaPackage(packageInfo)) {
|
||||||
synchronized(chinaApps) {
|
synchronized(chinaApps) {
|
||||||
chinaApps.add(packageInfo.packageName)
|
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_LIST = "per_app_proxy_list"
|
||||||
const val PER_APP_PROXY_MANAGED_MODE = "per_app_proxy_managed_mode"
|
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_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"
|
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 perAppProxyManagedMode by dataStore.boolean(SettingsKey.PER_APP_PROXY_MANAGED_MODE) { false }
|
||||||
var perAppProxyManagedList by dataStore.stringSet(SettingsKey.PER_APP_PROXY_MANAGED_LIST) { emptySet() }
|
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> {
|
fun getEffectivePerAppProxyList(): Set<String> {
|
||||||
return if (perAppProxyManagedMode) {
|
return if (perAppProxyManagedMode) {
|
||||||
perAppProxyList union perAppProxyManagedList
|
perAppProxyList union perAppProxyManagedList
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import io.nekohasekai.sfa.databinding.ViewVpnAppItemBinding
|
|||||||
import io.nekohasekai.sfa.ktx.dp2px
|
import io.nekohasekai.sfa.ktx.dp2px
|
||||||
import io.nekohasekai.sfa.ktx.toStringIterator
|
import io.nekohasekai.sfa.ktx.toStringIterator
|
||||||
import io.nekohasekai.sfa.ui.shared.AbstractActivity
|
import io.nekohasekai.sfa.ui.shared.AbstractActivity
|
||||||
|
import io.nekohasekai.sfa.vendor.PackageQueryManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -139,13 +140,7 @@ class VPNScanActivity : AbstractActivity<ActivityVpnScanBinding>() {
|
|||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
PackageManager.GET_UNINSTALLED_PACKAGES
|
PackageManager.GET_UNINSTALLED_PACKAGES
|
||||||
}
|
}
|
||||||
val installedPackages =
|
val installedPackages = PackageQueryManager.getInstalledPackages(flag)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
packageManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(flag.toLong()))
|
|
||||||
} else {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
packageManager.getInstalledPackages(flag)
|
|
||||||
}
|
|
||||||
val vpnAppList =
|
val vpnAppList =
|
||||||
installedPackages.filter {
|
installedPackages.filter {
|
||||||
it.services?.any { it.permission == Manifest.permission.BIND_VPN_SERVICE && it.applicationInfo != null }
|
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.databinding.ViewAppListItemBinding
|
||||||
import io.nekohasekai.sfa.ktx.clipboardText
|
import io.nekohasekai.sfa.ktx.clipboardText
|
||||||
import io.nekohasekai.sfa.ui.shared.AbstractActivity
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
@@ -80,6 +81,8 @@ class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
|
|||||||
val applicationLabel by lazy {
|
val applicationLabel by lazy {
|
||||||
appInfo.loadLabel(packageManager).toString()
|
appInfo.loadLabel(packageManager).toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val info: PackageInfo get() = packageInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var adapter: ApplicationAdapter
|
private lateinit var adapter: ApplicationAdapter
|
||||||
@@ -91,19 +94,6 @@ class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
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)
|
setTitle(R.string.per_app_proxy)
|
||||||
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(binding.appList) { view, windowInsets ->
|
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)
|
binding.perAppProxyMode.setText(R.string.per_app_proxy_mode_exclude_description)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reloadApplicationList()
|
if (!reloadApplicationList()) {
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
filterApplicationList()
|
filterApplicationList()
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
adapter = ApplicationAdapter(displayPackages)
|
adapter = ApplicationAdapter(displayPackages)
|
||||||
@@ -139,25 +131,31 @@ class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun reloadApplicationList() {
|
private suspend fun reloadApplicationList(): Boolean {
|
||||||
val packageManagerFlags =
|
val packageManagerFlags =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
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 {
|
} else {
|
||||||
@Suppress("DEPRECATION")
|
@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 =
|
val installedPackages = try {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
PackageQueryManager.getInstalledPackages(packageManagerFlags)
|
||||||
packageManager.getInstalledPackages(
|
} catch (e: PrivilegedAccessRequiredException) {
|
||||||
PackageManager.PackageInfoFlags.of(
|
withContext(Dispatchers.Main) {
|
||||||
packageManagerFlags.toLong(),
|
Toast.makeText(
|
||||||
),
|
this@PerAppProxyActivity,
|
||||||
)
|
R.string.privileged_access_required,
|
||||||
} else {
|
Toast.LENGTH_LONG
|
||||||
@Suppress("DEPRECATION")
|
).show()
|
||||||
packageManager.getInstalledPackages(packageManagerFlags)
|
finish()
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
val packages = mutableListOf<PackageCache>()
|
val packages = mutableListOf<PackageCache>()
|
||||||
for (packageInfo in installedPackages) {
|
for (packageInfo in installedPackages) {
|
||||||
if (packageInfo.packageName == packageName) continue
|
if (packageInfo.packageName == packageName) continue
|
||||||
@@ -173,6 +171,7 @@ class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
|
|||||||
}
|
}
|
||||||
this.packages = packages
|
this.packages = packages
|
||||||
this.selectedUIDs = selectedUIDs
|
this.selectedUIDs = selectedUIDs
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun filterApplicationList(selectedUIDs: Set<Int> = this.selectedUIDs) {
|
private fun filterApplicationList(selectedUIDs: Set<Int> = this.selectedUIDs) {
|
||||||
@@ -592,7 +591,7 @@ class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
|
|||||||
val progressInt = AtomicInteger()
|
val progressInt = AtomicInteger()
|
||||||
currentPackages.map { it ->
|
currentPackages.map { it ->
|
||||||
async {
|
async {
|
||||||
if (scanChinaPackage(it.packageName)) {
|
if (scanChinaPackage(it.info)) {
|
||||||
foundApps[it.packageName] = it
|
foundApps[it.packageName] = it
|
||||||
}
|
}
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
@@ -729,36 +728,17 @@ class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
|
|||||||
("(" + chinaAppPrefixList.joinToString("|").replace(".", "\\.") + ").*").toRegex()
|
("(" + chinaAppPrefixList.joinToString("|").replace(".", "\\.") + ").*").toRegex()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun scanChinaPackage(packageName: String): Boolean {
|
fun scanChinaPackage(packageInfo: PackageInfo): Boolean {
|
||||||
|
val packageName = packageInfo.packageName
|
||||||
skipPrefixList.forEach {
|
skipPrefixList.forEach {
|
||||||
if (packageName == it || packageName.startsWith("$it.")) return false
|
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)) {
|
if (packageName.matches(chinaAppRegex)) {
|
||||||
Log.d("PerAppProxyActivity", "Match package name: $packageName")
|
Log.d("PerAppProxyActivity", "Match package name: $packageName")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
try {
|
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
|
val appInfo = packageInfo.applicationInfo ?: return false
|
||||||
packageInfo.services?.forEach {
|
packageInfo.services?.forEach {
|
||||||
if (it.name.matches(chinaAppRegex)) {
|
if (it.name.matches(chinaAppRegex)) {
|
||||||
|
|||||||
@@ -180,9 +180,16 @@
|
|||||||
<string name="location_permission_background_description">在 Android 10 及更高版本中,需要<strong>后台位置</strong>权限。选择<strong>始终允许</strong>以授予权限。</string>
|
<string name="location_permission_background_description">在 Android 10 及更高版本中,需要<strong>后台位置</strong>权限。选择<strong>始终允许</strong>以授予权限。</string>
|
||||||
<string name="notification_permission_title">通知权限</string>
|
<string name="notification_permission_title">通知权限</string>
|
||||||
<string name="notification_permission_required_description">sing-box 无法在没有发送通知权限的情况下显示实时网速。请授予权限或禁用实时网速通知后再启动服务。</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_package_query_mode">模式</string>
|
||||||
<string name="per_app_proxy_disabled_message">Google Play 拒绝允许我们使用 QUERY_ALL_PACKAGES 权限(同时不禁止其他类似应用这样做),而这是列出应用程序所必需的。</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_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_connections">连接</string>
|
||||||
<string name="title_others">其他</string>
|
<string name="title_others">其他</string>
|
||||||
<string name="title_experimental_features">实验性功能</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_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="shizuku_description">Shizuku allows apps to use system APIs directly with higher privileges</string>
|
||||||
<string name="get_shizuku">Get Shizuku</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_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="silent_install_verify_failed">%s is not available or permission denied</string>
|
||||||
<string name="auto_update">Auto Update</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="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_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="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_package_query_mode">Mode</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_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_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="dashboard_items">Dashboard Items</string>
|
||||||
<string name="reset_order">Reset order</string>
|
<string name="reset_order">Reset order</string>
|
||||||
<string name="reset">Reset</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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<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" />
|
|
||||||
|
|
||||||
<application>
|
<application>
|
||||||
<provider
|
<provider
|
||||||
@@ -13,6 +8,10 @@
|
|||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:multiprocess="false"
|
android:multiprocess="false"
|
||||||
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".vendor.RootPackageManagerService"
|
||||||
|
android:exported="false" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -95,10 +95,6 @@ object Vendor : VendorInterface {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isPerAppProxyAvailable(): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun supportsTrackSelection(): Boolean {
|
override fun supportsTrackSelection(): Boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,10 +95,6 @@ object Vendor : VendorInterface {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isPerAppProxyAvailable(): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun supportsTrackSelection(): Boolean {
|
override fun supportsTrackSelection(): Boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,4 +7,17 @@
|
|||||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
tools:node="remove" />
|
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>
|
||||||
|
|||||||
@@ -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 {
|
override fun supportsTrackSelection(): Boolean {
|
||||||
// Play Store doesn't support track selection
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun checkUpdateAsync(): UpdateInfo? {
|
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
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user