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