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 <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2024-01-26 18:28:14 +05:30
parent c562f4351e
commit 7e899ff8eb
8 changed files with 220 additions and 152 deletions

View File

@@ -34,7 +34,7 @@ class InstallActivity : AppCompatActivity() {
override fun onProgressChanged(sessionId: Int, progress: Float) {} override fun onProgressChanged(sessionId: Int, progress: Float) {}
override fun onFinished(sessionId: Int, success: Boolean) { override fun onFinished(sessionId: Int, success: Boolean) {
if (sessionInstaller.sessionId == sessionId) { if (sessionInstaller.parentSessionId == sessionId) {
Log.i(TAG, "Install finished with status code: $success") Log.i(TAG, "Install finished with status code: $success")
finish() finish()
} }

View File

@@ -24,9 +24,9 @@ import com.aurora.store.R
import com.aurora.store.data.event.InstallerEvent import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.room.download.Download import com.aurora.store.data.room.download.Download
import com.aurora.store.util.Log import com.aurora.store.util.Log
import com.aurora.store.util.PackageUtil.isSharedLibraryInstalled
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import java.io.File
import java.util.regex.Pattern import java.util.regex.Pattern
class RootInstaller(context: Context) : InstallerBase(context) { class RootInstaller(context: Context) : InstallerBase(context) {
@@ -36,7 +36,13 @@ class RootInstaller(context: Context) : InstallerBase(context) {
Log.i("${download.packageName} already queued") Log.i("${download.packageName} already queued")
} else { } else {
if (Shell.getShell().isRoot) { 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 { } else {
postError( postError(
download.packageName, download.packageName,
@@ -48,10 +54,10 @@ class RootInstaller(context: Context) : InstallerBase(context) {
} }
} }
private fun xInstall(packageName: String, files: List<File>) { private fun xInstall(packageName: String, versionCode: Int, sharedLibPkgName: String = "") {
var totalSize = 0 var totalSize = 0
for (file in files) for (file in getFiles(packageName, versionCode, sharedLibPkgName))
totalSize += file.length().toInt() totalSize += file.length().toInt()
val result: Shell.Result = val result: Shell.Result =
@@ -67,7 +73,7 @@ class RootInstaller(context: Context) : InstallerBase(context) {
if (found) { if (found) {
val sessionId = sessionIdMatcher.group(1)?.toInt() val sessionId = sessionIdMatcher.group(1)?.toInt()
if (Shell.getShell().isRoot && sessionId != null) { 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}\"") Shell.cmd("cat \"${file.absoluteFile}\" | pm install-write -S ${file.length()} $sessionId \"${file.name}\"")
.exec() .exec()
} }

View File

@@ -20,23 +20,31 @@
package com.aurora.store.data.installer package com.aurora.store.data.installer
import android.app.PendingIntent
import android.content.Context 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.PACKAGE_SOURCE_STORE
import android.content.pm.PackageInstaller.SessionParams import android.content.pm.PackageInstaller.SessionParams
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Process
import com.aurora.extensions.isNAndAbove import com.aurora.extensions.isNAndAbove
import com.aurora.extensions.isOAndAbove import com.aurora.extensions.isOAndAbove
import com.aurora.extensions.isSAndAbove import com.aurora.extensions.isSAndAbove
import com.aurora.extensions.isTAndAbove import com.aurora.extensions.isTAndAbove
import com.aurora.extensions.isUAndAbove import com.aurora.extensions.isUAndAbove
import com.aurora.store.data.receiver.InstallerStatusReceiver
import com.aurora.store.data.room.download.Download import com.aurora.store.data.room.download.Download
import com.aurora.store.util.Log import com.aurora.store.util.Log
import com.aurora.store.util.PackageUtil.isSharedLibraryInstalled
import kotlin.properties.Delegates import kotlin.properties.Delegates
class SessionInstaller(context: Context) : InstallerBase(context) {
class SessionInstaller(context: Context) : SessionInstallerBase(context) { var parentSessionId by Delegates.notNull<Int>()
var sessionId by Delegates.notNull<Int>() private val packageInstaller = context.packageManager.packageInstaller
private val sessionIdMap = mutableMapOf<Int, String>()
override fun install(download: Download) { override fun install(download: Download) {
if (isAlreadyQueued(download.packageName)) { if (isAlreadyQueued(download.packageName)) {
@@ -44,35 +52,109 @@ class SessionInstaller(context: Context) : SessionInstallerBase(context) {
} else { } else {
Log.i("Received session install request for ${download.packageName}") Log.i("Received session install request for ${download.packageName}")
val packageInstaller = context.packageManager.packageInstaller val callback = object : PackageInstaller.SessionCallback() {
val sessionParams = SessionParams(SessionParams.MODE_FULL_INSTALL).apply { override fun onCreated(sessionId: Int) {}
setAppPackageName(download.packageName)
if (isOAndAbove()) { override fun onBadgingChanged(sessionId: Int) {}
setInstallReason(PackageManager.INSTALL_REASON_USER)
} override fun onActiveChanged(sessionId: Int, active: Boolean) {}
if (isNAndAbove()) {
setOriginatingUid(android.os.Process.myUid()) override fun onProgressChanged(sessionId: Int, progress: Float) {}
}
if (isSAndAbove()) { override fun onFinished(sessionId: Int, success: Boolean) {
setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED) if (sessionId in sessionIdMap.keys && success) {
} sessionIdMap.remove(sessionId)
if (isTAndAbove()) { if (sessionIdMap.isNotEmpty()) {
setPackageSource(PACKAGE_SOURCE_STORE) val nextSession = sessionIdMap.keys.first()
} commitInstall(sessionIdMap.getValue(nextSession), nextSession)
if (isUAndAbove()) { } else {
setInstallerPackageName(context.packageName) packageInstaller.unregisterSessionCallback(this)
}
}
} }
} }
sessionId = packageInstaller.createSession(sessionParams) download.sharedLibs.forEach {
val session = packageInstaller.openSession(sessionId) // 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( if (sessionIdMap.size > 1) packageInstaller.registerSessionCallback(callback)
sessionId, commitInstall(
session, sessionIdMap.getValue(sessionIdMap.keys.first()),
download.packageName, sessionIdMap.keys.first()
getFiles(download.packageName, download.versionCode)
) )
} }
} }
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)
}
} }

View File

@@ -1,84 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
* Copyright (C) 2023, grrfe <grrfe@420blaze.it>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
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<File>
) {
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()
)
}
}
}

View File

@@ -19,7 +19,9 @@
package com.aurora.store.data.installer package com.aurora.store.data.installer
import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.pm.* import android.content.pm.*
import android.content.pm.PackageInstaller.SessionParams import android.content.pm.PackageInstaller.SessionParams
import android.os.Build import android.os.Build
@@ -29,13 +31,16 @@ import androidx.annotation.RequiresApi
import com.aurora.extensions.isOAndAbove import com.aurora.extensions.isOAndAbove
import com.aurora.extensions.isSAndAbove import com.aurora.extensions.isSAndAbove
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.data.receiver.InstallerStatusReceiver
import com.aurora.store.data.room.download.Download import com.aurora.store.data.room.download.Download
import com.aurora.store.util.Log import com.aurora.store.util.Log
import com.aurora.store.util.PackageUtil.isSharedLibraryInstalled
import dev.rikka.tools.refine.Refine import dev.rikka.tools.refine.Refine
import rikka.shizuku.ShizukuBinderWrapper import rikka.shizuku.ShizukuBinderWrapper
import rikka.shizuku.SystemServiceHelper import rikka.shizuku.SystemServiceHelper
class ShizukuInstaller(context: Context) : SessionInstallerBase(context) { @RequiresApi(Build.VERSION_CODES.O)
class ShizukuInstaller(context: Context) : InstallerBase(context) {
companion object { companion object {
const val SHIZUKU_PACKAGE_NAME = "moe.shizuku.privileged.api" const val SHIZUKU_PACKAGE_NAME = "moe.shizuku.privileged.api"
@@ -65,48 +70,89 @@ class ShizukuInstaller(context: Context) : SessionInstallerBase(context) {
} else null } else null
} }
@RequiresApi(Build.VERSION_CODES.O)
override fun install(download: Download) { override fun install(download: Download) {
if (isAlreadyQueued(download.packageName)) { if (isAlreadyQueued(download.packageName)) {
Log.i("${download.packageName} already queued") Log.i("${download.packageName} already queued")
} else { } 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 { private fun install(packageName: String, versionCode: Int, sharedLibPkgName: String = "") {
val params = SessionParams(SessionParams.MODE_FULL_INSTALL) Log.i("Received session install request for ${sharedLibPkgName.ifBlank { packageName }}")
// Replace existing app (Updates) val (sessionId, session) = kotlin.runCatching {
var flags = Refine val params = SessionParams(SessionParams.MODE_FULL_INSTALL)
.unsafeCast<PackageInstallerHidden.SessionParamsHidden>(params).installFlags
flags = flags or PackageManagerHidden.INSTALL_REPLACE_EXISTING
Refine.unsafeCast<PackageInstallerHidden.SessionParamsHidden>(params).installFlags =
flags
val sessionId = packageInstaller!!.createSession(params) // Replace existing app (Updates)
val iSession = IPackageInstallerSession.Stub.asInterface( var flags = Refine
iPackageInstaller.openSession(sessionId).asShizukuBinder() .unsafeCast<PackageInstallerHidden.SessionParamsHidden>(params).installFlags
) flags = flags or PackageManagerHidden.INSTALL_REPLACE_EXISTING
val session = Refine.unsafeCast<PackageInstaller.Session>( Refine.unsafeCast<PackageInstallerHidden.SessionParamsHidden>(params).installFlags =
PackageInstallerHidden.SessionHidden(iSession) flags
)
sessionId to session val sessionId = packageInstaller!!.createSession(params)
}.getOrElse { ex -> val iSession = IPackageInstallerSession.Stub.asInterface(
ex.printStackTrace() iPackageInstaller.openSession(sessionId).asShizukuBinder()
postError( )
download.packageName, val session = Refine.unsafeCast<PackageInstaller.Session>(
context.getString(R.string.installer_status_failure), PackageInstallerHidden.SessionHidden(iSession)
context.getString(R.string.installer_shizuku_unavailable) )
)
return 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, sessionId,
session, callBackIntent,
download.packageName, flags
getFiles(download.packageName, download.versionCode)
) )
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())
} }
} }
} }

