From 7e899ff8eba1ee09db3143f19dd46834a7e3a0c1 Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Fri, 26 Jan 2024 18:28:14 +0530 Subject: [PATCH] IInstaller: Support installing shared libraries via supported installers Session, Shizuku and Root Installer are supported for now. Native and Aurora Services have been deprecated and won't be supported. App Manager might support it or it will be deprecated as well (if it doesn't). Signed-off-by: Aayush Gupta --- .../store/data/activity/InstallActivity.kt | 2 +- .../store/data/installer/RootInstaller.kt | 16 ++- .../store/data/installer/SessionInstaller.kt | 134 ++++++++++++++---- .../data/installer/SessionInstallerBase.kt | 84 ----------- .../store/data/installer/ShizukuInstaller.kt | 108 ++++++++++---- .../data/receiver/PackageManagerReceiver.kt | 6 + .../aurora/store/data/work/DownloadWorker.kt | 8 +- .../java/com/aurora/store/util/PackageUtil.kt | 14 +- 8 files changed, 220 insertions(+), 152 deletions(-) delete mode 100644 app/src/main/java/com/aurora/store/data/installer/SessionInstallerBase.kt diff --git a/app/src/main/java/com/aurora/store/data/activity/InstallActivity.kt b/app/src/main/java/com/aurora/store/data/activity/InstallActivity.kt index 7630a834d..860670491 100644 --- a/app/src/main/java/com/aurora/store/data/activity/InstallActivity.kt +++ b/app/src/main/java/com/aurora/store/data/activity/InstallActivity.kt @@ -34,7 +34,7 @@ class InstallActivity : AppCompatActivity() { override fun onProgressChanged(sessionId: Int, progress: Float) {} override fun onFinished(sessionId: Int, success: Boolean) { - if (sessionInstaller.sessionId == sessionId) { + if (sessionInstaller.parentSessionId == sessionId) { Log.i(TAG, "Install finished with status code: $success") finish() } diff --git a/app/src/main/java/com/aurora/store/data/installer/RootInstaller.kt b/app/src/main/java/com/aurora/store/data/installer/RootInstaller.kt index 7c320edcf..969d8a111 100644 --- a/app/src/main/java/com/aurora/store/data/installer/RootInstaller.kt +++ b/app/src/main/java/com/aurora/store/data/installer/RootInstaller.kt @@ -24,9 +24,9 @@ import com.aurora.store.R import com.aurora.store.data.event.InstallerEvent import com.aurora.store.data.room.download.Download import com.aurora.store.util.Log +import com.aurora.store.util.PackageUtil.isSharedLibraryInstalled import com.topjohnwu.superuser.Shell import org.greenrobot.eventbus.EventBus -import java.io.File import java.util.regex.Pattern class RootInstaller(context: Context) : InstallerBase(context) { @@ -36,7 +36,13 @@ class RootInstaller(context: Context) : InstallerBase(context) { Log.i("${download.packageName} already queued") } else { if (Shell.getShell().isRoot) { - xInstall(download.packageName, getFiles(download.packageName, download.versionCode)) + download.sharedLibs.forEach { + // Shared library packages cannot be updated + if (!isSharedLibraryInstalled(context, it.packageName, it.versionCode)) { + xInstall(download.packageName, download.versionCode, it.packageName) + } + } + xInstall(download.packageName, download.versionCode) } else { postError( download.packageName, @@ -48,10 +54,10 @@ class RootInstaller(context: Context) : InstallerBase(context) { } } - private fun xInstall(packageName: String, files: List) { + private fun xInstall(packageName: String, versionCode: Int, sharedLibPkgName: String = "") { var totalSize = 0 - for (file in files) + for (file in getFiles(packageName, versionCode, sharedLibPkgName)) totalSize += file.length().toInt() val result: Shell.Result = @@ -67,7 +73,7 @@ class RootInstaller(context: Context) : InstallerBase(context) { if (found) { val sessionId = sessionIdMatcher.group(1)?.toInt() if (Shell.getShell().isRoot && sessionId != null) { - for (file in files) { + for (file in getFiles(packageName, versionCode, sharedLibPkgName)) { Shell.cmd("cat \"${file.absoluteFile}\" | pm install-write -S ${file.length()} $sessionId \"${file.name}\"") .exec() } diff --git a/app/src/main/java/com/aurora/store/data/installer/SessionInstaller.kt b/app/src/main/java/com/aurora/store/data/installer/SessionInstaller.kt index c736e1f52..1a0f2dc4f 100644 --- a/app/src/main/java/com/aurora/store/data/installer/SessionInstaller.kt +++ b/app/src/main/java/com/aurora/store/data/installer/SessionInstaller.kt @@ -20,23 +20,31 @@ package com.aurora.store.data.installer +import android.app.PendingIntent import android.content.Context +import android.content.Intent +import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller.PACKAGE_SOURCE_STORE import android.content.pm.PackageInstaller.SessionParams import android.content.pm.PackageManager +import android.os.Process import com.aurora.extensions.isNAndAbove import com.aurora.extensions.isOAndAbove import com.aurora.extensions.isSAndAbove import com.aurora.extensions.isTAndAbove import com.aurora.extensions.isUAndAbove +import com.aurora.store.data.receiver.InstallerStatusReceiver import com.aurora.store.data.room.download.Download import com.aurora.store.util.Log +import com.aurora.store.util.PackageUtil.isSharedLibraryInstalled import kotlin.properties.Delegates +class SessionInstaller(context: Context) : InstallerBase(context) { -class SessionInstaller(context: Context) : SessionInstallerBase(context) { + var parentSessionId by Delegates.notNull() - var sessionId by Delegates.notNull() + private val packageInstaller = context.packageManager.packageInstaller + private val sessionIdMap = mutableMapOf() override fun install(download: Download) { if (isAlreadyQueued(download.packageName)) { @@ -44,35 +52,109 @@ class SessionInstaller(context: Context) : SessionInstallerBase(context) { } else { Log.i("Received session install request for ${download.packageName}") - val packageInstaller = context.packageManager.packageInstaller - val sessionParams = SessionParams(SessionParams.MODE_FULL_INSTALL).apply { - setAppPackageName(download.packageName) - if (isOAndAbove()) { - setInstallReason(PackageManager.INSTALL_REASON_USER) - } - if (isNAndAbove()) { - setOriginatingUid(android.os.Process.myUid()) - } - if (isSAndAbove()) { - setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED) - } - if (isTAndAbove()) { - setPackageSource(PACKAGE_SOURCE_STORE) - } - if (isUAndAbove()) { - setInstallerPackageName(context.packageName) + val callback = object : PackageInstaller.SessionCallback() { + override fun onCreated(sessionId: Int) {} + + override fun onBadgingChanged(sessionId: Int) {} + + override fun onActiveChanged(sessionId: Int, active: Boolean) {} + + override fun onProgressChanged(sessionId: Int, progress: Float) {} + + override fun onFinished(sessionId: Int, success: Boolean) { + if (sessionId in sessionIdMap.keys && success) { + sessionIdMap.remove(sessionId) + if (sessionIdMap.isNotEmpty()) { + val nextSession = sessionIdMap.keys.first() + commitInstall(sessionIdMap.getValue(nextSession), nextSession) + } else { + packageInstaller.unregisterSessionCallback(this) + } + } } } - sessionId = packageInstaller.createSession(sessionParams) - val session = packageInstaller.openSession(sessionId) + download.sharedLibs.forEach { + // Shared library packages cannot be updated + if (!isSharedLibraryInstalled(context, it.packageName, it.versionCode)) { + stageInstall(download.packageName, download.versionCode, it.packageName) + } + } + stageInstall(download.packageName, download.versionCode) - xInstall( - sessionId, - session, - download.packageName, - getFiles(download.packageName, download.versionCode) + if (sessionIdMap.size > 1) packageInstaller.registerSessionCallback(callback) + commitInstall( + sessionIdMap.getValue(sessionIdMap.keys.first()), + sessionIdMap.keys.first() ) } } + + private fun stageInstall(packageName: String, versionCode: Int, sharedLibPkgName: String = "") { + val packageInstaller = context.packageManager.packageInstaller + + val sessionParams = SessionParams(SessionParams.MODE_FULL_INSTALL).apply { + setAppPackageName(sharedLibPkgName.ifBlank { packageName }) + if (isOAndAbove()) { + setInstallReason(PackageManager.INSTALL_REASON_USER) + } + if (isNAndAbove()) { + setOriginatingUid(Process.myUid()) + } + if (isSAndAbove()) { + setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED) + } + if (isTAndAbove()) { + setPackageSource(PACKAGE_SOURCE_STORE) + } + if (isUAndAbove()) { + setInstallerPackageName(context.packageName) + } + } + val sessionId = packageInstaller.createSession(sessionParams) + val session = packageInstaller.openSession(sessionId) + + try { + Log.i("Writing splits to session for $packageName") + getFiles(packageName, versionCode, sharedLibPkgName).forEach { + it.inputStream().use { input -> + session.openWrite("${sharedLibPkgName.ifBlank { packageName }}_${System.currentTimeMillis()}", 0, -1).use { output -> + input.copyTo(output) + session.fsync(output) + } + } + } + + // Add session to list of staged sessions + sessionIdMap[sessionId] = packageName + if (sharedLibPkgName.isBlank()) parentSessionId = sessionId + } catch (exception: Exception) { + session.abandon() + removeFromInstallQueue(packageName) + postError(packageName, exception.localizedMessage, exception.stackTraceToString()) + } + } + + private fun commitInstall(packageName: String, sessionId: Int) { + Log.i("Starting install session for $packageName") + val session = packageInstaller.openSession(sessionId) + session.commit(getCallBackIntent(packageName).intentSender) + session.close() + } + + private fun getCallBackIntent(packageName: String): PendingIntent { + val callBackIntent = Intent(context, InstallerStatusReceiver::class.java).apply { + action = InstallerStatusReceiver.ACTION_INSTALL_STATUS + setPackage(context.packageName) + putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName) + addFlags(Intent.FLAG_RECEIVER_FOREGROUND) + } + val flags = if (isSAndAbove()) { + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } + + return PendingIntent.getBroadcast(context, parentSessionId, callBackIntent, flags) + } } diff --git a/app/src/main/java/com/aurora/store/data/installer/SessionInstallerBase.kt b/app/src/main/java/com/aurora/store/data/installer/SessionInstallerBase.kt deleted file mode 100644 index 7c351dd48..000000000 --- a/app/src/main/java/com/aurora/store/data/installer/SessionInstallerBase.kt +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * Copyright (C) 2023, grrfe - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.data.installer - -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.content.pm.PackageInstaller -import com.aurora.extensions.isSAndAbove -import com.aurora.store.data.receiver.InstallerStatusReceiver -import com.aurora.store.util.Log -import java.io.File - -abstract class SessionInstallerBase(context: Context) : InstallerBase(context) { - - protected fun xInstall( - sessionId: Int, - session: PackageInstaller.Session, - packageName: String, - files: List - ) { - - try { - Log.i("Writing splits to session for $packageName") - - files.forEach { - it.inputStream().use { input -> - session.openWrite("${packageName}_${System.currentTimeMillis()}", 0, -1).use { output -> - input.copyTo(output) - session.fsync(output) - } - } - } - - val callBackIntent = Intent(context, InstallerStatusReceiver::class.java).apply { - action = InstallerStatusReceiver.ACTION_INSTALL_STATUS - setPackage(context.packageName) - putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName) - addFlags(Intent.FLAG_RECEIVER_FOREGROUND) - } - val flags = if (isSAndAbove()) - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE else - PendingIntent.FLAG_UPDATE_CURRENT - - val pendingIntent = PendingIntent.getBroadcast( - context, - sessionId, - callBackIntent, - flags - ) - - Log.i("Starting install session for $packageName") - session.commit(pendingIntent.intentSender) - session.close() - } catch (e: Exception) { - session.abandon() - removeFromInstallQueue(packageName) - - postError( - packageName, - e.localizedMessage, - e.stackTraceToString() - ) - } - } -} diff --git a/app/src/main/java/com/aurora/store/data/installer/ShizukuInstaller.kt b/app/src/main/java/com/aurora/store/data/installer/ShizukuInstaller.kt index ae5553703..27c0a8e54 100644 --- a/app/src/main/java/com/aurora/store/data/installer/ShizukuInstaller.kt +++ b/app/src/main/java/com/aurora/store/data/installer/ShizukuInstaller.kt @@ -19,7 +19,9 @@ package com.aurora.store.data.installer +import android.app.PendingIntent import android.content.Context +import android.content.Intent import android.content.pm.* import android.content.pm.PackageInstaller.SessionParams import android.os.Build @@ -29,13 +31,16 @@ import androidx.annotation.RequiresApi import com.aurora.extensions.isOAndAbove import com.aurora.extensions.isSAndAbove import com.aurora.store.R +import com.aurora.store.data.receiver.InstallerStatusReceiver import com.aurora.store.data.room.download.Download import com.aurora.store.util.Log +import com.aurora.store.util.PackageUtil.isSharedLibraryInstalled import dev.rikka.tools.refine.Refine import rikka.shizuku.ShizukuBinderWrapper import rikka.shizuku.SystemServiceHelper -class ShizukuInstaller(context: Context) : SessionInstallerBase(context) { +@RequiresApi(Build.VERSION_CODES.O) +class ShizukuInstaller(context: Context) : InstallerBase(context) { companion object { const val SHIZUKU_PACKAGE_NAME = "moe.shizuku.privileged.api" @@ -65,48 +70,89 @@ class ShizukuInstaller(context: Context) : SessionInstallerBase(context) { } else null } - @RequiresApi(Build.VERSION_CODES.O) override fun install(download: Download) { if (isAlreadyQueued(download.packageName)) { Log.i("${download.packageName} already queued") } else { - Log.i("Received session install request for ${download.packageName}") + download.sharedLibs.forEach { + // Shared library packages cannot be updated + if (!isSharedLibraryInstalled(context, it.packageName, it.versionCode)) { + install(download.packageName, download.versionCode, it.packageName) + } + } + install(download.packageName, download.versionCode) + } + } - val (sessionId, session) = kotlin.runCatching { - val params = SessionParams(SessionParams.MODE_FULL_INSTALL) + private fun install(packageName: String, versionCode: Int, sharedLibPkgName: String = "") { + Log.i("Received session install request for ${sharedLibPkgName.ifBlank { packageName }}") - // Replace existing app (Updates) - var flags = Refine - .unsafeCast(params).installFlags - flags = flags or PackageManagerHidden.INSTALL_REPLACE_EXISTING - Refine.unsafeCast(params).installFlags = - flags + val (sessionId, session) = kotlin.runCatching { + val params = SessionParams(SessionParams.MODE_FULL_INSTALL) - val sessionId = packageInstaller!!.createSession(params) - val iSession = IPackageInstallerSession.Stub.asInterface( - iPackageInstaller.openSession(sessionId).asShizukuBinder() - ) - val session = Refine.unsafeCast( - PackageInstallerHidden.SessionHidden(iSession) - ) + // Replace existing app (Updates) + var flags = Refine + .unsafeCast(params).installFlags + flags = flags or PackageManagerHidden.INSTALL_REPLACE_EXISTING + Refine.unsafeCast(params).installFlags = + flags - sessionId to session - }.getOrElse { ex -> - ex.printStackTrace() - postError( - download.packageName, - context.getString(R.string.installer_status_failure), - context.getString(R.string.installer_shizuku_unavailable) - ) - return + val sessionId = packageInstaller!!.createSession(params) + val iSession = IPackageInstallerSession.Stub.asInterface( + iPackageInstaller.openSession(sessionId).asShizukuBinder() + ) + val session = Refine.unsafeCast( + PackageInstallerHidden.SessionHidden(iSession) + ) + + sessionId to session + }.getOrElse { ex -> + ex.printStackTrace() + postError( + packageName, + context.getString(R.string.installer_status_failure), + context.getString(R.string.installer_shizuku_unavailable) + ) + return + } + + try { + Log.i("Writing splits to session for ${sharedLibPkgName.ifBlank { packageName }}") + getFiles(packageName, versionCode, sharedLibPkgName).forEach { + it.inputStream().use { input -> + session.openWrite("${sharedLibPkgName.ifBlank { packageName }}_${System.currentTimeMillis()}", 0, -1).use { output -> + input.copyTo(output) + session.fsync(output) + } + } } - xInstall( + val callBackIntent = Intent(context, InstallerStatusReceiver::class.java).apply { + action = InstallerStatusReceiver.ACTION_INSTALL_STATUS + setPackage(context.packageName) + addFlags(Intent.FLAG_RECEIVER_FOREGROUND) + putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, sharedLibPkgName.ifBlank { packageName }) + } + val flags = if (isSAndAbove()) { + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } + + val pendingIntent = PendingIntent.getBroadcast( + context, sessionId, - session, - download.packageName, - getFiles(download.packageName, download.versionCode) + callBackIntent, + flags ) + + Log.i("Starting install session for $packageName") + session.commit(pendingIntent.intentSender) + session.close() + } catch (exception: Exception) { + session.abandon() + removeFromInstallQueue(packageName) + postError(packageName, exception.localizedMessage, exception.stackTraceToString()) } } } diff --git a/app/src/main/java/com/aurora/store/data/receiver/PackageManagerReceiver.kt b/app/src/main/java/com/aurora/store/data/receiver/PackageManagerReceiver.kt index 4e910c02b..a95b86230 100644 --- a/app/src/main/java/com/aurora/store/data/receiver/PackageManagerReceiver.kt +++ b/app/src/main/java/com/aurora/store/data/receiver/PackageManagerReceiver.kt @@ -32,11 +32,14 @@ import com.aurora.store.data.model.DownloadStatus import com.aurora.store.data.room.download.Download import com.aurora.store.util.DownloadWorkerUtil import com.aurora.store.util.NotificationUtil +import com.aurora.store.util.PackageUtil.isSharedLibrary import com.aurora.store.util.PathUtil import com.aurora.store.util.Preferences import com.aurora.store.util.Preferences.PREFERENCE_AUTO_DELETE import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.firstOrNull import org.greenrobot.eventbus.EventBus @@ -56,6 +59,9 @@ open class PackageManagerReceiver : BroadcastReceiver() { if (intent.action != null && intent.data != null) { val packageName = intent.data!!.encodedSchemeSpecificPart + // We don't care about shared libraries, bail out + if (isSharedLibrary(context, packageName)) return@goAsync + when (intent.action) { Intent.ACTION_PACKAGE_ADDED -> { Log.i(TAG, "Installed $packageName") diff --git a/app/src/main/java/com/aurora/store/data/work/DownloadWorker.kt b/app/src/main/java/com/aurora/store/data/work/DownloadWorker.kt index 9a5ef1e55..4e9a5e845 100644 --- a/app/src/main/java/com/aurora/store/data/work/DownloadWorker.kt +++ b/app/src/main/java/com/aurora/store/data/work/DownloadWorker.kt @@ -20,6 +20,7 @@ import com.aurora.extensions.isSAndAbove import com.aurora.extensions.requiresObbDir import com.aurora.gplayapi.helpers.PurchaseHelper import com.aurora.store.data.installer.AppInstaller +import com.aurora.store.data.installer.SessionInstaller import com.aurora.store.data.model.DownloadInfo import com.aurora.store.data.model.DownloadStatus import com.aurora.store.data.model.Request @@ -42,6 +43,7 @@ import java.io.File import java.net.URL import java.security.DigestInputStream import java.security.MessageDigest +import kotlinx.coroutines.MainScope import kotlinx.coroutines.NonCancellable import kotlin.properties.Delegates import com.aurora.gplayapi.data.models.File as GPlayFile @@ -159,9 +161,11 @@ class DownloadWorker @AssistedInject constructor( } private suspend fun onSuccess() { - withContext(NonCancellable) { + val installer = appInstaller.getPreferredInstaller() + val dispatcher = if (installer is SessionInstaller) Dispatchers.Main else NonCancellable + withContext(dispatcher) { try { - appInstaller.getPreferredInstaller().install(download) + installer.install(download) } catch (exception: Exception) { Log.e(TAG, "Failed to install ${download.packageName}", exception) } diff --git a/app/src/main/java/com/aurora/store/util/PackageUtil.kt b/app/src/main/java/com/aurora/store/util/PackageUtil.kt index b5bb2b83b..5111c4725 100644 --- a/app/src/main/java/com/aurora/store/util/PackageUtil.kt +++ b/app/src/main/java/com/aurora/store/util/PackageUtil.kt @@ -53,14 +53,22 @@ object PackageUtil { } } + fun isSharedLibrary(context: Context, packageName: String): Boolean { + return if (isOAndAbove()) { + getAllSharedLibraries(context).any { it.name == packageName } + } else { + false + } + } + fun isSharedLibraryInstalled(context: Context, packageName: String, versionCode: Int): Boolean { - if (isOAndAbove()) { + return if (isOAndAbove()) { val sharedLibraries = getAllSharedLibraries(context) - return sharedLibraries.any { + sharedLibraries.any { it.name == packageName && it.version == versionCode } } else { - return false + false } }