AppInstaller: Rework and extend silent install logic check

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2024-10-07 18:36:58 +05:30
parent 6ea0cda277
commit 9a49ccc727
10 changed files with 130 additions and 38 deletions

View File

@@ -1,6 +1,9 @@
package com.aurora.extensions package com.aurora.extensions
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build
import androidx.annotation.RequiresApi
import com.aurora.store.BuildConfig
fun PackageManager.getInstallerPackageNameCompat(packageName: String): String? { fun PackageManager.getInstallerPackageNameCompat(packageName: String): String? {
return if (isRAndAbove()) { return if (isRAndAbove()) {
@@ -10,3 +13,17 @@ fun PackageManager.getInstallerPackageNameCompat(packageName: String): String? {
return getInstallerPackageName(packageName) return getInstallerPackageName(packageName)
} }
} }
@RequiresApi(Build.VERSION_CODES.S)
fun PackageManager.getUpdateOwnerPackageNameCompat(packageName: String): String? {
// https://developer.android.com/reference/android/content/pm/PackageInstaller.SessionParams#setRequireUserAction(int)
val installSourceInfo = getInstallSourceInfo(packageName)
return when {
isUAndAbove() -> {
// If update ownership is null, we can still silently update it if we installed it
installSourceInfo.updateOwnerPackageName ?: installSourceInfo.installingPackageName
}
isSAndAbove() -> installSourceInfo.installingPackageName
else -> if (packageName == BuildConfig.APPLICATION_ID) BuildConfig.APPLICATION_ID else null
}
}

View File

@@ -30,10 +30,17 @@ import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.content.pm.PackageInfoCompat import androidx.core.content.pm.PackageInfoCompat
import com.aurora.extensions.getUpdateOwnerPackageNameCompat
import com.aurora.extensions.isOAndAbove import com.aurora.extensions.isOAndAbove
import com.aurora.extensions.isPAndAbove import com.aurora.extensions.isPAndAbove
import com.aurora.extensions.isSAndAbove
import com.aurora.extensions.isTAndAbove
import com.aurora.extensions.isUAndAbove
import com.aurora.extensions.isVAndAbove
import com.aurora.store.BuildConfig
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.data.model.InstallerInfo import com.aurora.store.data.model.InstallerInfo
import com.aurora.store.util.CertUtil
import com.aurora.store.util.NotificationUtil import com.aurora.store.util.NotificationUtil
import com.aurora.store.util.PackageUtil import com.aurora.store.util.PackageUtil
import com.aurora.store.util.Preferences import com.aurora.store.util.Preferences
@@ -101,6 +108,37 @@ class AppInstaller @Inject constructor(
return installers return installers
} }
/**
* Checks if the given package can be silently installed
* @param context [Context]
* @param packageName Package to silently install
*/
fun canInstallSilently(context: Context, packageName: String, targetSdk: Int): Boolean {
return when (getCurrentInstaller(context)) {
Installer.SESSION -> {
// Silent install cannot be done on initial install and below A12
if (!PackageUtil.isInstalled(context, packageName) || !isSAndAbove()) return false
// We cannot do silent updates if we are not the update owner
if (context.packageManager.getUpdateOwnerPackageNameCompat(packageName) != BuildConfig.APPLICATION_ID) return false
// Ensure app being installed satisfies Android's requirement for targetSdk level
when (Build.VERSION.SDK_INT) {
Build.VERSION_CODES.VANILLA_ICE_CREAM -> targetSdk == Build.VERSION_CODES.TIRAMISU
Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> targetSdk == Build.VERSION_CODES.S
Build.VERSION_CODES.TIRAMISU -> targetSdk == Build.VERSION_CODES.R
Build.VERSION_CODES.S -> targetSdk == Build.VERSION_CODES.Q
else -> false // Only Android version above 12 can silently update apps
}
}
Installer.NATIVE -> false // Deprecated
Installer.ROOT -> hasRootAccess()
Installer.SERVICE -> false // Deprecated
Installer.AM -> false // We cannot check if AppManager has ability to auto-update
Installer.SHIZUKU -> isOAndAbove() && hasShizukuOrSui(context) && hasShizukuPerm()
}
}
fun notifyInstallation(context: Context, displayName: String, packageName: String) { fun notifyInstallation(context: Context, displayName: String, packageName: String) {
val notificationManager = context.getSystemService<NotificationManager>() val notificationManager = context.getSystemService<NotificationManager>()
val notification = NotificationUtil.getInstallNotification(context, displayName, packageName) val notification = NotificationUtil.getInstallNotification(context, displayName, packageName)

View File

@@ -13,7 +13,7 @@ import com.aurora.store.data.room.update.UpdateDao
@Database( @Database(
entities = [Download::class, Favourite::class, Update::class], entities = [Download::class, Favourite::class, Update::class],
version = 3, version = 4,
exportSchema = false exportSchema = false
) )
@TypeConverters(DownloadConverter::class) @TypeConverters(DownloadConverter::class)

View File

@@ -0,0 +1,43 @@
package com.aurora.store.data.room
import android.util.Log
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
/**
* A helper class for doing migrations for the [AuroraDatabase].
* @see [RoomModule]
*/
object MigrationHelper {
// ADD ALL NEW MIGRATION STEPS HERE TOO
val MIGRATION_1_4 = object : Migration(1, 4) {
override fun migrate(db: SupportSQLiteDatabase) {
migrateFrom3To4(db)
}
}
val MIGRATION_3_4 = object : Migration(3, 4) {
override fun migrate(db: SupportSQLiteDatabase) = migrateFrom3To4(db)
}
private const val TAG = "MigrationHelper"
/**
* Add targetSdk column to download and update table for checking if silent install is possible.
*/
private fun migrateFrom3To4(database: SupportSQLiteDatabase) {
database.beginTransaction()
try {
listOf("download", "update").forEach {
database.execSQL("ALTER TABLE `$it` ADD COLUMN targetSdk INTEGER NOT NULL")
database.execSQL("UPDATE `$it` SET targetSdk=1")
}
database.setTransactionSuccessful()
} catch (exception: Exception) {
Log.e(TAG, "Failed while migrating from database version 3 to 4", exception)
} finally {
database.endTransaction()
}
}
}

View File

@@ -2,6 +2,8 @@ package com.aurora.store.data.room
import android.content.Context import android.content.Context
import androidx.room.Room import androidx.room.Room
import com.aurora.store.data.room.MigrationHelper.MIGRATION_1_4
import com.aurora.store.data.room.MigrationHelper.MIGRATION_3_4
import com.aurora.store.data.room.download.DownloadConverter import com.aurora.store.data.room.download.DownloadConverter
import com.aurora.store.data.room.download.DownloadDao import com.aurora.store.data.room.download.DownloadDao
import com.aurora.store.data.room.favourites.FavouriteDao import com.aurora.store.data.room.favourites.FavouriteDao
@@ -26,6 +28,7 @@ object RoomModule {
downloadConverter: DownloadConverter downloadConverter: DownloadConverter
): AuroraDatabase { ): AuroraDatabase {
return Room.databaseBuilder(context, AuroraDatabase::class.java, DATABASE) return Room.databaseBuilder(context, AuroraDatabase::class.java, DATABASE)
.addMigrations(MIGRATION_3_4, MIGRATION_1_4)
.addTypeConverter(downloadConverter) .addTypeConverter(downloadConverter)
.build() .build()
} }

View File

@@ -27,7 +27,8 @@ data class Download(
var totalFiles: Int, var totalFiles: Int,
var downloadedFiles: Int, var downloadedFiles: Int,
var fileList: List<File>, var fileList: List<File>,
val sharedLibs: List<SharedLib> val sharedLibs: List<SharedLib>,
val targetSdk: Int,
) : Parcelable { ) : Parcelable {
val isFinished get() = downloadStatus in DownloadStatus.finished val isFinished get() = downloadStatus in DownloadStatus.finished
val isRunning get() = downloadStatus in DownloadStatus.running val isRunning get() = downloadStatus in DownloadStatus.running
@@ -50,7 +51,8 @@ data class Download(
0, 0,
0, 0,
app.fileList.filterNot { it.url.isBlank() }, app.fileList.filterNot { it.url.isBlank() },
app.dependencies.dependentLibraries.map { SharedLib.fromApp(it) } app.dependencies.dependentLibraries.map { SharedLib.fromApp(it) },
app.targetSdk
) )
} }
@@ -71,7 +73,8 @@ data class Download(
0, 0,
0, 0,
update.fileList, update.fileList,
update.sharedLibs update.sharedLibs,
update.targetSdk
) )
} }
} }