View File

@@ -32,11 +32,14 @@ import com.aurora.store.data.model.DownloadStatus
import com.aurora.store.data.room.download.Download import com.aurora.store.data.room.download.Download
import com.aurora.store.util.DownloadWorkerUtil import com.aurora.store.util.DownloadWorkerUtil
import com.aurora.store.util.NotificationUtil import com.aurora.store.util.NotificationUtil
import com.aurora.store.util.PackageUtil.isSharedLibrary
import com.aurora.store.util.PathUtil import com.aurora.store.util.PathUtil
import com.aurora.store.util.Preferences import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_AUTO_DELETE import com.aurora.store.util.Preferences.PREFERENCE_AUTO_DELETE
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
@@ -56,6 +59,9 @@ open class PackageManagerReceiver : BroadcastReceiver() {
if (intent.action != null && intent.data != null) { if (intent.action != null && intent.data != null) {
val packageName = intent.data!!.encodedSchemeSpecificPart val packageName = intent.data!!.encodedSchemeSpecificPart
// We don't care about shared libraries, bail out
if (isSharedLibrary(context, packageName)) return@goAsync
when (intent.action) { when (intent.action) {
Intent.ACTION_PACKAGE_ADDED -> { Intent.ACTION_PACKAGE_ADDED -> {
Log.i(TAG, "Installed $packageName") Log.i(TAG, "Installed $packageName")

View File

@@ -20,6 +20,7 @@ import com.aurora.extensions.isSAndAbove
import com.aurora.extensions.requiresObbDir import com.aurora.extensions.requiresObbDir
import com.aurora.gplayapi.helpers.PurchaseHelper import com.aurora.gplayapi.helpers.PurchaseHelper
import com.aurora.store.data.installer.AppInstaller 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.DownloadInfo
import com.aurora.store.data.model.DownloadStatus import com.aurora.store.data.model.DownloadStatus
import com.aurora.store.data.model.Request import com.aurora.store.data.model.Request
@@ -42,6 +43,7 @@ import java.io.File
import java.net.URL import java.net.URL
import java.security.DigestInputStream import java.security.DigestInputStream
import java.security.MessageDigest import java.security.MessageDigest
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
import kotlin.properties.Delegates import kotlin.properties.Delegates
import com.aurora.gplayapi.data.models.File as GPlayFile import com.aurora.gplayapi.data.models.File as GPlayFile
@@ -159,9 +161,11 @@ class DownloadWorker @AssistedInject constructor(
} }
private suspend fun onSuccess() { private suspend fun onSuccess() {
withContext(NonCancellable) { val installer = appInstaller.getPreferredInstaller()
val dispatcher = if (installer is SessionInstaller) Dispatchers.Main else NonCancellable
withContext(dispatcher) {
try { try {
appInstaller.getPreferredInstaller().install(download) installer.install(download)
} catch (exception: Exception) { } catch (exception: Exception) {
Log.e(TAG, "Failed to install ${download.packageName}", exception) Log.e(TAG, "Failed to install ${download.packageName}", exception)
} }

View File

@@ -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 { fun isSharedLibraryInstalled(context: Context, packageName: String, versionCode: Int): Boolean {
if (isOAndAbove()) { return if (isOAndAbove()) {
val sharedLibraries = getAllSharedLibraries(context) val sharedLibraries = getAllSharedLibraries(context)
return sharedLibraries.any { sharedLibraries.any {
it.name == packageName && it.version == versionCode it.name == packageName && it.version == versionCode
} }
} else { } else {
return false false
} }
} }