diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 8732ea7..bfbba08 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ ubuntu-latest, windows-latest, macOS-latest ] + os: [ ubuntu-latest, windows-latest, macos-15-intel ] steps: - name: Check out diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 821af44..acfd657 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -21,8 +21,11 @@ android { namespace = "io.i10nstaller" defaultConfig { applicationId = "io.i10nstaller" - versionCode = 9 - versionName = "1.8" + versionCode = 10 + versionName = "1.9" + ndk { + abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")) + } optimization { keepRules { ignoreFromAllExternalDependencies(true) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 8e5ff69..437c850 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -21,6 +21,9 @@ -keepclassmembers class io.i10nstaller.viewmodel.InstallViewModel { (android.app.Application); } +-keep class io.i10nstaller.model.ModernInstallResultSender { + public static android.content.IntentSender create(kotlin.jvm.functions.Function4); +} -keepnames class moe.shizuku.api.BinderContainer -keepclassmembers class rikka.shizuku.Shizuku { private static rikka.shizuku.ShizukuRemoteProcess newProcess(java.lang.String[], java.lang.String[], java.lang.String); diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a66cfd7..1ba7f83 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,11 +10,21 @@ android:name="${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION" tools:node="remove" /> + + + + + + Log.e(TAG, "Uncaught exception on thread ${thread.name}", error) + if (previousHandler != null) { + previousHandler.uncaughtException(thread, error) + } else { + Process.killProcess(Process.myPid()) + exitProcess(10) + } + } + } + + private companion object { + const val TAG = "InstallerCrash" + } +} diff --git a/app/src/main/java/io/i10nstaller/model/InstallFlags.kt b/app/src/main/java/io/i10nstaller/model/InstallFlags.kt new file mode 100644 index 0000000..8eb22ae --- /dev/null +++ b/app/src/main/java/io/i10nstaller/model/InstallFlags.kt @@ -0,0 +1,16 @@ +package io.i10nstaller.model + +internal const val MATCH_ANY_USER = 0x00400000 +internal const val MATCH_KNOWN_PACKAGES = 0x00402000 +internal const val INSTALL_FROM_ADB = 0x00000020 +internal const val INSTALL_REPLACE_EXISTING = 0x00000002 +internal const val INSTALL_ALLOW_TEST = 0x00000004 +internal const val INSTALL_GRANT_ALL_REQUESTED_PERMISSIONS = 0x00000100 +internal const val INSTALL_REQUEST_DOWNGRADE = 0x00000080 +internal const val INSTALL_FULL_APP = 0x00004000 +internal const val INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK = 0x01000000 +internal const val INSTALL_REQUEST_UPDATE_OWNERSHIP = 0x02000000 +internal const val INSTALL_SUCCEEDED = 1 +internal const val INSTALL_FAILED_INTERNAL_ERROR = -110 +internal const val DELETE_KEEP_DATA = 0x00000001 +internal const val EXTRA_LEGACY_STATUS = "android.content.pm.extra.LEGACY_STATUS" diff --git a/app/src/main/java/io/i10nstaller/model/InstallRepository.kt b/app/src/main/java/io/i10nstaller/model/InstallRepository.kt index 3489e54..087bcc0 100644 --- a/app/src/main/java/io/i10nstaller/model/InstallRepository.kt +++ b/app/src/main/java/io/i10nstaller/model/InstallRepository.kt @@ -8,6 +8,7 @@ import android.content.ContentValues import android.content.Intent import android.content.IntentSender import android.content.Intent_rename +import android.content.res.AssetFileDescriptor import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo import android.content.pm.PackageInstaller @@ -22,6 +23,7 @@ import android.net.Uri import android.os.Build import android.os.Environment import android.os.Process +import android.os.ParcelFileDescriptor import android.provider.DocumentsContract import android.provider.MediaStore import android.provider.OpenableColumns @@ -34,29 +36,36 @@ import androidx.lifecycle.MutableLiveData import io.i10nstaller.R import io.i10nstaller.model.Hook.wrap import io.i10nstaller.model.InstallAborted.Companion.ABORT_CLOSE -import io.i10nstaller.model.InstallAborted.Companion.ABORT_CREATE +import io.i10nstaller.model.InstallAborted.Companion.ABORT_FAILURE import io.i10nstaller.model.InstallAborted.Companion.ABORT_INFO import io.i10nstaller.model.InstallAborted.Companion.ABORT_NOTFOUND import io.i10nstaller.model.InstallAborted.Companion.ABORT_PARSE -import io.i10nstaller.model.InstallAborted.Companion.ABORT_SHIZUKU import io.i10nstaller.model.InstallAborted.Companion.ABORT_SPLIT import io.i10nstaller.model.InstallAborted.Companion.ABORT_WRITE import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import rikka.shizuku.Shizuku -import rikka.shizuku.ShizukuProvider import java.io.File import java.io.IOException +private const val INTERACT_ACROSS_USERS = "android.permission.INTERACT_ACROSS_USERS" + class InstallRepository(private val context: Application) { private val TAG = InstallRepository::class.java.simpleName private val CLEANUP_TAG = "InstallCleanup" - private val postInstallScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private val postInstallExceptionHandler = CoroutineExceptionHandler { _, error -> + Log.e("InstallerCrash", "Post-install coroutine failed", error) + } + private val postInstallScope = CoroutineScope( + SupervisorJob() + Dispatchers.IO + postInstallExceptionHandler + ) val installResult = MutableLiveData() val stagingProgress = MutableLiveData() private lateinit var packageManager: PackageManager + private lateinit var currentUserPackageManager: PackageManager private lateinit var packageInstaller: PackageInstaller private var stagedSessionId = SessionInfo.INVALID_ID private var callingUid = Process.INVALID_UID @@ -64,39 +73,196 @@ class InstallRepository(private val context: Application) { private var apkLite: ApkLite? = null private var sourceUri: Uri? = null private var sourcePath: String? = null + private var stagedSourceFile: File? = null private var grantPermissionsAfterInstall = false private var deleteSourceAfterInstall = false + private var useShizuku = false fun preCheck(intent: Intent): Boolean { - if (!Shizuku.pingBinder() - || Shizuku.checkSelfPermission() != PackageManager.PERMISSION_GRANTED - || Shizuku.checkRemotePermission(Manifest.permission.INSTALL_PACKAGES) - != PackageManager.PERMISSION_GRANTED - ) { - installResult.value = InstallAborted( - ABORT_SHIZUKU, - context.packageManager.getLaunchIntentForPackage( - ShizukuProvider.MANAGER_APPLICATION_ID - ) - ) - return false - } - this.intent = intent sourceUri = intent.data ?: intent.getParcelableExtra(Intent.EXTRA_STREAM) sourcePath = null Log.v(TAG, "Intent: $intent") - Hook.init(context) - packageManager = Hook.pm - packageInstaller = Hook.installer - Hook.disableAdbVerify(context) + return try { + currentUserPackageManager = + context.createPackageContext(context.packageName, 0).packageManager + if (hasShizukuInstallPermission()) { + Hook.init(context) + packageManager = Hook.pm + packageInstaller = Hook.installer + useShizuku = true + try { + Hook.disableAdbVerify(context) + } catch (e: Exception) { + Log.w(TAG, "Could not disable ADB install verification", e) + } + } else { + packageManager = context.packageManager + packageInstaller = packageManager.packageInstaller + useShizuku = false + } + installResult.value = InstallParse() + true + } catch (e: Exception) { + installResult.value = failureStage(e) + false + } catch (e: LinkageError) { + installResult.value = failureStage(e) + false + } + } + + private fun hasShizukuInstallPermission(): Boolean { + return hasShizukuPermission(Manifest.permission.INSTALL_PACKAGES) + } + + private fun hasShizukuPermission(permission: String): Boolean { + return try { + Shizuku.pingBinder() + && Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED + && (Shizuku.checkRemotePermission(permission) + == PackageManager.PERMISSION_GRANTED) + } catch (e: Exception) { + Log.w(TAG, "Could not verify Shizuku permission $permission", e) + false + } catch (e: LinkageError) { + Log.w(TAG, "Could not verify Shizuku permission $permission", e) + false + } + } + + private fun getPackageInfo(packageName: String, flags: Int): PackageInfo { + val includesAnyUser = flags and MATCH_ANY_USER != 0 + val currentUserFlags = flags and MATCH_ANY_USER.inv() + if (includesAnyUser && + (!useShizuku || !hasShizukuPermission(INTERACT_ACROSS_USERS)) + ) { + Log.i(TAG, "Querying $packageName for the current user; cross-user access is unavailable") + return currentUserPackageManager.getPackageInfo(packageName, currentUserFlags) + } + + return try { + packageManager.getPackageInfo(packageName, flags) + } catch (e: SecurityException) { + if (!includesAnyUser && !useShizuku) throw e + Log.w( + TAG, + "Package query for $packageName was denied; retrying for the current user", + e + ) + currentUserPackageManager.getPackageInfo(packageName, currentUserFlags) + } + } + + private fun getApplicationInfo(packageName: String, flags: Int): android.content.pm.ApplicationInfo { + val includesAnyUser = flags and MATCH_ANY_USER != 0 + val currentUserFlags = flags and MATCH_ANY_USER.inv() + if (includesAnyUser && + (!useShizuku || !hasShizukuPermission(INTERACT_ACROSS_USERS)) + ) { + Log.i(TAG, "Querying $packageName for the current user; cross-user access is unavailable") + return currentUserPackageManager.getApplicationInfo(packageName, currentUserFlags) + } + + return try { + packageManager.getApplicationInfo(packageName, flags) + } catch (e: SecurityException) { + if (!includesAnyUser && !useShizuku) throw e + Log.w( + TAG, + "Application query for $packageName was denied; retrying for the current user", + e + ) + currentUserPackageManager.getApplicationInfo(packageName, currentUserFlags) + } + } + + private fun resolveContentProvider(authority: String): android.content.pm.ProviderInfo? { + return try { + packageManager.resolveContentProvider(authority, 0) + } catch (e: SecurityException) { + Log.w(TAG, "Could not resolve provider $authority; retrying with the current-user manager", e) + currentUserPackageManager.resolveContentProvider(authority, 0) + } + } - installResult.value = InstallParse() - return true + private fun getInstallerPackageName(packageName: String): String? { + return try { + packageManager.getInstallerPackageName(packageName) + } catch (e: SecurityException) { + Log.w( + TAG, + "Could not query installer for $packageName; retrying with the current-user manager", + e + ) + currentUserPackageManager.getInstallerPackageName(packageName) + } + } + + private fun getInstallSessions(): List { + val privilegedCrossUserQuery = + useShizuku && hasShizukuPermission(INTERACT_ACROSS_USERS) + val currentUserInstaller = if (privilegedCrossUserQuery) { + packageInstaller + } else { + currentUserPackageManager.packageInstaller + } + return try { + currentUserInstaller.allSessions + } catch (e: SecurityException) { + if (currentUserInstaller === packageInstaller) { + Log.w(TAG, "Could not query privileged install sessions; retrying for current user", e) + try { + currentUserPackageManager.packageInstaller.allSessions + } catch (fallbackError: SecurityException) { + Log.w(TAG, "Could not query current-user install sessions", fallbackError) + emptyList() + } + } else { + Log.w(TAG, "Could not query current-user install sessions", e) + emptyList() + } + } + } + + private fun getInstallSession(sessionId: Int): SessionInfo? { + val privilegedCrossUserQuery = + useShizuku && hasShizukuPermission(INTERACT_ACROSS_USERS) + val currentUserInstaller = if (privilegedCrossUserQuery) { + packageInstaller + } else { + currentUserPackageManager.packageInstaller + } + return try { + currentUserInstaller.getSessionInfo(sessionId) + } catch (e: SecurityException) { + if (currentUserInstaller === packageInstaller) { + Log.w(TAG, "Could not query privileged session $sessionId; retrying for current user", e) + try { + currentUserPackageManager.packageInstaller.getSessionInfo(sessionId) + } catch (fallbackError: SecurityException) { + Log.w(TAG, "Could not query current-user session $sessionId", fallbackError) + null + } + } else { + Log.w(TAG, "Could not query current-user session $sessionId", e) + null + } + } } fun parseUri() { + try { + parseUriInternal() + } catch (e: Exception) { + reportUnexpectedFailure(e) + } catch (e: LinkageError) { + reportUnexpectedFailure(e) + } + } + + private fun parseUriInternal() { var uri = intent.data if (uri == null) { uri = intent.getParcelableExtra(Intent.EXTRA_STREAM) @@ -135,12 +301,53 @@ class InstallRepository(private val context: Application) { commit: Boolean, full: Boolean, removeSplit: Boolean + ) { + try { + installInternal( + setInstaller, + grantPermissions, + deleteSource, + commit, + full, + removeSplit + ) + } catch (e: Exception) { + cleanupInstall() + reportUnexpectedFailure(e) + } catch (e: LinkageError) { + cleanupInstall() + reportUnexpectedFailure(e) + } catch (t: Throwable) { + if (t is VirtualMachineError || t is ThreadDeath) throw t + cleanupInstall() + reportUnexpectedFailure(t) + } + } + + private fun installInternal( + setInstaller: Boolean, + grantPermissions: Boolean, + deleteSource: Boolean, + commit: Boolean, + full: Boolean, + removeSplit: Boolean ) { val uri = intent.data if (sourceUri != uri) sourcePath = null sourceUri = uri grantPermissionsAfterInstall = grantPermissions deleteSourceAfterInstall = deleteSource + if (useShizuku && !hasShizukuInstallPermission()) { + throw SecurityException("Shizuku disconnected before the installation started") + } + if (!useShizuku && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && + !packageManager.canRequestPackageInstalls() + ) { + throw SecurityException( + "Allow this app to install unknown apps in Android settings, then retry" + ) + } installResult.postValue(InstallInstalling(apkLite!!)) if (ContentResolver.SCHEME_CONTENT != uri?.scheme && ContentResolver.SCHEME_FILE != uri?.scheme @@ -153,36 +360,49 @@ class InstallRepository(private val context: Application) { try { val params: SessionParams = createSessionParams(setInstaller, full) stagedSessionId = packageInstaller.createSession(params) - } catch (e: IOException) { + } catch (e: Exception) { Log.e(TAG, "Failed to create a staging session", e) - installResult.postValue(InstallAborted(ABORT_CREATE)) + reportUnexpectedFailure(e) return } } try { val session = packageInstaller.openSession(stagedSessionId) - session.wrap() - if (removeSplit) { - session.removeSplit(apkLite!!.splitName!!) - } else { - context.contentResolver.openAssetFileDescriptor(uri, "r")?.use { afd -> - captureSourcePath(afd.parcelFileDescriptor.fd) - if (apkLite!!.zip) { - PackageUtil.stageZip(session, afd) { - stagingProgress.postValue(it) - } - } else { - PackageUtil.stageApk(session, afd) { - stagingProgress.postValue(it) + try { + if (useShizuku) session.wrap() + if (removeSplit) { + session.removeSplit(apkLite!!.splitName!!) + } else { + val afd = stagedSourceFile?.let(::openStagedSourceDescriptor) + ?: context.contentResolver.openAssetFileDescriptor(uri, "r") + ?: throw IOException("Could not open the selected package file") + afd.use { + captureSourcePath(it.parcelFileDescriptor.fd) + if (apkLite!!.zip) { + PackageUtil.stageZip(session, it) { + stagingProgress.postValue(it) + } + } else { + PackageUtil.stageApk(session, it) { + stagingProgress.postValue(it) + } } } } + } finally { + session.close() } } catch (e: Exception) { cleanupInstall() Log.e(TAG, "Could not stage APK.", e) - installResult.postValue(InstallAborted(ABORT_WRITE)) + reportUnexpectedFailure(e) + return + } catch (t: Throwable) { + if (t is VirtualMachineError || t is ThreadDeath) throw t + cleanupInstall() + Log.e(TAG, "Could not stream package into the install session.", t) + reportUnexpectedFailure(t) return } @@ -198,15 +418,15 @@ class InstallRepository(private val context: Application) { Log.v(TAG, "content URI: $uri") if (ContentResolver.SCHEME_CONTENT == uri.scheme) { uri.authority?.let { authority -> - packageManager.resolveContentProvider(authority, 0)?.also { info -> + resolveContentProvider(authority)?.also { info -> callingUid = info.applicationInfo.uid } } } val apk = try { - context.contentResolver.openAssetFileDescriptor(uri, "r")?.use { afd -> + openPackageDescriptor(uri)?.use { afd -> captureSourcePath(afd.parcelFileDescriptor.fd) - PackageUtil.parseZipFromFd(afd) + PackageUtil.parseZipFromFd(context, afd, stagedSourceFile) } } catch (e: Exception) { Log.e(TAG, "Failed to parse APK from content URI: $uri", e) @@ -216,27 +436,29 @@ class InstallRepository(private val context: Application) { return InstallAborted(ABORT_PARSE) } val old = try { - packageManager.getPackageInfo(apk.packageName, PackageManager_rename.MATCH_KNOWN_PACKAGES) + getPackageInfo(apk.packageName, MATCH_KNOWN_PACKAGES) } catch (_: PackageManager.NameNotFoundException) { null } var full = true if (apk.isSplit()) { - for (item in packageInstaller.allSessions) { + for (item in getInstallSessions()) { if (item.isActive && item.appPackageName == apk.packageName) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - // noinspection NewApi - if (item.installerUid != Shizuku.getUid()) { - continue + if (useShizuku && hasShizukuPermission(INTERACT_ACROSS_USERS)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + // noinspection NewApi + if (item.installerUid != Shizuku.getUid()) continue + } else { + if (item.installReason != PackageManager.INSTALL_REASON_USER) continue + item as PackageInstaller_rename.SessionInfo + if (item.installFlags and INSTALL_FROM_ADB == 0) { + continue + } } } else { - if (item.installReason != PackageManager.INSTALL_REASON_USER) continue - item as PackageInstaller_rename.SessionInfo - if (item.installFlags and PackageManager_rename.INSTALL_FROM_ADB == 0) { - continue - } + if (item.installerPackageName != context.packageName) continue } - val info = packageInstaller.getSessionInfo(item.sessionId)!! + val info = getInstallSession(item.sessionId) ?: continue stagedSessionId = info.sessionId full = info.mode == SessionParams.MODE_FULL_INSTALL apk.label = info.appLabel as String? @@ -260,7 +482,7 @@ class InstallRepository(private val context: Application) { apk.icon = old.applicationInfo!!.loadIcon(packageManager) } // use this field to store the installer package name - old.sharedUserId = packageManager.getInstallerPackageName(apk.packageName) + old.sharedUserId = getInstallerPackageName(apk.packageName) } if (apk.label == null && ContentResolver.SCHEME_CONTENT == uri.scheme) { context.contentResolver.query(uri, null, null, null, null).use { cursor -> @@ -279,9 +501,53 @@ class InstallRepository(private val context: Application) { return InstallUserAction(apk, old, full, skipCreate) } + private fun openPackageDescriptor(uri: Uri): AssetFileDescriptor? { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { + return context.contentResolver.openAssetFileDescriptor(uri, "r") + } + + val cachedFile = File.createTempFile("installer-source-", ".apk", context.cacheDir) + try { + val input = when (uri.scheme) { + ContentResolver.SCHEME_FILE -> { + val path = uri.path ?: throw IOException("Package file URI has no path") + File(path).inputStream() + } + ContentResolver.SCHEME_CONTENT -> + context.contentResolver.openInputStream(uri) + ?: throw IOException("Could not read the selected package") + else -> throw IOException("Unsupported package URI: $uri") + } + input.use { source -> + cachedFile.outputStream().use { destination -> + source.copyTo(destination) + } + } + if (cachedFile.length() == 0L) { + throw IOException("The selected package file is empty") + } + + val descriptor = openStagedSourceDescriptor(cachedFile) + stagedSourceFile?.delete() + stagedSourceFile = cachedFile + return descriptor + } catch (e: Exception) { + cachedFile.delete() + throw e + } catch (e: LinkageError) { + cachedFile.delete() + throw e + } + } + + private fun openStagedSourceDescriptor(file: File): AssetFileDescriptor { + val descriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY) + return AssetFileDescriptor(descriptor, 0, file.length()) + } + private fun processPackageUri(packageName: String): InstallStage { val info = try { - packageManager.getPackageInfo(packageName, PackageManager_rename.MATCH_KNOWN_PACKAGES) + getPackageInfo(packageName, MATCH_KNOWN_PACKAGES) } catch (e: PackageManager.NameNotFoundException) { Log.e(TAG, "Requested package not available.", e) return InstallAborted(ABORT_NOTFOUND) @@ -337,15 +603,18 @@ class InstallRepository(private val context: Application) { pm.installExistingPackage(apkLite!!.packageName, PackageManager.INSTALL_REASON_USER) setStageBasedOnResult( PackageInstaller.STATUS_SUCCESS, - PackageManager_rename.INSTALL_SUCCEEDED, + INSTALL_SUCCEEDED, null ) } catch (e: PackageManager.NameNotFoundException) { setStageBasedOnResult( PackageInstaller.STATUS_FAILURE, - PackageManager_rename.INSTALL_FAILED_INTERNAL_ERROR, + INSTALL_FAILED_INTERNAL_ERROR, e.localizedMessage ) + } catch (e: Exception) { + Log.e(TAG, "Could not install existing package", e) + reportUnexpectedFailure(e) } } @@ -357,9 +626,9 @@ class InstallRepository(private val context: Application) { } val params = SessionParams(mode) var installer = context.packageName - if (setInstaller) { + if (setInstaller && useShizuku) { try { - packageManager.getPackageInfo("com.android.vending", PackageManager.MATCH_SYSTEM_ONLY) + getPackageInfo("com.android.vending", PackageManager.MATCH_SYSTEM_ONLY) installer = "com.android.vending" } catch (_: PackageManager.NameNotFoundException) { installer = "com.android.shell" @@ -369,17 +638,24 @@ class InstallRepository(private val context: Application) { } } else { val referrerUri: Uri? = intent.getParcelableExtra(Intent.EXTRA_REFERRER) - params.setReferrerUri(referrerUri) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + params.setReferrerUri(referrerUri) + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val source = if (referrerUri != null) PackageInstaller.PACKAGE_SOURCE_DOWNLOADED_FILE else PackageInstaller.PACKAGE_SOURCE_LOCAL_FILE params.setPackageSource(source) } params.setOriginatingUri(intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI)) - params.setOriginatingUid(intent.getIntExtra(Intent_rename.EXTRA_ORIGINATING_UID, callingUid)) + if (useShizuku) { + params.setOriginatingUid( + intent.getIntExtra(Intent_rename.EXTRA_ORIGINATING_UID, callingUid) + ) + } + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && useShizuku) { + params.setInstallerPackageName(installer) } - // noinspection NewApi - params.setInstallerPackageName(installer) params.setInstallReason(PackageManager.INSTALL_REASON_USER) params.setAppPackageName(apkLite!!.packageName) if (apkLite!!.needSplit()) { @@ -387,57 +663,164 @@ class InstallRepository(private val context: Application) { params.setAppLabel(apkLite!!.label) } - val p = params as PackageInstaller_rename.SessionParams - p.installFlags = p.installFlags or PackageManager_rename.INSTALL_ALLOW_TEST - p.installFlags = p.installFlags or PackageManager_rename.INSTALL_GRANT_ALL_REQUESTED_PERMISSIONS - p.installFlags = p.installFlags or PackageManager_rename.INSTALL_REPLACE_EXISTING - p.installFlags = p.installFlags or PackageManager_rename.INSTALL_REQUEST_DOWNGRADE - p.installFlags = p.installFlags or PackageManager_rename.INSTALL_FULL_APP - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - p.installFlags = p.installFlags or PackageManager_rename.INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK - p.installFlags = p.installFlags or PackageManager_rename.INSTALL_REQUEST_UPDATE_OWNERSHIP + if (useShizuku) { + val p = params as PackageInstaller_rename.SessionParams + p.installFlags = p.installFlags or INSTALL_ALLOW_TEST + p.installFlags = p.installFlags or INSTALL_GRANT_ALL_REQUESTED_PERMISSIONS + p.installFlags = p.installFlags or INSTALL_REPLACE_EXISTING + p.installFlags = p.installFlags or INSTALL_REQUEST_DOWNGRADE + p.installFlags = p.installFlags or INSTALL_FULL_APP + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + p.installFlags = p.installFlags or INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK + p.installFlags = p.installFlags or INSTALL_REQUEST_UPDATE_OWNERSHIP + } } return params } private fun commit() { - val receiver = LocalIntentReceiver(::setStageBasedOnResult) + var resultReceiver: InstallResultSender? = null try { + val callbackReceiver = createInstallResultSender { status, legacyStatus, message, resultIntent -> + try { + if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) { + launchPendingUserAction(resultIntent) + } else { + setStageBasedOnResult(status, legacyStatus, message) + } + } catch (e: Exception) { + reportUnexpectedFailure(e) + } catch (e: LinkageError) { + reportUnexpectedFailure(e) + } catch (t: Throwable) { + if (t is VirtualMachineError || t is ThreadDeath) throw t + reportUnexpectedFailure(t) + } + } + resultReceiver = callbackReceiver val session = packageInstaller.openSession(stagedSessionId) - session.wrap() - // noinspection RequestInstallPackagesPolicy - session.commit(receiver.intentSender as IntentSender) + try { + if (useShizuku) session.wrap() + // noinspection RequestInstallPackagesPolicy + session.commit(callbackReceiver.intentSender) + } finally { + session.close() + } } catch (e: Exception) { Log.e(TAG, "Failed to commit staged session", e) cleanupInstall() - setStageBasedOnResult( - PackageInstaller.STATUS_FAILURE, - PackageManager_rename.INSTALL_FAILED_INTERNAL_ERROR, - e.localizedMessage - ) + resultReceiver?.close?.invoke() + reportUnexpectedFailure(e) + } catch (e: LinkageError) { + Log.e(TAG, "Failed to create or commit install result receiver", e) + cleanupInstall() + resultReceiver?.close?.invoke() + reportUnexpectedFailure(e) + } + } + + private fun createInstallResultSender( + callback: (Int, Int, String?, Intent) -> Unit + ): InstallResultSender { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + try { + val helper = Class.forName( + "io.i10nstaller.model.ModernInstallResultSender" + ) + val sender = helper.getMethod( + "create", + kotlin.jvm.functions.Function4::class.java + ).invoke(null, callback) as IntentSender + return InstallResultSender(sender) {} + } catch (e: LinkageError) { + Log.w(TAG, "Hidden install-result receiver is unavailable; using PendingIntent", e) + } catch (e: Exception) { + Log.w(TAG, "Could not create hidden install-result receiver; using PendingIntent", e) + } } + + val receiver = PendingIntentInstallReceiver(context, callback) + return InstallResultSender(receiver.pendingIntent.intentSender, receiver::close) + } + + private data class InstallResultSender( + val intentSender: IntentSender, + val close: () -> Unit + ) + + private fun launchPendingUserAction(resultIntent: Intent) { + val confirmationIntent = resultIntent.getParcelableExtra(Intent.EXTRA_INTENT) + ?: throw IllegalStateException("Package installer did not provide a confirmation screen") + confirmationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(confirmationIntent) } private fun setStageBasedOnResult(statusCode: Int, legacyStatus: Int, message: String?) { + try { + setStageBasedOnResultInternal(statusCode, legacyStatus, message) + } catch (e: Exception) { + reportUnexpectedFailure(e) + } catch (e: LinkageError) { + reportUnexpectedFailure(e) + } + } + + private fun setStageBasedOnResultInternal(statusCode: Int, legacyStatus: Int, message: String?) { + val installedApk = apkLite + if (installedApk == null) { + reportUnexpectedFailure(IllegalStateException("Install result received without a package")) + return + } if (statusCode == PackageInstaller.STATUS_SUCCESS) { - val packageName = apkLite!!.packageName + val packageName = installedApk.packageName val installedSource = sourceUri - val launchIntent = packageManager.getLaunchIntentForPackage(packageName) - installResult.postValue(InstallSuccess(apkLite!!, launchIntent)) + val launchIntent = try { + packageManager.getLaunchIntentForPackage(packageName) + } catch (e: Exception) { + Log.w(TAG, "Could not resolve launch activity for $packageName", e) + null + } + installResult.postValue(InstallSuccess(installedApk, launchIntent)) postInstallScope.launch { if (grantPermissionsAfterInstall) grantPostInstallAccess(packageName) if (deleteSourceAfterInstall) deleteSourceApk(installedSource) } } else { - installResult.postValue(InstallFailed(apkLite!!, legacyStatus, statusCode, message)) + installResult.postValue(InstallFailed(installedApk, legacyStatus, statusCode, message)) + } + } + + fun reportUnexpectedFailure(error: Throwable) { + installResult.postValue(failureStage(error)) + } + + private fun failureStage(error: Throwable): InstallStage { + Log.e(TAG, "Install operation failed", error) + val message = error.localizedMessage + ?.takeIf { it.isNotBlank() } + ?: error.javaClass.simpleName + val currentApk = apkLite + return if (currentApk != null) { + InstallFailed( + currentApk, + INSTALL_FAILED_INTERNAL_ERROR, + PackageInstaller.STATUS_FAILURE, + message + ) + } else { + InstallAborted(ABORT_FAILURE, message = message) } } private fun grantPostInstallAccess(packageName: String) { + if (!useShizuku || !hasShizukuInstallPermission()) { + Log.w(TAG, "Skipping post-install permissions because Shizuku is unavailable") + return + } if (!packageName.matches(Regex("[A-Za-z0-9_.]+"))) return val uid = try { - packageManager.getApplicationInfo(packageName, 0).uid + getApplicationInfo(packageName, 0).uid } catch (e: PackageManager.NameNotFoundException) { Log.w(TAG, "Could not resolve UID for $packageName", e) return @@ -955,6 +1338,10 @@ class InstallRepository(private val context: Application) { logCleanupOutput: Boolean = false ): Boolean { try { + if (!hasShizukuInstallPermission()) { + Log.w(TAG, "Skipping Shizuku operation because permission is unavailable: $operation") + return false + } val factory = Shizuku::class.java.getDeclaredMethod( "newProcess", Array::class.java, @@ -985,12 +1372,19 @@ class InstallRepository(private val context: Application) { private fun shellQuote(value: String): String = "'${value.replace("'", "'\\''")}'" fun cleanupInstall() { - if (stagedSessionId > 0) { + if (stagedSessionId != SessionInfo.INVALID_ID) { try { packageInstaller.abandonSession(stagedSessionId) - } catch (_: SecurityException) { + } catch (e: Exception) { + Log.w(TAG, "Could not abandon install session $stagedSessionId", e) } - stagedSessionId = 0 + stagedSessionId = SessionInfo.INVALID_ID + } + stagedSourceFile?.let { file -> + if (!file.delete()) { + Log.w(TAG, "Could not delete staged package ${file.absolutePath}") + } + stagedSourceFile = null } } @@ -1066,39 +1460,61 @@ class InstallRepository(private val context: Application) { intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) if (uninstall) { - val receiver = LocalIntentReceiver { statusCode, legacyCode, msg -> - if (statusCode == PackageInstaller.STATUS_SUCCESS) { - installResult.postValue(InstallSuccess(apkLite!!, intent, path)) - } else { - installResult.postValue(InstallSuccess(apkLite!!, intent, "$path\n\n$msg")) - } - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { - try { - // noinspection MissingPermission - packageInstaller.requestArchive( - info.packageName, - receiver.intentSender as IntentSender - ) - return - } catch (e: PackageManager.NameNotFoundException) { - Log.w(TAG, "Failed to archive for package ${info.packageName}", e) + var resultReceiver: InstallResultSender? = null + try { + val callbackReceiver = + createInstallResultSender { statusCode, legacyCode, msg, resultIntent -> + try { + if (statusCode == PackageInstaller.STATUS_PENDING_USER_ACTION) { + launchPendingUserAction(resultIntent) + } else if (statusCode == PackageInstaller.STATUS_SUCCESS) { + installResult.postValue(InstallSuccess(apkLite!!, intent, path)) + } else { + installResult.postValue( + InstallSuccess(apkLite!!, intent, "$path\n\n$msg") + ) + } + } catch (e: Exception) { + reportUnexpectedFailure(e) + } catch (e: LinkageError) { + reportUnexpectedFailure(e) + } + } + resultReceiver = callbackReceiver + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + try { + // noinspection MissingPermission + packageInstaller.requestArchive( + info.packageName, + callbackReceiver.intentSender + ) + return + } catch (e: PackageManager.NameNotFoundException) { + Log.w(TAG, "Failed to archive for package ${info.packageName}", e) + } } + // noinspection MissingPermission NewApi + packageInstaller.uninstall( + VersionedPackage(info.packageName, info.longVersionCode), + DELETE_KEEP_DATA, + callbackReceiver.intentSender + ) + val userId = Os.getuid() / 100000 + val pm = packageManager as PackageManager_rename + pm.deleteApplicationCacheFilesAsUser(info.packageName, userId, null) + } catch (e: Exception) { + resultReceiver?.close?.invoke() + reportUnexpectedFailure(e) + } catch (e: LinkageError) { + resultReceiver?.close?.invoke() + reportUnexpectedFailure(e) } - // noinspection MissingPermission NewApi - packageInstaller.uninstall( - VersionedPackage(info.packageName, info.longVersionCode), - PackageManager_rename.DELETE_KEEP_DATA, - receiver.intentSender as IntentSender - ) - val userId = Os.getuid() / 100000 - val pm = packageManager as PackageManager_rename - pm.deleteApplicationCacheFilesAsUser(info.packageName, userId, null) } else { installResult.postValue(InstallSuccess(apkLite!!, intent, path)) } } + fun setPackageEnabled(packageName: String, enabled: Boolean) { val newState = if (enabled) { PackageManager.COMPONENT_ENABLED_STATE_ENABLED diff --git a/app/src/main/java/io/i10nstaller/model/InstallStages.kt b/app/src/main/java/io/i10nstaller/model/InstallStages.kt index 642c263..db67038 100644 --- a/app/src/main/java/io/i10nstaller/model/InstallStages.kt +++ b/app/src/main/java/io/i10nstaller/model/InstallStages.kt @@ -49,6 +49,7 @@ class InstallFailed( class InstallAborted( val abortReason: Int, val intent: Intent? = null, + val message: String? = null, ) : InstallStage(STAGE_ABORTED) { companion object { @@ -60,5 +61,6 @@ class InstallAborted( const val ABORT_CREATE = 5 const val ABORT_WRITE = 6 const val ABORT_INFO = 7 + const val ABORT_FAILURE = 8 } } diff --git a/app/src/main/java/io/i10nstaller/model/LocalIntentReceiver.java b/app/src/main/java/io/i10nstaller/model/LocalIntentReceiver.java index 6c642a7..dbe1b1c 100644 --- a/app/src/main/java/io/i10nstaller/model/LocalIntentReceiver.java +++ b/app/src/main/java/io/i10nstaller/model/LocalIntentReceiver.java @@ -5,12 +5,12 @@ import android.content.Intent; import android.content.IntentSender_rename; import android.content.pm.PackageInstaller; -import android.content.pm.PackageInstaller_rename; -import android.content.pm.PackageManager_rename; import android.os.Bundle; import android.os.IBinder; public class LocalIntentReceiver { + private static final String EXTRA_LEGACY_STATUS = "android.content.pm.extra.LEGACY_STATUS"; + private static final int INSTALL_FAILED_INTERNAL_ERROR = -110; private final ResultObserver mResultObserver; private final IIntentSender.Stub mLocalSender = new IIntentSender.Stub() { @@ -19,9 +19,9 @@ public void send(int code, Intent intent, String resolvedType, IBinder whitelist IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) { int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE); String statusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE); - int legacyStatus = intent.getIntExtra(PackageInstaller_rename.EXTRA_LEGACY_STATUS, - PackageManager_rename.INSTALL_FAILED_INTERNAL_ERROR); - mResultObserver.onResult(status, legacyStatus, statusMessage); + int legacyStatus = intent.getIntExtra(EXTRA_LEGACY_STATUS, + INSTALL_FAILED_INTERNAL_ERROR); + mResultObserver.onResult(status, legacyStatus, statusMessage, intent); } }; @@ -34,6 +34,6 @@ public IntentSender_rename getIntentSender() { } public interface ResultObserver { - void onResult(int status, int legacyStatus, String message); + void onResult(int status, int legacyStatus, String message, Intent resultIntent); } } diff --git a/app/src/main/java/io/i10nstaller/model/ModernInstallResultSender.kt b/app/src/main/java/io/i10nstaller/model/ModernInstallResultSender.kt new file mode 100644 index 0000000..077fa83 --- /dev/null +++ b/app/src/main/java/io/i10nstaller/model/ModernInstallResultSender.kt @@ -0,0 +1,14 @@ +package io.i10nstaller.model + +import android.content.Intent +import android.content.IntentSender + +internal object ModernInstallResultSender { + @JvmStatic + fun create( + callback: (Int, Int, String?, Intent) -> Unit + ): IntentSender { + val receiver = LocalIntentReceiver(callback::invoke) + return receiver.intentSender as IntentSender + } +} diff --git a/app/src/main/java/io/i10nstaller/model/PackageUtil.kt b/app/src/main/java/io/i10nstaller/model/PackageUtil.kt index 1d46f6e..fa76faf 100644 --- a/app/src/main/java/io/i10nstaller/model/PackageUtil.kt +++ b/app/src/main/java/io/i10nstaller/model/PackageUtil.kt @@ -4,6 +4,7 @@ import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo import android.content.pm.PackageInstaller import android.content.pm.PackageManager +import android.content.Context import android.content.res.ApkAssets import android.content.res.AssetFileDescriptor import android.content.res.AssetManager @@ -13,7 +14,9 @@ import android.content.res.XmlBlock import android.content.res.XmlResourceParser import android.graphics.drawable.Drawable import android.os.Build +import android.os.Build.VERSION_CODES import android.util.DisplayMetrics +import android.util.Log import org.apache.commons.compress.archivers.zip.ZipArchiveEntry import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream @@ -25,11 +28,14 @@ import org.xmlpull.v1.XmlPullParser import java.io.File import java.io.FileDescriptor import java.io.FileInputStream +import java.io.FileOutputStream import java.io.IOException import java.util.Locale import java.util.function.Consumer object PackageUtil { + private const val TAG = "PackageUtil" + fun getApkLite(pm: PackageManager, pkgInfo: PackageInfo): ApkLite { val info = pkgInfo.applicationInfo ?: ApplicationInfo().apply { packageName = pkgInfo.packageName @@ -48,22 +54,55 @@ object PackageUtil { ).apply { zip = true } } - fun parseZipFromFd(afd: AssetFileDescriptor): ApkLite? { + fun parseZipFromFd( + context: Context, + afd: AssetFileDescriptor, + archivePath: File? = null + ): ApkLite? { ZipFile.builder() .setIgnoreLocalFileHeader(true) .setSeekableByteChannel(afd.createInputStream().getChannel()) .get().use { zipFile -> val xml = zipFile.getEntry("AndroidManifest.xml") if (xml != null) { - try { - return getAppLiteFromApkFd( + val apkLite = try { + getAppLiteFromApkFd( afd.getFileDescriptor(), - afd.toString(), afd.getLength() + afd.toString(), + afd.getLength() ) - } catch (_: IOException) { - zipFile.getInputStream(xml).use { input -> - val bytes = IOUtils.toByteArray(input) - return getAppLiteFromXmlBytes(bytes) + } catch (e: Exception) { + Log.w( + TAG, + "Could not load APK assets from file descriptor; parsing manifest bytes", + e + ) + null + } catch (e: LinkageError) { + Log.w(TAG, "APK assets API is unavailable; parsing manifest bytes", e) + null + } + if (apkLite != null) { + resolveArchiveResources( + context, + afd, + apkLite, + archivePath, + Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q + ) + return apkLite + } + + zipFile.getInputStream(xml).use { input -> + val bytes = IOUtils.toByteArray(input) + return getAppLiteFromXmlBytes(bytes)?.also { + resolveArchiveResources( + context, + afd, + it, + archivePath, + Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q + ) } } } @@ -102,6 +141,61 @@ object PackageUtil { } + private fun resolveArchiveResources( + context: Context, + afd: AssetFileDescriptor, + apkLite: ApkLite, + archivePath: File?, + force: Boolean + ) { + if (!force && + !apkLite.label.isNullOrBlank() && + !apkLite.label!!.startsWith("@") && + apkLite.icon != null + ) { + return + } + + var temporaryArchive: File? = null + try { + val archiveFile = archivePath ?: File.createTempFile( + "installer-archive-", + ".apk", + context.cacheDir + ).also { temporaryArchive = it } + if (archivePath == null) { + afd.createInputStream().use { input -> + FileOutputStream(archiveFile).use { output -> + input.copyTo(output) + } + } + } + + val packageManager = context.packageManager + val archiveInfo = packageManager.getPackageArchiveInfo( + archiveFile.absolutePath, + PackageManager.GET_META_DATA + ) ?: return + val applicationInfo = archiveInfo.applicationInfo ?: return + applicationInfo.sourceDir = archiveFile.absolutePath + applicationInfo.publicSourceDir = archiveFile.absolutePath + + val label = applicationInfo.loadLabel(packageManager)?.toString() + ?.takeIf { it.isNotBlank() && !it.startsWith("@") } + ?: archiveInfo.packageName + if (force || apkLite.label.isNullOrBlank() || apkLite.label!!.startsWith("@")) { + apkLite.label = label + } + applicationInfo.loadIcon(packageManager)?.let { apkLite.icon = it } + } catch (e: Exception) { + Log.w(TAG, "Could not resolve app label and icon from APK archive resources", e) + } catch (e: LinkageError) { + Log.w(TAG, "APK archive metadata API is unavailable", e) + } finally { + temporaryArchive?.delete() + } + } + private fun getAppLiteFromXmlBytesLink(xmlBytes: ByteArray): ApkLite? { XmlBlock(xmlBytes).use { it.newParser().use { parser -> @@ -130,11 +224,7 @@ object PackageUtil { } private fun getAppLiteFromApkFd(fd: FileDescriptor, name: String, length: Long): ApkLite? { - val assets = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - ApkAssets.loadFromFd(fd, name, 0, length, 0, null) - } else { - ApkAssets.loadFromFd(fd, name, false, false) - } + val assets = loadApkAssetsFromFd(fd, name, length) val am = `AssetManager$Builder`() .addApkAssets(assets) .build() as AssetManager @@ -147,6 +237,61 @@ object PackageUtil { } } + private fun loadApkAssetsFromFd( + fd: FileDescriptor, + friendlyName: String, + length: Long + ): ApkAssets { + val method = ApkAssets::class.java.declaredMethods + .filter { candidate -> + candidate.name == "loadFromFd" && + java.lang.reflect.Modifier.isStatic(candidate.modifiers) && + isSupportedLoadFromFdSignature(candidate.parameterTypes) + } + .minByOrNull { candidate -> loadFromFdPriority(candidate.parameterTypes) } + ?: throw NoSuchMethodException("No supported ApkAssets.loadFromFd method") + method.isAccessible = true + + val parameters = method.parameterTypes + val arguments = when { + parameters.size == 6 -> arrayOf(fd, friendlyName, 0L, length, 0, null) + parameters.size == 4 && parameters[2] == Int::class.javaPrimitiveType -> + arrayOf(fd, friendlyName, 0, null) + parameters.size == 3 -> arrayOf(fd, friendlyName, 0) + else -> arrayOf(fd, friendlyName, false, false) + } + return method.invoke(null, *arguments) as ApkAssets + } + + private fun loadFromFdPriority(parameters: Array>): Int = + when { + parameters.size == 6 -> 0 + parameters.size == 4 && parameters[2] == Int::class.javaPrimitiveType -> 1 + parameters.size == 3 -> 2 + else -> 3 + } + + private fun isSupportedLoadFromFdSignature(parameters: Array>): Boolean { + if (parameters.size < 2 || + parameters[0] != FileDescriptor::class.java || + parameters[1] != String::class.java + ) { + return false + } + return when (parameters.size) { + 6 -> parameters[2] == Long::class.javaPrimitiveType && + parameters[3] == Long::class.javaPrimitiveType && + parameters[4] == Int::class.javaPrimitiveType && + parameters[5].simpleName == "AssetsProvider" + 4 -> (parameters[2] == Int::class.javaPrimitiveType && + parameters[3].simpleName == "AssetsProvider") || + (parameters[2] == Boolean::class.javaPrimitiveType && + parameters[3] == Boolean::class.javaPrimitiveType) + 3 -> parameters[2] == Int::class.javaPrimitiveType + else -> false + } + } + private fun parsingAndroidManifest(parser: XmlResourceParser, res: Resources): ApkLite? { val ns = "http://schemas.android.com/apk/res/android" var icon: Drawable? = null @@ -362,17 +507,29 @@ object PackageUtil { ) { callback.accept(0) val total = afd.getLength() - val instream = afd.createInputStream() var totalRead = 0L - val fileName = System.currentTimeMillis().toString() + val fileName = if (Build.VERSION.SDK_INT <= VERSION_CODES.Q) { + "base.apk" + } else { + System.currentTimeMillis().toString() + } session.openWrite(fileName, 0, total).use { out -> - val buffer = ByteArray(16 * 1024) - var numRead: Int - while (instream.read(buffer).also { numRead = it } != -1) { - out.write(buffer, 0, numRead) - totalRead += numRead - val fraction = totalRead.toFloat() / total.toFloat() - callback.accept((fraction * 100.0).toInt()) + afd.createInputStream().use { instream -> + val buffer = ByteArray(16 * 1024) + var numRead: Int + while (instream.read(buffer).also { numRead = it } != -1) { + out.write(buffer, 0, numRead) + totalRead += numRead + if (total > 0L) { + callback.accept((totalRead.toFloat() / total.toFloat() * 100.0).toInt()) + } + } + } + if (totalRead == 0L) throw IOException("The selected package contains no data") + if (total >= 0L && totalRead != total) { + throw IOException( + "The selected package stream was incomplete ($totalRead of $total bytes)" + ) } session.fsync(out) } diff --git a/app/src/main/java/io/i10nstaller/model/PendingIntentInstallReceiver.kt b/app/src/main/java/io/i10nstaller/model/PendingIntentInstallReceiver.kt new file mode 100644 index 0000000..9af7cdc --- /dev/null +++ b/app/src/main/java/io/i10nstaller/model/PendingIntentInstallReceiver.kt @@ -0,0 +1,87 @@ +package io.i10nstaller.model + +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageInstaller +import android.os.Build +import android.util.Log +import java.util.UUID + +internal class PendingIntentInstallReceiver( + context: Context, + private val onResult: (Int, Int, String?, Intent) -> Unit +) : AutoCloseable { + private val appContext = context.applicationContext + private val action = "${appContext.packageName}.INSTALL_RESULT.${UUID.randomUUID()}" + private var registered = false + private val receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val status = intent.getIntExtra( + PackageInstaller.EXTRA_STATUS, + PackageInstaller.STATUS_FAILURE + ) + val legacyStatus = intent.getIntExtra( + EXTRA_LEGACY_STATUS, + INSTALL_FAILED_INTERNAL_ERROR + ) + val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + try { + onResult(status, legacyStatus, message, intent) + } finally { + if (status != PackageInstaller.STATUS_PENDING_USER_ACTION) close() + } + } + } + + val pendingIntent: PendingIntent + + init { + val filter = IntentFilter(action) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + appContext.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED) + } else { + @Suppress("DEPRECATION") + appContext.registerReceiver(receiver, filter) + } + registered = true + + try { + val intent = Intent(action).setPackage(appContext.packageName) + val flags = PendingIntent.FLAG_UPDATE_CURRENT or + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0 + pendingIntent = PendingIntent.getBroadcast( + appContext, + action.hashCode(), + intent, + flags + ) + } catch (e: Exception) { + appContext.unregisterReceiver(receiver) + registered = false + throw e + } catch (e: LinkageError) { + appContext.unregisterReceiver(receiver) + registered = false + throw e + } + } + + override fun close() { + pendingIntent.cancel() + if (registered) { + try { + appContext.unregisterReceiver(receiver) + } catch (e: IllegalArgumentException) { + Log.w(TAG, "Install result receiver was already unregistered", e) + } + registered = false + } + } + + private companion object { + const val TAG = "InstallResultReceiver" + } +} diff --git a/app/src/main/java/io/i10nstaller/ui/InstallLaunch.kt b/app/src/main/java/io/i10nstaller/ui/InstallLaunch.kt index e60daee..0906bdf 100644 --- a/app/src/main/java/io/i10nstaller/ui/InstallLaunch.kt +++ b/app/src/main/java/io/i10nstaller/ui/InstallLaunch.kt @@ -166,14 +166,17 @@ class InstallLaunch : FragmentActivity() { } private fun showDialogInner(newDialog: DialogFragment?) { + if (fragmentManager.isDestroyed) return val currentDialog = fragmentManager.findFragmentByTag("dialog") as DialogFragment? if (currentDialog is BaseDialogFragment && newDialog is BaseDialogFragment && currentDialog.mDialogData.stageCode == newDialog.mDialogData.stageCode ) { return } - currentDialog?.dismissAllowingStateLoss() - newDialog?.show(fragmentManager, "dialog") + val transaction = fragmentManager.beginTransaction() + currentDialog?.let(transaction::remove) + newDialog?.let { transaction.add(it, "dialog") } + transaction.commitAllowingStateLoss() } private fun isOptionsIntent(intent: Intent): Boolean = diff --git a/app/src/main/java/io/i10nstaller/ui/fragments/ArchiveConfirmationFragment.java b/app/src/main/java/io/i10nstaller/ui/fragments/ArchiveConfirmationFragment.java index 875d909..117cddd 100644 --- a/app/src/main/java/io/i10nstaller/ui/fragments/ArchiveConfirmationFragment.java +++ b/app/src/main/java/io/i10nstaller/ui/fragments/ArchiveConfirmationFragment.java @@ -22,6 +22,12 @@ public ArchiveConfirmationFragment(PackageUserAction dialogData) { mDialogData = dialogData; } + @Override + public void onStart() { + super.onStart(); + focusPositiveButtonForDpad(); + } + @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { diff --git a/app/src/main/java/io/i10nstaller/ui/fragments/BaseDialogFragment.java b/app/src/main/java/io/i10nstaller/ui/fragments/BaseDialogFragment.java index 34e40d5..a0d1368 100644 --- a/app/src/main/java/io/i10nstaller/ui/fragments/BaseDialogFragment.java +++ b/app/src/main/java/io/i10nstaller/ui/fragments/BaseDialogFragment.java @@ -1,6 +1,11 @@ package io.i10nstaller.ui.fragments; +import android.app.AlertDialog; +import android.app.UiModeManager; +import android.content.Context; import android.content.DialogInterface; +import android.content.pm.PackageManager; +import android.content.res.Configuration; import android.os.Bundle; import androidx.annotation.NonNull; @@ -27,6 +32,50 @@ public void onCreate(@Nullable Bundle savedInstanceState) { .get(InstallViewModel.class); } + @Override + public void onStart() { + super.onStart(); + if (getDialog() instanceof AlertDialog alertDialog) { + boolean isTvDevice = isTvDevice(); + for (int which : new int[] { + AlertDialog.BUTTON_POSITIVE, + AlertDialog.BUTTON_NEGATIVE, + AlertDialog.BUTTON_NEUTRAL + }) { + var button = alertDialog.getButton(which); + if (button != null) { + button.setFocusable(true); + button.setFocusableInTouchMode( + isTvDevice && which == AlertDialog.BUTTON_POSITIVE); + button.setClickable(true); + } + } + } + } + + private boolean isTvDevice() { + var context = requireContext(); + var uiModeManager = + (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE); + return (uiModeManager != null + && uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) + || !context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN); + } + + protected void focusPositiveButtonForDpad() { + if (getDialog() instanceof AlertDialog alertDialog) { + var positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); + if (positiveButton != null) { + boolean isTvDevice = isTvDevice(); + positiveButton.post(() -> { + if (isTvDevice || !positiveButton.isInTouchMode()) { + positiveButton.requestFocus(); + } + }); + } + } + } + @Override public void onCancel(@NonNull DialogInterface dialog) { super.onCancel(dialog); diff --git a/app/src/main/java/io/i10nstaller/ui/fragments/InstallConfirmationFragment.java b/app/src/main/java/io/i10nstaller/ui/fragments/InstallConfirmationFragment.java index 0737bb4..b587d78 100644 --- a/app/src/main/java/io/i10nstaller/ui/fragments/InstallConfirmationFragment.java +++ b/app/src/main/java/io/i10nstaller/ui/fragments/InstallConfirmationFragment.java @@ -51,17 +51,22 @@ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { .setIcon(mDialogData.getApkLite().getIcon()) .setTitle(mDialogData.getApkLite().getLabel()) .setView(dialogView) - .setPositiveButton(R.string.install, - (dialog, which) -> mViewModel.initiateInstall( - InstallOptions.getBoolean( - context, InstallOptions.KEY_SET_INSTALLER, false), - InstallOptions.getBoolean( - context, InstallOptions.KEY_GRANT_PERMISSIONS, false), - InstallOptions.getBoolean( - context, InstallOptions.KEY_DELETE_APK, false), - true, - full, - remove)) + .setPositiveButton(R.string.install, (dialog, which) -> { + try { + mViewModel.initiateInstall( + InstallOptions.getBoolean( + context, InstallOptions.KEY_SET_INSTALLER, false), + InstallOptions.getBoolean( + context, InstallOptions.KEY_GRANT_PERMISSIONS, false), + InstallOptions.getBoolean( + context, InstallOptions.KEY_DELETE_APK, false), + true, + full, + remove); + } catch (Exception e) { + mViewModel.reportUnexpectedFailure(e); + } + }) .setNegativeButton(android.R.string.cancel, (dialog, which) -> cleanAndFinish()) .create(); @@ -72,7 +77,9 @@ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { @Override public void onStart() { super.onStart(); - mDialog.getButton(AlertDialog.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true); + var positiveButton = mDialog.getButton(AlertDialog.BUTTON_POSITIVE); + positiveButton.setFilterTouchesWhenObscured(true); + focusPositiveButtonForDpad(); } @Override diff --git a/app/src/main/java/io/i10nstaller/ui/fragments/InstallErrorFragment.java b/app/src/main/java/io/i10nstaller/ui/fragments/InstallErrorFragment.java index c775e9a..3d11f59 100644 --- a/app/src/main/java/io/i10nstaller/ui/fragments/InstallErrorFragment.java +++ b/app/src/main/java/io/i10nstaller/ui/fragments/InstallErrorFragment.java @@ -64,6 +64,9 @@ private void checkShizuku(Context context) { } private String getErrorMessage(Context context, int code) { + if (mAborted.getMessage() != null && !mAborted.getMessage().isBlank()) { + return mAborted.getMessage(); + } switch (code) { case InstallAborted.ABORT_SHIZUKU -> { if (!Shizuku.pingBinder()) { @@ -106,6 +109,8 @@ private LinearLayout createOptionsView(Context context) { private void addOption(Context context, LinearLayout parent, int label, String key) { var checkbox = new CheckBox(context); + checkbox.setFocusable(true); + checkbox.setClickable(true); checkbox.setText(label); checkbox.setChecked(InstallOptions.getBoolean(context, key, false)); checkbox.setOnCheckedChangeListener((button, checked) -> diff --git a/app/src/main/java/io/i10nstaller/ui/fragments/InstallParseFragment.java b/app/src/main/java/io/i10nstaller/ui/fragments/InstallParseFragment.java index 6d0f5a3..1e793ba 100644 --- a/app/src/main/java/io/i10nstaller/ui/fragments/InstallParseFragment.java +++ b/app/src/main/java/io/i10nstaller/ui/fragments/InstallParseFragment.java @@ -42,7 +42,10 @@ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { @Override public void onStart() { super.onStart(); - mDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setEnabled(false); + var cancelButton = mDialog.getButton(DialogInterface.BUTTON_NEGATIVE); + cancelButton.setFocusable(true); + cancelButton.setClickable(true); + cancelButton.setEnabled(false); } } diff --git a/app/src/main/java/io/i10nstaller/ui/fragments/InstallSuccessFragment.java b/app/src/main/java/io/i10nstaller/ui/fragments/InstallSuccessFragment.java index 0a6ecbd..3ed9037 100644 --- a/app/src/main/java/io/i10nstaller/ui/fragments/InstallSuccessFragment.java +++ b/app/src/main/java/io/i10nstaller/ui/fragments/InstallSuccessFragment.java @@ -11,7 +11,6 @@ import androidx.annotation.Nullable; import io.i10nstaller.R; -import io.i10nstaller.model.Hook; import io.i10nstaller.model.InstallSuccess; public class InstallSuccessFragment extends BaseDialogFragment { @@ -39,7 +38,7 @@ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { (dialog, which) -> cleanAndFinish()) .setPositiveButton(R.string.launch, (dialog, which) -> { cleanAndFinish(); - Hook.INSTANCE.startActivity(mDialogData.getStartIntent()); + requireActivity().startActivity(mDialogData.getStartIntent()); }) .create(); return mDialog; diff --git a/app/src/main/java/io/i10nstaller/viewmodel/InstallViewModel.kt b/app/src/main/java/io/i10nstaller/viewmodel/InstallViewModel.kt index 8989e00..780b9c9 100644 --- a/app/src/main/java/io/i10nstaller/viewmodel/InstallViewModel.kt +++ b/app/src/main/java/io/i10nstaller/viewmodel/InstallViewModel.kt @@ -21,7 +21,7 @@ class InstallViewModel(application: Application) : AndroidViewModel(application) fun preprocessIntent(intent: Intent) { if (repository.preCheck(intent)) { - executor.executeOnDiskIO { + executeDiskIo { repository.parseUri() } } @@ -31,6 +31,10 @@ class InstallViewModel(application: Application) : AndroidViewModel(application) repository.cleanupInstall() } + fun reportUnexpectedFailure(error: Throwable) { + repository.reportUnexpectedFailure(error) + } + fun initiateInstall( setInstaller: Boolean, grantPermissions: Boolean, @@ -39,7 +43,7 @@ class InstallViewModel(application: Application) : AndroidViewModel(application) full: Boolean, removeSplit: Boolean ) { - executor.executeOnDiskIO { + executeDiskIo { repository.install( setInstaller, grantPermissions, @@ -52,14 +56,27 @@ class InstallViewModel(application: Application) : AndroidViewModel(application) } fun archivePackage(info: PackageInfo, uninstall: Boolean) { - executor.executeOnDiskIO { + executeDiskIo { repository.archivePackage(info, uninstall) } } fun setPackageEnabled(packageName: String, enabled: Boolean) { - executor.executeOnDiskIO { + executeDiskIo { repository.setPackageEnabled(packageName, enabled) } } + + private fun executeDiskIo(block: () -> Unit) { + executor.executeOnDiskIO { + try { + block() + } catch (e: Exception) { + repository.reportUnexpectedFailure(e) + } catch (t: Throwable) { + if (t is VirtualMachineError || t is ThreadDeath) throw t + repository.reportUnexpectedFailure(t) + } + } + } } diff --git a/app/src/main/res/layout/install_content_view.xml b/app/src/main/res/layout/install_content_view.xml index 53c8a03..0a0a9c5 100644 --- a/app/src/main/res/layout/install_content_view.xml +++ b/app/src/main/res/layout/install_content_view.xml @@ -24,6 +24,8 @@ android:layout_height="wrap_content" android:layout_marginStart="-4dp" android:layout_marginTop="8dp" + android:clickable="true" + android:focusable="true" android:visibility="gone" /> @@ -41,6 +45,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="-4dp" + android:clickable="true" + android:focusable="true" android:text="@string/delete_apk_after_install" android:visibility="gone" /> @@ -50,6 +56,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="-4dp" + android:clickable="true" + android:focusable="true" android:text="@string/silent_mode" android:visibility="gone" /> diff --git a/stub/src/main/java/android/content/pm/PackageInstaller_rename.java b/stub/src/main/java/android/content/pm/PackageInstaller_rename.java index 3c2e73a..ac97cc7 100644 --- a/stub/src/main/java/android/content/pm/PackageInstaller_rename.java +++ b/stub/src/main/java/android/content/pm/PackageInstaller_rename.java @@ -4,7 +4,7 @@ import android.net.Uri; public class PackageInstaller_rename { - public static String EXTRA_LEGACY_STATUS = "android.content.pm.extra.LEGACY_STATUS"; + public static final String EXTRA_LEGACY_STATUS = "android.content.pm.extra.LEGACY_STATUS"; public static class SessionParams { public int mode; diff --git a/stub/src/main/java/android/content/pm/PackageManager_rename.java b/stub/src/main/java/android/content/pm/PackageManager_rename.java index faadf8f..ae23cf0 100644 --- a/stub/src/main/java/android/content/pm/PackageManager_rename.java +++ b/stub/src/main/java/android/content/pm/PackageManager_rename.java @@ -8,248 +8,248 @@ public class PackageManager_rename { - public static int GET_ACTIVITIES = 0x00000001; + public static final int GET_ACTIVITIES = 0x00000001; - public static int GET_RECEIVERS = 0x00000002; + public static final int GET_RECEIVERS = 0x00000002; - public static int GET_SERVICES = 0x00000004; + public static final int GET_SERVICES = 0x00000004; - public static int GET_PROVIDERS = 0x00000008; + public static final int GET_PROVIDERS = 0x00000008; - public static int GET_INSTRUMENTATION = 0x00000010; + public static final int GET_INSTRUMENTATION = 0x00000010; - public static int GET_INTENT_FILTERS = 0x00000020; + public static final int GET_INTENT_FILTERS = 0x00000020; - public static int GET_SIGNATURES = 0x00000040; + public static final int GET_SIGNATURES = 0x00000040; - public static int GET_RESOLVED_FILTER = 0x00000040; + public static final int GET_RESOLVED_FILTER = 0x00000040; - public static int GET_META_DATA = 0x00000080; + public static final int GET_META_DATA = 0x00000080; - public static int GET_GIDS = 0x00000100; + public static final int GET_GIDS = 0x00000100; - public static int GET_DISABLED_COMPONENTS = 0x00000200; + public static final int GET_DISABLED_COMPONENTS = 0x00000200; - public static int MATCH_DISABLED_COMPONENTS = 0x00000200; + public static final int MATCH_DISABLED_COMPONENTS = 0x00000200; - public static int GET_SHARED_LIBRARY_FILES = 0x00000400; + public static final int GET_SHARED_LIBRARY_FILES = 0x00000400; - public static int GET_URI_PERMISSION_PATTERNS = 0x00000800; + public static final int GET_URI_PERMISSION_PATTERNS = 0x00000800; - public static int GET_PERMISSIONS = 0x00001000; + public static final int GET_PERMISSIONS = 0x00001000; - public static int GET_UNINSTALLED_PACKAGES = 0x00002000; + public static final int GET_UNINSTALLED_PACKAGES = 0x00002000; - public static int MATCH_UNINSTALLED_PACKAGES = 0x00002000; + public static final int MATCH_UNINSTALLED_PACKAGES = 0x00002000; - public static int GET_CONFIGURATIONS = 0x00004000; + public static final int GET_CONFIGURATIONS = 0x00004000; - public static int GET_DISABLED_UNTIL_USED_COMPONENTS = 0x00008000; + public static final int GET_DISABLED_UNTIL_USED_COMPONENTS = 0x00008000; - public static int MATCH_DISABLED_UNTIL_USED_COMPONENTS = 0x00008000; + public static final int MATCH_DISABLED_UNTIL_USED_COMPONENTS = 0x00008000; - public static int MATCH_DEFAULT_ONLY = 0x00010000; + public static final int MATCH_DEFAULT_ONLY = 0x00010000; - public static int MATCH_ALL = 0x00020000; + public static final int MATCH_ALL = 0x00020000; - public static int MATCH_DIRECT_BOOT_UNAWARE = 0x00040000; + public static final int MATCH_DIRECT_BOOT_UNAWARE = 0x00040000; - public static int MATCH_DIRECT_BOOT_AWARE = 0x00080000; + public static final int MATCH_DIRECT_BOOT_AWARE = 0x00080000; - public static int MATCH_SYSTEM_ONLY = 0x00100000; + public static final int MATCH_SYSTEM_ONLY = 0x00100000; - public static int MATCH_FACTORY_ONLY = 0x00200000; + public static final int MATCH_FACTORY_ONLY = 0x00200000; - public static int MATCH_ANY_USER = 0x00400000; + public static final int MATCH_ANY_USER = 0x00400000; - public static int MATCH_KNOWN_PACKAGES = MATCH_UNINSTALLED_PACKAGES | MATCH_ANY_USER; + public static final int MATCH_KNOWN_PACKAGES = 0x00402000; - public static int MATCH_INSTANT = 0x00800000; + public static final int MATCH_INSTANT = 0x00800000; - public static int MATCH_VISIBLE_TO_INSTANT_APP_ONLY = 0x01000000; + public static final int MATCH_VISIBLE_TO_INSTANT_APP_ONLY = 0x01000000; - public static int MATCH_EXPLICITLY_VISIBLE_ONLY = 0x02000000; + public static final int MATCH_EXPLICITLY_VISIBLE_ONLY = 0x02000000; - public static int MATCH_STATIC_SHARED_AND_SDK_LIBRARIES = 0x04000000; + public static final int MATCH_STATIC_SHARED_AND_SDK_LIBRARIES = 0x04000000; - public static int GET_SIGNING_CERTIFICATES = 0x08000000; + public static final int GET_SIGNING_CERTIFICATES = 0x08000000; - public static int MATCH_DIRECT_BOOT_AUTO = 0x10000000; + public static final int MATCH_DIRECT_BOOT_AUTO = 0x10000000; - public static int MATCH_DEBUG_TRIAGED_MISSING = MATCH_DIRECT_BOOT_AUTO; + public static final int MATCH_DEBUG_TRIAGED_MISSING = 0x10000000; - public static int MATCH_CLONE_PROFILE = 0x20000000; + public static final int MATCH_CLONE_PROFILE = 0x20000000; - public static int MATCH_HIDDEN_UNTIL_INSTALLED_COMPONENTS = 0x20000000; + public static final int MATCH_HIDDEN_UNTIL_INSTALLED_COMPONENTS = 0x20000000; - public static int MATCH_APEX = 0x40000000; + public static final int MATCH_APEX = 0x40000000; - public static int GET_ATTRIBUTIONS = 0x80000000; + public static final int GET_ATTRIBUTIONS = 0x80000000; - public static long GET_ATTRIBUTIONS_LONG = 0x80000000L; + public static final long GET_ATTRIBUTIONS_LONG = 0x80000000L; - public static long MATCH_ARCHIVED_PACKAGES = 1L << 32; + public static final long MATCH_ARCHIVED_PACKAGES = 1L << 32; - public static long MATCH_QUARANTINED_COMPONENTS = 1L << 33; + public static final long MATCH_QUARANTINED_COMPONENTS = 1L << 33; - public static long MATCH_CLONE_PROFILE_LONG = 1L << 34; + public static final long MATCH_CLONE_PROFILE_LONG = 1L << 34; - public static int INSTALL_REPLACE_EXISTING = 0x00000002; + public static final int INSTALL_REPLACE_EXISTING = 0x00000002; - public static int INSTALL_ALLOW_TEST = 0x00000004; + public static final int INSTALL_ALLOW_TEST = 0x00000004; - public static int INSTALL_INTERNAL = 0x00000010; + public static final int INSTALL_INTERNAL = 0x00000010; - public static int INSTALL_FROM_ADB = 0x00000020; + public static final int INSTALL_FROM_ADB = 0x00000020; - public static int INSTALL_ALL_USERS = 0x00000040; + public static final int INSTALL_ALL_USERS = 0x00000040; - public static int INSTALL_REQUEST_DOWNGRADE = 0x00000080; + public static final int INSTALL_REQUEST_DOWNGRADE = 0x00000080; - public static int INSTALL_GRANT_ALL_REQUESTED_PERMISSIONS = 0x00000100; + public static final int INSTALL_GRANT_ALL_REQUESTED_PERMISSIONS = 0x00000100; - public static int INSTALL_FORCE_VOLUME_UUID = 0x00000200; + public static final int INSTALL_FORCE_VOLUME_UUID = 0x00000200; - public static int INSTALL_FORCE_PERMISSION_PROMPT = 0x00000400; + public static final int INSTALL_FORCE_PERMISSION_PROMPT = 0x00000400; - public static int INSTALL_INSTANT_APP = 0x00000800; + public static final int INSTALL_INSTANT_APP = 0x00000800; - public static int INSTALL_DONT_KILL_APP = 0x00001000; + public static final int INSTALL_DONT_KILL_APP = 0x00001000; - public static int INSTALL_FULL_APP = 0x00004000; + public static final int INSTALL_FULL_APP = 0x00004000; - public static int INSTALL_ALLOCATE_AGGRESSIVE = 0x00008000; + public static final int INSTALL_ALLOCATE_AGGRESSIVE = 0x00008000; - public static int INSTALL_VIRTUAL_PRELOAD = 0x00010000; + public static final int INSTALL_VIRTUAL_PRELOAD = 0x00010000; - public static int INSTALL_APEX = 0x00020000; + public static final int INSTALL_APEX = 0x00020000; - public static int INSTALL_ENABLE_ROLLBACK = 0x00040000; + public static final int INSTALL_ENABLE_ROLLBACK = 0x00040000; - public static int INSTALL_DISABLE_VERIFICATION = 0x00080000; + public static final int INSTALL_DISABLE_VERIFICATION = 0x00080000; - public static int INSTALL_ALLOW_DOWNGRADE = 0x00100000; + public static final int INSTALL_ALLOW_DOWNGRADE = 0x00100000; - public static int INSTALL_STAGED = 0x00200000; + public static final int INSTALL_STAGED = 0x00200000; - public static int INSTALL_ALL_WHITELIST_RESTRICTED_PERMISSIONS = 0x00400000; + public static final int INSTALL_ALL_WHITELIST_RESTRICTED_PERMISSIONS = 0x00400000; - public static int INSTALL_DISABLE_ALLOWED_APEX_UPDATE_CHECK = 0x00800000; + public static final int INSTALL_DISABLE_ALLOWED_APEX_UPDATE_CHECK = 0x00800000; - public static int INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK = 0x01000000; + public static final int INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK = 0x01000000; - public static int INSTALL_REQUEST_UPDATE_OWNERSHIP = 1 << 25; + public static final int INSTALL_REQUEST_UPDATE_OWNERSHIP = 1 << 25; - public static int INSTALL_FROM_MANAGED_USER_OR_PROFILE = 1 << 26; + public static final int INSTALL_FROM_MANAGED_USER_OR_PROFILE = 1 << 26; - public static int INSTALL_ARCHIVED = 1 << 27; + public static final int INSTALL_ARCHIVED = 1 << 27; - public static int INSTALL_IGNORE_DEXOPT_PROFILE = 1 << 28; + public static final int INSTALL_IGNORE_DEXOPT_PROFILE = 1 << 28; - public static int INSTALL_UNARCHIVE_DRAFT = 1 << 29; + public static final int INSTALL_UNARCHIVE_DRAFT = 1 << 29; - public static int INSTALL_UNARCHIVE = 1 << 30; + public static final int INSTALL_UNARCHIVE = 1 << 30; - public static int INSTALL_DEVELOPMENT_FORCE_NON_STAGED_APEX_UPDATE = 1; + public static final int INSTALL_DEVELOPMENT_FORCE_NON_STAGED_APEX_UPDATE = 1; - public static int INSTALL_SUCCEEDED = 1; + public static final int INSTALL_SUCCEEDED = 1; - public static int INSTALL_FAILED_INTERNAL_ERROR = -110; + public static final int INSTALL_FAILED_INTERNAL_ERROR = -110; - public static int DELETE_KEEP_DATA = 0x00000001; + public static final int DELETE_KEEP_DATA = 0x00000001; - public static int DELETE_ALL_USERS = 0x00000002; + public static final int DELETE_ALL_USERS = 0x00000002; - public static int DELETE_SYSTEM_APP = 0x00000004; + public static final int DELETE_SYSTEM_APP = 0x00000004; - public static int DELETE_DONT_KILL_APP = 0x00000008; + public static final int DELETE_DONT_KILL_APP = 0x00000008; - public static int DELETE_ARCHIVE = 0x00000010; + public static final int DELETE_ARCHIVE = 0x00000010; - public static int DELETE_CHATTY = 0x80000000; + public static final int DELETE_CHATTY = 0x80000000; - public static int DELETE_SUCCEEDED = 1; + public static final int DELETE_SUCCEEDED = 1; - public static int DELETE_FAILED_INTERNAL_ERROR = -1; + public static final int DELETE_FAILED_INTERNAL_ERROR = -1; - public static int DELETE_FAILED_DEVICE_POLICY_MANAGER = -2; + public static final int DELETE_FAILED_DEVICE_POLICY_MANAGER = -2; - public static int DELETE_FAILED_USER_RESTRICTED = -3; + public static final int DELETE_FAILED_USER_RESTRICTED = -3; - public static int DELETE_FAILED_OWNER_BLOCKED = -4; + public static final int DELETE_FAILED_OWNER_BLOCKED = -4; - public static int DELETE_FAILED_ABORTED = -5; + public static final int DELETE_FAILED_ABORTED = -5; public native int installExistingPackage(String packageName, int installReason);