diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 27268bc3..1c38cc89 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,9 +23,10 @@ android:label="@string/app_name" android:requestLegacyExternalStorage="true" android:roundIcon="@mipmap/ic_launcher_round" - android:supportsRtl="true" - tools:ignore="AllowBackup"> - + android:supportsRtl="true"> + + + { - @Suppress("DEPRECATION") // No. - val confirmationIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT)!! - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - - startActivity(confirmationIntent) - } - - PackageInstaller.STATUS_SUCCESS -> showToast(R.string.installer_success) - PackageInstaller.STATUS_FAILURE_ABORTED -> showToast(R.string.installer_aborted) - - else -> { - Log.i(BuildConfig.TAG, "Install failed with error code $statusCode") - - if (errorMessages[statusCode] != null) { - showToast(errorMessages[statusCode]!!) - } else { - showToast(R.string.install_error_code, statusCode) - } - } - } - - stopSelf() - return START_NOT_STICKY - } - - override fun onBind(intent: Intent): IBinder? = null -} diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/InstallStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/InstallStep.kt index 026b4c9c..190b58ad 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/InstallStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/InstallStep.kt @@ -1,12 +1,13 @@ package com.aliucord.manager.installer.steps.install -import android.app.Application import com.aliucord.manager.R import com.aliucord.manager.installer.steps.StepGroup import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.base.Step +import com.aliucord.manager.installer.steps.base.StepState import com.aliucord.manager.installer.steps.patch.CopyDependenciesStep -import com.aliucord.manager.installer.util.installApks +import com.aliucord.manager.installers.InstallerResult +import com.aliucord.manager.manager.InstallerManager import com.aliucord.manager.manager.PreferencesManager import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -15,7 +16,7 @@ import org.koin.core.component.inject * Install the final APK with the system's PackageManager. */ class InstallStep : Step(), KoinComponent { - private val application: Application by inject() + private val installers: InstallerManager by inject() private val prefs: PreferencesManager by inject() override val group = StepGroup.Install @@ -24,9 +25,19 @@ class InstallStep : Step(), KoinComponent { override suspend fun execute(container: StepRunner) { val apk = container.getStep().patchedApk - application.installApks( + val result = installers.getActiveInstaller().waitInstall( + apks = listOf(apk), silent = !prefs.devMode, - apks = arrayOf(apk), ) + + when (result) { + is InstallerResult.Error -> throw Error("Failed to install APKs: ${result.debugReason}") + is InstallerResult.Cancelled -> { + // The install screen is automatically closed immediately once cleanup finishes + state = StepState.Skipped + } + + else -> {} + } } } diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/util/PackageInstaller.kt b/app/src/main/kotlin/com/aliucord/manager/installer/util/PackageInstaller.kt index d679a579..52536299 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/util/PackageInstaller.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/util/PackageInstaller.kt @@ -1,52 +1,10 @@ package com.aliucord.manager.installer.util -import android.annotation.SuppressLint -import android.app.Application -import android.app.PendingIntent import android.content.Context import android.content.Intent -import android.content.pm.PackageInstaller.SessionParams -import android.content.pm.PackageManager import android.net.Uri -import android.os.Build -import com.aliucord.manager.installer.service.InstallService -import java.io.File - -fun Application.installApks(silent: Boolean = false, vararg apks: File) { - val packageInstaller = packageManager.packageInstaller - val params = SessionParams(SessionParams.MODE_FULL_INSTALL).apply { - if (Build.VERSION.SDK_INT >= 31) { - setInstallScenario(PackageManager.INSTALL_SCENARIO_FAST) - - if (silent) { - setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED) - } - } - } - - val sessionId = packageInstaller.createSession(params) - val session = packageInstaller.openSession(sessionId) - - apks.forEach { apk -> - session.openWrite(apk.name, 0, apk.length()).use { - it.write(apk.readBytes()) - session.fsync(it) - } - } - - val callbackIntent = Intent(this, InstallService::class.java) - - @SuppressLint("UnspecifiedImmutableFlag") - val contentIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - PendingIntent.getService(this, 0, callbackIntent, PendingIntent.FLAG_MUTABLE) - } else { - PendingIntent.getService(this, 0, callbackIntent, 0) - } - - session.commit(contentIntent.intentSender) - session.close() -} +// TODO: move this to PMInstaller fun Context.uninstallApk(packageName: String) { val packageURI = Uri.parse("package:$packageName") val uninstallIntent = Intent(Intent.ACTION_DELETE, packageURI).apply { diff --git a/app/src/main/kotlin/com/aliucord/manager/installers/Installer.kt b/app/src/main/kotlin/com/aliucord/manager/installers/Installer.kt new file mode 100644 index 00000000..644318ef --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installers/Installer.kt @@ -0,0 +1,22 @@ +package com.aliucord.manager.installers + +import java.io.File + +/** + * A generic installer interface that manages installing APKs + */ +interface Installer { + /** + * Starts an installation and forgets about it. A toast will be shown when the installation was completed. + * @param apks All APKs (including any splits) to merge into a single install. + * @param silent If this is an update, then the update will occur without user interaction. + */ + fun install(apks: List, silent: Boolean = true) + + /** + * Starts an installation and waits for it to finish with a result. A toast will be shown when the installation was completed. + * @param apks All APKs (including any splits) to merge into a single install. + * @param silent If this is an update, then the update will occur without user interaction. + */ + suspend fun waitInstall(apks: List, silent: Boolean = true): InstallerResult +} diff --git a/app/src/main/kotlin/com/aliucord/manager/installers/InstallerResult.kt b/app/src/main/kotlin/com/aliucord/manager/installers/InstallerResult.kt new file mode 100644 index 00000000..174fe07b --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installers/InstallerResult.kt @@ -0,0 +1,41 @@ +package com.aliucord.manager.installers + +import android.os.Parcelable +import androidx.annotation.StringRes +import kotlinx.parcelize.Parcelize + +/** + * The state of an APK installation after it has completed and cleaned up. + */ +sealed interface InstallerResult : Parcelable { + /** + * The installation was successfully completed. + */ + @Parcelize + data object Success : InstallerResult + + /** + * This installation was interrupted and the install session has been canceled. + * @param systemTriggered Whether the cancellation happened from the system (ie. clicked cancel on the install prompt) + * Otherwise, this was caused by a coroutine cancellation. + */ + @Parcelize + data class Cancelled(val systemTriggered: Boolean) : InstallerResult + + /** + * This installation encountered an error and has been aborted. + * All implementors should implement [Parcelable]. + */ + abstract class Error : InstallerResult { + /** + * Loggable error that should not be shown to the user. + */ + abstract val debugReason: String + + /** + * Simplified + translatable user facing errors. + */ + @get:StringRes + abstract val localizedReason: Int + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/installers/pm/PMInstaller.kt b/app/src/main/kotlin/com/aliucord/manager/installers/pm/PMInstaller.kt new file mode 100644 index 00000000..9892ee97 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installers/pm/PMInstaller.kt @@ -0,0 +1,126 @@ +package com.aliucord.manager.installers.pm + +import android.annotation.SuppressLint +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 +import android.os.Process +import android.util.Log +import com.aliucord.manager.BuildConfig +import com.aliucord.manager.installers.Installer +import com.aliucord.manager.installers.InstallerResult +import kotlinx.coroutines.suspendCancellableCoroutine +import java.io.File + +/** + * APK installer using the [PackageInstaller] from the system's [PackageManager] service. + */ +class PMInstaller( + val context: Context, +) : Installer { + init { + val pkgInstaller = context.packageManager.packageInstaller + + // Destroy all open sessions that may have not been previously cleaned up + for (session in pkgInstaller.mySessions) { + Log.d(BuildConfig.TAG, "Deleting PackageInstaller session ${session.sessionId}") + pkgInstaller.abandonSession(session.sessionId) + } + } + + override fun install(apks: List, silent: Boolean) { + startInstall(createInstallSession(silent), apks, false) + } + + override suspend fun waitInstall(apks: List, silent: Boolean): InstallerResult { + val sessionId = createInstallSession(silent) + + return suspendCancellableCoroutine { continuation -> + // This will receive parsed data forwarded by PMIntentReceiver + val relayReceiver = PMResultReceiver(sessionId, continuation) + + // Unregister PMResultReceiver when this coroutine finishes + // additionally, cancel the install session entirely + continuation.invokeOnCancellation { + context.unregisterReceiver(relayReceiver) + context.packageManager.packageInstaller.abandonSession(sessionId) + } + + @SuppressLint("UnspecifiedRegisterReceiverFlag") + if (Build.VERSION.SDK_INT >= 33) { + context.registerReceiver(relayReceiver, relayReceiver.filter, Context.RECEIVER_NOT_EXPORTED) + } else { + context.registerReceiver(relayReceiver, relayReceiver.filter) + } + + startInstall(sessionId, apks, relay = true) + } + } + + /** + * Starts a [PackageInstaller] session with the necessary params. + * @param silent If this is an update, then the update will occur without user interaction. + * @return The open install session id. + */ + private fun createInstallSession(silent: Boolean): Int { + val params = SessionParams(SessionParams.MODE_FULL_INSTALL).apply { + setInstallLocation(PackageInfo.INSTALL_LOCATION_AUTO) + + if (Build.VERSION.SDK_INT >= 24) { + setOriginatingUid(Process.myUid()) + } + + if (Build.VERSION.SDK_INT >= 26) { + setInstallReason(PackageManager.INSTALL_REASON_USER) + } + + if (Build.VERSION.SDK_INT >= 31) { + setInstallScenario(PackageManager.INSTALL_SCENARIO_FAST) + + if (silent) { + setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED) + } + } + + if (Build.VERSION.SDK_INT >= 34) { + setPackageSource(PackageInstaller.PACKAGE_SOURCE_OTHER) + } + } + + return context.packageManager.packageInstaller.createSession(params) + } + + /** + * Start a [PackageInstaller] session for installation. + * @param apks The apks to install + * @param relay Whether to use the [PMResultReceiver] flow. + */ + private fun startInstall(sessionId: Int, apks: List, relay: Boolean) { + val callbackIntent = Intent(context, PMIntentReceiver::class.java) + .putExtra(PMIntentReceiver.EXTRA_SESSION_ID, sessionId) + .putExtra(PMIntentReceiver.EXTRA_RELAY_ENABLED, relay) + + val pendingIntent = PendingIntent.getBroadcast( + /* context = */ context, + /* requestCode = */ 0, + /* intent = */ callbackIntent, + /* flags = */ PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, + ) + + context.packageManager.packageInstaller.openSession(sessionId).use { session -> + val bufferSize = 1 * 1024 * 1024 // 1MiB + + for (apk in apks) { + session.openWrite(apk.name, 0, apk.length()).use { outStream -> + apk.inputStream().use { it.copyTo(outStream, bufferSize) } + session.fsync(outStream) + } + } + + session.commit(pendingIntent.intentSender) + } + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/installers/pm/PMInstallerError.kt b/app/src/main/kotlin/com/aliucord/manager/installers/pm/PMInstallerError.kt new file mode 100644 index 00000000..b7676d12 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installers/pm/PMInstallerError.kt @@ -0,0 +1,39 @@ +package com.aliucord.manager.installers.pm + +import android.content.pm.PackageInstaller +import android.os.Parcelable +import com.aliucord.manager.R +import com.aliucord.manager.installers.InstallerResult +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize + +/** + * Translates the errors returned by PackageInstaller's [PackageInstaller.EXTRA_STATUS] + * that is captured by a receiver into something human readable. + */ +@Parcelize +data class PMInstallerError(val status: Int) : InstallerResult.Error(), Parcelable { + @IgnoredOnParcel + override val debugReason = when (status) { + PackageInstaller.STATUS_FAILURE -> "Unknown failure" + PackageInstaller.STATUS_FAILURE_BLOCKED -> "Blocked" + PackageInstaller.STATUS_FAILURE_INVALID -> "Invalid package" + PackageInstaller.STATUS_FAILURE_CONFLICT -> "Package conflict" + PackageInstaller.STATUS_FAILURE_STORAGE -> "Storage error" + PackageInstaller.STATUS_FAILURE_INCOMPATIBLE -> "Device incompatibility" + /* PackageInstaller.STATUS_FAILURE_TIMEOUT */ 8 -> "Operation timeout" + else -> "Unknown code ($status)" + } + + @IgnoredOnParcel + override val localizedReason = when (status) { + PackageInstaller.STATUS_FAILURE -> R.string.install_error_unknown + PackageInstaller.STATUS_FAILURE_BLOCKED -> R.string.install_error_blocked + PackageInstaller.STATUS_FAILURE_INVALID -> R.string.install_error_invalid + PackageInstaller.STATUS_FAILURE_CONFLICT -> R.string.install_error_conflict + PackageInstaller.STATUS_FAILURE_STORAGE -> R.string.install_error_storage + PackageInstaller.STATUS_FAILURE_INCOMPATIBLE -> R.string.install_error_incompatible + /* PackageInstaller.STATUS_FAILURE_TIMEOUT */ 8 -> R.string.install_error_timeout + else -> R.string.install_error_unknown + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/installers/pm/PMIntentReceiver.kt b/app/src/main/kotlin/com/aliucord/manager/installers/pm/PMIntentReceiver.kt new file mode 100644 index 00000000..c464e2fc --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installers/pm/PMIntentReceiver.kt @@ -0,0 +1,73 @@ +package com.aliucord.manager.installers.pm + +import android.content.* +import android.content.pm.PackageInstaller +import android.util.Log +import com.aliucord.manager.BuildConfig +import com.aliucord.manager.R +import com.aliucord.manager.installers.InstallerResult +import com.aliucord.manager.util.showToast + +class PMIntentReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val realSessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1) + val expectedSessionId = intent.getIntExtra(EXTRA_SESSION_ID, -2) + + if (realSessionId != expectedSessionId) return + + val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999) + val installerResult = when (status) { + -999 -> { + // Invalid intent + null + } + + PackageInstaller.STATUS_PENDING_USER_ACTION -> { + @Suppress("DEPRECATION") + val confirmationIntent = intent + .getParcelableExtra(Intent.EXTRA_INTENT)!! + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + context.startActivity(confirmationIntent) + + null // no result yet + } + + PackageInstaller.STATUS_SUCCESS -> { + context.showToast(R.string.installer_success) + InstallerResult.Success + } + + PackageInstaller.STATUS_FAILURE_ABORTED -> { + context.showToast(R.string.installer_aborted) + InstallerResult.Cancelled(systemTriggered = true) + } + + else -> { + Log.w(BuildConfig.TAG, "Install failed with error code $status") + + if (status <= PackageInstaller.STATUS_SUCCESS) + null // Unknown status code (not an error) + else { + PMInstallerError(status).also { + context.showToast(it.localizedReason) + } + } + } + } + + // Forward result to PMResultReceiver if relaying is enabled and have a real result + if (installerResult != null && intent.getBooleanExtra(EXTRA_RELAY_ENABLED, false)) { + val relayIntent = Intent(PMResultReceiver.ACTION_RECEIVE_RESULT) + .putExtra(PMResultReceiver.EXTRA_RESULT, installerResult) + .putExtra(PMResultReceiver.EXTRA_SESSION_ID, realSessionId) + + context.sendBroadcast(relayIntent) + } + } + + companion object { + const val EXTRA_RELAY_ENABLED = "relayEnabled" + const val EXTRA_SESSION_ID = "expectedSessionId" + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/installers/pm/PMResultReceiver.kt b/app/src/main/kotlin/com/aliucord/manager/installers/pm/PMResultReceiver.kt new file mode 100644 index 00000000..f9f17ec6 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installers/pm/PMResultReceiver.kt @@ -0,0 +1,33 @@ +package com.aliucord.manager.installers.pm + +import android.content.* +import com.aliucord.manager.installers.InstallerResult +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume + +class PMResultReceiver( + private val sessionId: Int, + private val continuation: Continuation, +) : BroadcastReceiver() { + /** + * The intent filter this receiver should be registered with to work properly. + */ + val filter = IntentFilter(ACTION_RECEIVE_RESULT) + + @Suppress("DEPRECATION") + override fun onReceive(context: Context, intent: Intent) { + if (intent.action != ACTION_RECEIVE_RESULT) return + if (intent.getIntExtra(EXTRA_SESSION_ID, -1) != sessionId) return + + val result = intent.getParcelableExtra(EXTRA_RESULT) + ?: return + + continuation.resume(result) + } + + companion object { + const val ACTION_RECEIVE_RESULT = "com.aliucord.manager.RELAY_PM_RESULT" + const val EXTRA_RESULT = "installerResult" + const val EXTRA_SESSION_ID = "sessionId" + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/manager/InstallerManager.kt b/app/src/main/kotlin/com/aliucord/manager/manager/InstallerManager.kt new file mode 100644 index 00000000..c6a4d905 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/manager/InstallerManager.kt @@ -0,0 +1,29 @@ +package com.aliucord.manager.manager + +import com.aliucord.manager.installers.Installer +import com.aliucord.manager.installers.pm.PMInstaller +import org.koin.core.annotation.KoinInternalApi +import org.koin.core.component.KoinComponent +import kotlin.reflect.KClass + +/** + * Handle providing the correct install manager based on preferences. + */ +class InstallerManager( + private val prefs: PreferencesManager, +) : KoinComponent { + fun getActiveInstaller(): Installer = + getInstaller(prefs.installer) + + @OptIn(KoinInternalApi::class) + fun getInstaller(type: InstallerSetting): Installer = + getKoin().scopeRegistry.rootScope.get(clazz = type.installerClass) +} + +enum class InstallerSetting( + // @StringRes + // private val localizedName: Int, + val installerClass: KClass, +) { + PM(PMInstaller::class), +} diff --git a/app/src/main/kotlin/com/aliucord/manager/manager/PreferencesManager.kt b/app/src/main/kotlin/com/aliucord/manager/manager/PreferencesManager.kt index a15db049..f49d71fb 100644 --- a/app/src/main/kotlin/com/aliucord/manager/manager/PreferencesManager.kt +++ b/app/src/main/kotlin/com/aliucord/manager/manager/PreferencesManager.kt @@ -9,6 +9,7 @@ class PreferencesManager(preferences: SharedPreferences) : BasePreferenceManager var dynamicColor by booleanPreference("dynamic_color", true) var replaceIcon by booleanPreference("replace_icon", true) var devMode by booleanPreference("dev_mode", false) + var installer by enumPreference("installer", InstallerSetting.PM) var debuggable by booleanPreference("debuggable", false) var appName by stringPreference("app_name", "Aliucord") var packageName by stringPreference("package_name", "com.aliucord") diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallModel.kt index 80252f80..37d30fd9 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallModel.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallModel.kt @@ -12,6 +12,8 @@ import com.aliucord.manager.R import com.aliucord.manager.installer.steps.KotlinInstallRunner import com.aliucord.manager.installer.steps.StepGroup import com.aliucord.manager.installer.steps.base.Step +import com.aliucord.manager.installer.steps.base.StepState +import com.aliucord.manager.installer.steps.install.InstallStep import com.aliucord.manager.manager.PathManager import com.aliucord.manager.ui.util.toUnsafeImmutable import com.aliucord.manager.util.* @@ -79,13 +81,20 @@ class InstallModel( // Execute all the steps and catch any errors when (val error = runner.executeAll()) { - // Successfully installed null -> { - mutableState.value = InstallScreenState.Success - - // Wait 20s before returning to Home - delay(5000) - mutableState.value = InstallScreenState.CloseScreen + // If install step is marked skipped then the installation was manually aborted + // and if so, immediately close install screen + if (runner.getStep().state == StepState.Skipped) { + mutableState.value = InstallScreenState.CloseScreen + } + // At this point, the installation has successfully completed + else { + mutableState.value = InstallScreenState.Success + + // Wait 20s before returning to Home + delay(5000) + mutableState.value = InstallScreenState.CloseScreen + } } else -> { diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/widgets/updater/UpdaterViewModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/widgets/updater/UpdaterViewModel.kt index 55338188..e32a7ad3 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/widgets/updater/UpdaterViewModel.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/widgets/updater/UpdaterViewModel.kt @@ -6,8 +6,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.aliucord.manager.BuildConfig import com.aliucord.manager.domain.repository.GithubRepository -import com.aliucord.manager.installer.util.installApks -import com.aliucord.manager.manager.DownloadManager +import com.aliucord.manager.manager.* import com.aliucord.manager.network.utils.SemVer import com.aliucord.manager.network.utils.getOrNull import kotlinx.coroutines.Dispatchers @@ -16,6 +15,7 @@ import kotlinx.coroutines.launch class UpdaterViewModel( private val github: GithubRepository, private val downloadManager: DownloadManager, + private val installers: InstallerManager, private val application: Application, ) : ViewModel() { var showDialog by mutableStateOf(false) @@ -42,7 +42,10 @@ class UpdaterViewModel( it.delete() downloadManager.download(url, it) - application.installApks(silent = true, it) + installers.getInstaller(InstallerSetting.PM).install( + apks = listOf(it), + silent = true, + ) it.delete() } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6e7a5cdc..0de92053 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -89,12 +89,11 @@ Saved to %s Failed to save %s Successfully installed Aliucord - Aborted Aliucord installation + Cancelled Aliucord installation Failed to verify download Please uninstall your current version of Aliucord in order to continue! Failed to install (Unknown reason) - Failed to install (error code %d) Installation was blocked One or more APKs were invalid or corrupt Conflicts with an existing app, usually due to mismatched signatures