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 onFinished(sessionId: Int, success: Boolean) {
if (sessionInstaller.sessionId == sessionId) {
if (sessionInstaller.parentSessionId == sessionId) {
Log.i(TAG, "Install finished with status code: $success")
finish()
}

View File

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

View File

@@ -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<Int>()
var sessionId by Delegates.notNull<Int>()
private val packageInstaller = context.packageManager.packageInstaller
private val sessionIdMap = mutableMapOf<Int, String>()
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)
}
}

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
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<PackageInstallerHidden.SessionParamsHidden>(params).installFlags
flags = flags or PackageManagerHidden.INSTALL_REPLACE_EXISTING
Refine.unsafeCast<PackageInstallerHidden.SessionParamsHidden>(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<PackageInstaller.Session>(
PackageInstallerHidden.SessionHidden(iSession)
)
// Replace existing app (Updates)
var flags = Refine
.unsafeCast<PackageInstallerHidden.SessionParamsHidden>(params).installFlags
flags = flags or PackageManagerHidden.INSTALL_REPLACE_EXISTING
Refine.unsafeCast<PackageInstallerHidden.SessionParamsHidden>(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<PackageInstaller.Session>(
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())
}
}
}

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.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")

View File

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

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 {
if (isOAndAbove()) {
return if (isOAndAbove()) {
val sharedLibraries = getAllSharedLibraries(context)
return sharedLibraries.any {
sharedLibraries.any {
it.name == packageName && it.version == versionCode
}
} else {
return false
false
}
}