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

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