From 27097ddf21b2cb711bc52fff2b805073148c8d49 Mon Sep 17 00:00:00 2001 From: Willie Shen Date: Tue, 30 Jun 2026 18:33:30 +0800 Subject: [PATCH] Fixes #718 where the Shizuku Installer would only install apps to the profile where Shizuku is running, ignoring the profile Droid-ify is running in --- app/build.gradle.kts | 13 +- app/src/main/AndroidManifest.xml | 10 + .../installers/shizuku/ShizukuInstaller.kt | 117 +++--- .../shizuku_apk_installer/ShizukuWorker.kt | 353 ++++++++++++++++++ app/src/main/res/xml/file_paths.xml | 4 + 5 files changed, 435 insertions(+), 62 deletions(-) create mode 100644 app/src/main/kotlin/dev/re7gog/shizuku_apk_installer/ShizukuWorker.kt create mode 100644 app/src/main/res/xml/file_paths.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 62881f182..8dc776df9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -7,6 +7,7 @@ plugins { alias(libs.plugins.kotlin.serialization) alias(libs.plugins.kotlin.parcelize) alias(libs.plugins.compose) + id("dev.rikka.tools.refine") version "4.4.0" } android { @@ -18,7 +19,7 @@ android { defaultConfig { applicationId = "com.looker.droidify" - minSdk = 23 + minSdk = 26 versionName = latestVersionName versionCode = 730 @@ -188,6 +189,16 @@ dependencies { androidTestImplementation(libs.bundles.test.android) kspAndroidTest(libs.hilt.compiler) + // shizuku_apk_installer dependencies + implementation("dev.rikka.tools.refine:runtime:4.4.0") + implementation("dev.rikka.shizuku:api:13.1.5") + implementation("dev.rikka.shizuku:provider:13.1.5") + implementation("io.github.iamr0s:Dhizuku-API:2.5.4") + + implementation("dev.rikka.tools.refine:runtime:4.4.0") + compileOnly("dev.rikka.hidden:stub:4.4.0") + implementation("org.lsposed.hiddenapibypass:hiddenapibypass:6.1") + // debugImplementation(libs.leakcanary) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1709004be..8b5645a1c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -214,6 +214,16 @@ android:authorities="${applicationId}.androidx-startup" tools:node="remove" /> + + + + - var sessionId: String? = null - val file = Cache.getReleaseFile(context, installItem.installFileName) - try { - val fileSize = file.length() - if (fileSize == 0L) { - cont.cancel() - error("File is not valid: Size ${file.size}") - } - if (cont.isCompleted) return@suspendCancellableCoroutine - val installerPackage = context.packageName - file.inputStream().use { - val createCommand = - if (SdkCheck.isNougat) { - "pm install-create --user current -i $installerPackage -S $fileSize" - } else { - "pm install-create -i $installerPackage -S $fileSize" - } - val createResult = exec(createCommand) - sessionId = SESSION_ID_REGEX.find(createResult.out)?.value - ?: run { - cont.cancel() - error("Failed to create install session") - } - if (cont.isCompleted) return@suspendCancellableCoroutine +class ShizukuInstaller(private val context: Context) : Installer { + private val appContext = context.applicationContext + private val adapter = ShizukuWorkerAdapter(appContext) - val writeResult = exec("pm install-write -S $fileSize $sessionId base -", it) - if (writeResult.resultCode != 0) { - cont.cancel() - error("Failed to write APK to session $sessionId") + override suspend fun install(installItem: InstallItem): InstallState { + return withContext(Dispatchers.IO) { + try { + val permissionStatus = adapter.checkPermission() + if (!permissionStatus.startsWith("granted")) { + return@withContext InstallState.Failed } - if (cont.isCompleted) return@suspendCancellableCoroutine - val commitResult = exec("pm install-commit $sessionId") - if (commitResult.resultCode != 0) { - cont.cancel() - error("Failed to commit install session $sessionId") + val file = Cache.getReleaseFile(appContext, installItem.installFileName) + if (!file.exists() || file.length() == 0L) { + return@withContext InstallState.Failed } - if (cont.isCompleted) return@suspendCancellableCoroutine - cont.resume(InstallState.Installed) + + val result = adapter.installApkFile( + file = file, + installerPackageName = appContext.packageName + ) + + when (result) { + PackageInstaller.STATUS_SUCCESS -> InstallState.Installed + else -> InstallState.Failed + } + } catch (e: Exception) { + InstallState.Failed } - } catch (_: Exception) { - if (sessionId != null) exec("pm install-abandon $sessionId") - cont.resume(InstallState.Failed) } } override suspend fun uninstall(packageName: PackageName) = context.uninstallPackage(packageName) - override fun close() = Unit - - private data class ShellResult(val resultCode: Int, val out: String) - - private fun exec(command: String, stdin: InputStream? = null): ShellResult { - val process = rikka.shizuku.Shizuku.newProcess(arrayOf("sh", "-c", command), null, null) - if (stdin != null) { - process.outputStream.use { stdin.copyTo(it) } - } - val output = process.inputStream.bufferedReader().use(BufferedReader::readText) - val resultCode = process.waitFor() - return ShellResult(resultCode, output) + override fun close() { + adapter.exit() } } diff --git a/app/src/main/kotlin/dev/re7gog/shizuku_apk_installer/ShizukuWorker.kt b/app/src/main/kotlin/dev/re7gog/shizuku_apk_installer/ShizukuWorker.kt new file mode 100644 index 000000000..36405759b --- /dev/null +++ b/app/src/main/kotlin/dev/re7gog/shizuku_apk_installer/ShizukuWorker.kt @@ -0,0 +1,353 @@ +package dev.re7gog.shizuku_apk_installer + +import android.annotation.SuppressLint +import android.content.Context +import android.content.IIntentReceiver +import android.content.IIntentSender +import android.content.Intent +import android.content.IntentSender +import android.content.pm.IPackageInstaller +import android.content.pm.IPackageInstallerSession +import android.content.pm.IPackageManager +import android.content.pm.PackageInstaller +import android.content.pm.PackageInstallerHidden +import android.content.pm.PackageManager +import android.content.pm.PackageManagerHidden +import android.os.Build +import android.os.Bundle +import android.os.IBinder +import android.os.IInterface +import android.os.Process +import android.os.RemoteException +import android.system.Os +import android.util.Log +import androidx.core.net.toUri +import com.rosan.dhizuku.api.Dhizuku +import com.rosan.dhizuku.api.DhizukuRequestPermissionListener +import com.rosan.dhizuku.shared.DhizukuVariables +import dev.rikka.tools.refine.Refine +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.withContext +import org.lsposed.hiddenapibypass.HiddenApiBypass +import rikka.shizuku.Shizuku +import rikka.shizuku.ShizukuBinderWrapper +import rikka.shizuku.ShizukuProvider +import rikka.shizuku.SystemServiceHelper +import rikka.sui.Sui +import java.io.IOException +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class ShizukuWorker(private val appContext: Context) { + private var isBinderAvailable = false + private val requestPermissionCode = (1000..2000).random() + private val requestPermissionMutex by lazy { Mutex(locked = true) } + private var permissionGranted = false + private var isRoot = false + private var fakeInstallSource = "" + + private var dInitSucceeded = false + private val dRequestPermissionMutex by lazy { Mutex(locked = true) } + private var dPermissionGranted = false + + private val binderReceivedListener = Shizuku.OnBinderReceivedListener { isBinderAvailable = true } + private val binderDeadListener = Shizuku.OnBinderDeadListener { isBinderAvailable = false } + private val requestPermissionResultListener = + Shizuku.OnRequestPermissionResultListener { requestCode: Int, grantResult: Int -> + if (requestCode == requestPermissionCode) { + permissionGranted = grantResult == PackageManager.PERMISSION_GRANTED + requestPermissionMutex.unlock() + } + } + + private val contextD by lazy { + appContext.createPackageContext( + Dhizuku.getOwnerComponent().packageName, Context.CONTEXT_IGNORE_SECURITY + ) + } + + init { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + HiddenApiBypass.addHiddenApiExemptions("Landroid/content", "Landroid/os") + dInitSucceeded = Dhizuku.init(appContext) + if (!dInitSucceeded) { + val isSui = Sui.init(appContext.packageName) + if (!isSui) { // Not sure if it's needed + ShizukuProvider.enableMultiProcessSupport(false) + ShizukuProvider.requestBinderForNonProviderProcess(appContext) + } + Shizuku.addBinderReceivedListenerSticky(binderReceivedListener) + Shizuku.addBinderDeadListener(binderDeadListener) + Shizuku.addRequestPermissionResultListener(requestPermissionResultListener) + } + } + + fun exit() { + if (dInitSucceeded) return + Shizuku.removeBinderReceivedListener(binderReceivedListener) + Shizuku.removeBinderDeadListener(binderDeadListener) + Shizuku.removeRequestPermissionResultListener(requestPermissionResultListener) + } + + private suspend fun checkShizukuPermission(): String { + return if (Shizuku.isPreV11()) { + "old_shizuku" + } else if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { + if (!registerUidObserverPermissionLimitedCheck()) { + "granted_" + if (isRoot) "root" else "adb" + } else "old_android_with_adb" + } else if (Shizuku.shouldShowRequestPermissionRationale()) { // "Deny and don't ask again" + "denied" + } else { + Shizuku.requestPermission(requestPermissionCode) + requestPermissionMutex.lock() + if (!registerUidObserverPermissionLimitedCheck()) { + if (permissionGranted) { + "granted_" + if (isRoot) "root" else "adb" + } else "denied" + } else "old_android_with_adb" + } + } + + suspend fun checkPermission(): String { + return if (!isBinderAvailable and !dInitSucceeded) { + "services_not_found" + } else if (dInitSucceeded) { + if (Dhizuku.isPermissionGranted()) + "granted_owner" + else { + Dhizuku.requestPermission(object : DhizukuRequestPermissionListener() { + @Throws(RemoteException::class) + override fun onRequestPermission(grantResult: Int) { + dPermissionGranted = grantResult == PackageManager.PERMISSION_GRANTED + dRequestPermissionMutex.unlock() + } + }) + dRequestPermissionMutex.lock() + if (dPermissionGranted) + "granted_owner" + else if (isBinderAvailable) + checkShizukuPermission() + else + "denied" + } + } else checkShizukuPermission() + } + + /** + * Android 8.0 with ADB lacks IActivityManager#registerUidObserver permission, + * so we can't install apps without activity + */ + private fun registerUidObserverPermissionLimitedCheck(): Boolean { + isRoot = Shizuku.getUid() == 0 + return !isRoot and (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) + } + + // Thanks to https://github.com/LSPosed/LSPatch and https://gitlab.com/AuroraOSS/AuroraStore + + private fun IBinder.wrap() = ShizukuBinderWrapper(this) + private fun IInterface.asShizukuBinder() = this.asBinder().wrap() + + private fun IBinder.dwrap() = Dhizuku.binderWrapper(this) + private fun IInterface.asDhizukuBinder() = this.asBinder().dwrap() + + private val iPackageInstaller: IPackageInstaller by lazy { + val iPackageManager = IPackageManager.Stub.asInterface( + SystemServiceHelper.getSystemService("package").wrap()) + IPackageInstaller.Stub.asInterface(iPackageManager.packageInstaller.asShizukuBinder()) + } + + private val iPackageInstallerD: IPackageInstaller by lazy { + val iPackageManager = IPackageManager.Stub.asInterface( + SystemServiceHelper.getSystemService("package").dwrap()) + IPackageInstaller.Stub.asInterface(iPackageManager.packageInstaller.asDhizukuBinder()) + } + + private val packageInstaller: PackageInstaller by lazy { + val installerPackageName = if (fakeInstallSource == "") + appContext.packageName else fakeInstallSource + val userId = if (!isRoot) Process.myUserHandle().hashCode() else 0 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + Refine.unsafeCast(PackageInstallerHidden( + iPackageInstaller, installerPackageName, appContext.attributionTag, userId)) + } else { + Refine.unsafeCast( + PackageInstallerHidden(iPackageInstaller, installerPackageName, userId)) + } + /* + DEPRECATED + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + ... + else { + Refine.unsafeCast(PackageInstallerHidden( + appContext, appContext.packageManager, iPackageInstaller, installerPackageName, userId)) + } + */ + } + + private val packageInstallerD: PackageInstaller by lazy { + val installerPackageName = DhizukuVariables.OFFICIAL_PACKAGE_NAME + val userId = Os.getuid() / 100000 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + Refine.unsafeCast(PackageInstallerHidden( + iPackageInstallerD, installerPackageName, contextD.attributionTag, userId)) + } else { + Refine.unsafeCast( + PackageInstallerHidden(iPackageInstallerD, installerPackageName, userId)) + } + } + + private val sessionParams: PackageInstaller.SessionParams by lazy { + val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL) + var flags = Refine.unsafeCast(params).installFlags + + flags = flags or PackageManagerHidden.INSTALL_ALLOW_TEST or PackageManagerHidden.INSTALL_REPLACE_EXISTING + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + flags = flags or PackageManagerHidden.INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK + } + + Refine.unsafeCast(params).installFlags = flags + params + } + + private fun createPackageInstallerSession(): PackageInstaller.Session { + val sessionId = packageInstaller.createSession(sessionParams) + val iSession = IPackageInstallerSession.Stub.asInterface( + iPackageInstaller.openSession(sessionId).asShizukuBinder()) + return Refine.unsafeCast(PackageInstallerHidden.SessionHidden(iSession)) + } + + private fun createPackageInstallerSessionD(): PackageInstaller.Session { + val sessionId = packageInstallerD.createSession(sessionParams) + val iSession = IPackageInstallerSession.Stub.asInterface( + iPackageInstallerD.openSession(sessionId).asDhizukuBinder()) + return Refine.unsafeCast(PackageInstallerHidden.SessionHidden(iSession)) + } + + private fun createPackageInstallerSessionUniversal(): PackageInstaller.Session { + return if (dInitSucceeded) + createPackageInstallerSessionD() + else + createPackageInstallerSession() + } + + /** + * Install a list of APK splits (AAB) using their URIs. + * The permission must have already been checked! + * @param fakeInstallSource set install source app package name + */ + suspend fun installAPKs(apkURIs: List, fakeInstallSource: String = ""): Int { + if (!dInitSucceeded) isRoot = Shizuku.getUid() == 0 + this.fakeInstallSource = fakeInstallSource + var status = PackageInstaller.STATUS_FAILURE + withContext(Dispatchers.IO) { + runCatching { + createPackageInstallerSessionUniversal().use { session -> + apkURIs.forEachIndexed { index, uriString -> + val uri = uriString.toUri() + val stream = (if (dInitSucceeded) + contextD.contentResolver.openInputStream(uri) + else + appContext.contentResolver.openInputStream(uri)) ?: throw IOException("Cannot open input stream") + stream.use { + session.openWrite("$index.apk", 0, stream.available().toLong()).use { + stream.copyTo(it) + session.fsync(it) + } + } + } + var result: Intent? = null + suspendCoroutine { cont -> + val adapter = IntentSenderHelper.IIntentSenderAdaptor { intent -> + result = intent + cont.resume(Unit) + } + val intentSender = IntentSenderHelper.newIntentSender(adapter) + session.commit(intentSender) + } + result?.let { + status = it.getIntExtra( + PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) + val message = it.getStringExtra( + PackageInstaller.EXTRA_STATUS_MESSAGE) ?: "No message" + Log.i("shizuku_apk_installer", "Package installer result: $message") + } ?: throw IOException("Intent is null") + } + }.onFailure { + val message = it.message + "\n" + it.stackTraceToString() + Log.e("shizuku_apk_installer", "Installing error: $message") + } + } + return status + } + + /** + * Uninstall a package (app) by its name. + * The permission must have already been checked! + * android.permission.REQUEST_DELETE_PACKAGES is not needed. + */ + @SuppressLint("MissingPermission") + suspend fun uninstallPackage(packageName: String): Int { + var status = PackageInstaller.STATUS_FAILURE + withContext(Dispatchers.IO) { + runCatching { + var result: Intent? = null + suspendCoroutine { cont -> + val adapter = IntentSenderHelper.IIntentSenderAdaptor { intent -> + result = intent + cont.resume(Unit) + } + val intentSender = IntentSenderHelper.newIntentSender(adapter) + if (dInitSucceeded) + packageInstallerD.uninstall(packageName, intentSender) + else + packageInstaller.uninstall(packageName, intentSender) + } + result?.let { + status = it.getIntExtra( + PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) + val message = it.getStringExtra( + PackageInstaller.EXTRA_STATUS_MESSAGE) ?: "No message" + Log.i("shizuku_apk_installer", "Package uninstaller result: $message") + } ?: throw IOException("Intent is null") + }.onFailure { + val message = it.message + "\n" + it.stackTraceToString() + Log.e("shizuku_apk_installer", "Uninstalling error: $message") + } + } + return status + } +} + +object IntentSenderHelper { + fun newIntentSender(binder: IIntentSender): IntentSender { + return IntentSender::class.java.getConstructor(IIntentSender::class.java).newInstance(binder) + } + + class IIntentSenderAdaptor(private val listener: (Intent) -> Unit) : IIntentSender.Stub() { + override fun send( + code: Int, + intent: Intent, + resolvedType: String?, + finishedReceiver: IIntentReceiver?, + requiredPermission: String?, + options: Bundle? + ): Int { + listener(intent) + return 0 + } + + override fun send( + code: Int, + intent: Intent, + resolvedType: String?, + whitelistToken: IBinder?, + finishedReceiver: IIntentReceiver?, + requiredPermission: String?, + options: Bundle? + ) { + listener(intent) + } + } +} diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml new file mode 100644 index 000000000..d2e2c8dea --- /dev/null +++ b/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,4 @@ + + + +