View File

@@ -29,7 +29,8 @@ data class Update(
val hasValidCert: Boolean, val hasValidCert: Boolean,
val offerType: Int, val offerType: Int,
var fileList: List<File>, var fileList: List<File>,
val sharedLibs: List<SharedLib> val sharedLibs: List<SharedLib>,
val targetSdk: Int
) : Parcelable { ) : Parcelable {
companion object { companion object {
@@ -52,7 +53,8 @@ data class Update(
}, },
app.offerType, app.offerType,
app.fileList.filterNot { it.url.isBlank() }, app.fileList.filterNot { it.url.isBlank() },
app.dependencies.dependentLibraries.map { SharedLib.fromApp(it) } app.dependencies.dependentLibraries.map { SharedLib.fromApp(it) },
app.targetSdk
) )
} }
} }

View File

@@ -15,13 +15,9 @@ import androidx.work.WorkManager
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.aurora.extensions.isIgnoringBatteryOptimizations import com.aurora.extensions.isIgnoringBatteryOptimizations
import com.aurora.extensions.isMAndAbove import com.aurora.extensions.isMAndAbove
import com.aurora.extensions.isOAndAbove
import com.aurora.extensions.isSAndAbove
import com.aurora.store.data.installer.AppInstaller import com.aurora.store.data.installer.AppInstaller
import com.aurora.store.data.installer.AppInstaller.Companion.Installer
import com.aurora.store.data.providers.AuthProvider import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.util.AppUtil import com.aurora.store.util.AppUtil
import com.aurora.store.util.CertUtil
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.Preferences import com.aurora.store.util.Preferences
@@ -141,7 +137,9 @@ class UpdateWorker @AssistedInject constructor(
} else { } else {
if (appContext.isIgnoringBatteryOptimizations()) { if (appContext.isIgnoringBatteryOptimizations()) {
// Trigger download for apps if they can be auto-updated (if any) // Trigger download for apps if they can be auto-updated (if any)
updatesList.filter { canAutoUpdate(it.packageName) }.let { list -> updatesList.filter {
AppInstaller.canInstallSilently(appContext, it.packageName, it.targetSdk)
}.let { list ->
if (list.isEmpty()) return@let if (list.isEmpty()) return@let
Log.i(TAG, "Found auto-update enabled apps, updating!") Log.i(TAG, "Found auto-update enabled apps, updating!")
@@ -149,7 +147,9 @@ class UpdateWorker @AssistedInject constructor(
} }
// Notify about remaining apps (if any) // Notify about remaining apps (if any)
updatesList.filterNot { canAutoUpdate(it.packageName) }.let { list -> updatesList.filterNot {
AppInstaller.canInstallSilently(appContext, it.packageName, it.targetSdk)
}.let { list ->
if (list.isEmpty()) return@let if (list.isEmpty()) return@let
Log.i(TAG, "Found apps that cannot be auto-updated, notifying!") Log.i(TAG, "Found apps that cannot be auto-updated, notifying!")
@@ -179,19 +179,4 @@ class UpdateWorker @AssistedInject constructor(
} }
return Result.success() return Result.success()
} }
/**
* Checks if the given package can be auto-updated or not
*/
private fun canAutoUpdate(packageName: String): Boolean {
return when (AppInstaller.getCurrentInstaller(appContext)) {
Installer.SESSION -> isSAndAbove() && CertUtil.isAuroraStoreApp(appContext, packageName)
Installer.NATIVE -> false
Installer.ROOT -> AppInstaller.hasRootAccess()
Installer.SERVICE -> AppInstaller.hasAuroraService(appContext)
Installer.AM -> false // We cannot check if AppManager has ability to auto-update
Installer.SHIZUKU -> isOAndAbove() && AppInstaller.hasShizukuOrSui(appContext) &&
AppInstaller.hasShizukuPerm()
}
}
} }

View File

@@ -43,10 +43,6 @@ object CertUtil {
private const val CERT_BEGIN = "-----BEGIN CERTIFICATE-----" private const val CERT_BEGIN = "-----BEGIN CERTIFICATE-----"
private const val CERT_END = "-----END CERTIFICATE-----" private const val CERT_END = "-----END CERTIFICATE-----"
fun isAuroraStoreApp(context: Context, packageName: String): Boolean {
return context.packageManager.getInstallerPackageNameCompat(packageName) == context.packageName
}
fun isFDroidApp(context: Context, packageName: String): Boolean { fun isFDroidApp(context: Context, packageName: String): Boolean {
return isInstalledByFDroid(context, packageName) || isSignedByFDroid(context, packageName) return isInstalledByFDroid(context, packageName) || isSignedByFDroid(context, packageName)
} }

View File

@@ -20,6 +20,7 @@ import com.aurora.Constants
import com.aurora.store.MainActivity import com.aurora.store.MainActivity
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.data.activity.InstallActivity import com.aurora.store.data.activity.InstallActivity
import com.aurora.store.data.installer.AppInstaller
import com.aurora.store.data.model.DownloadStatus 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.data.room.update.Update import com.aurora.store.data.room.update.Update
@@ -109,13 +110,17 @@ object NotificationUtil {
builder.setAutoCancel(true) builder.setAutoCancel(true)
builder.setCategory(Notification.CATEGORY_STATUS) builder.setCategory(Notification.CATEGORY_STATUS)
builder.setContentIntent(getContentIntentForDetails(context, download.packageName)) builder.setContentIntent(getContentIntentForDetails(context, download.packageName))
builder.addAction(
NotificationCompat.Action.Builder( // Show install action if app cannot be silently installed
R.drawable.ic_install, if (!AppInstaller.canInstallSilently(context, download.packageName, download.targetSdk)) {
context.getString(R.string.action_install), builder.addAction(
getInstallIntent(context, download) NotificationCompat.Action.Builder(
).build() R.drawable.ic_install,
) context.getString(R.string.action_install),
getInstallIntent(context, download)
).build()
)
}
} }
DownloadStatus.DOWNLOADING, DownloadStatus.QUEUED -> { DownloadStatus.DOWNLOADING, DownloadStatus.QUEUED -> {