feat: rewrite PM installer (#68)
- Separate installer into a generic interface (for Shizuku support later) - Heavily improve installation UI flow - Better handling of waiting for install completions - Handle install prompt cancellations
This commit is contained in:
parent
8ba070b23b
commit
8f1058f6bc
16 changed files with 414 additions and 113 deletions
|
|
@ -23,9 +23,10 @@
|
|||
android:label="@string/app_name"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
tools:ignore="AllowBackup">
|
||||
<service android:name=".installer.service.InstallService" />
|
||||
android:supportsRtl="true">
|
||||
|
||||
<receiver android:name=".installers.pm.PMIntentReceiver" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import android.app.Application
|
|||
import com.aliucord.manager.di.*
|
||||
import com.aliucord.manager.domain.repository.AliucordMavenRepository
|
||||
import com.aliucord.manager.domain.repository.GithubRepository
|
||||
import com.aliucord.manager.installers.pm.PMInstaller
|
||||
import com.aliucord.manager.manager.InstallerManager
|
||||
import com.aliucord.manager.network.service.*
|
||||
import com.aliucord.manager.ui.screens.about.AboutModel
|
||||
import com.aliucord.manager.ui.screens.home.HomeModel
|
||||
|
|
@ -60,6 +62,12 @@ class ManagerApplication : Application() {
|
|||
single { providePreferences() }
|
||||
single { provideDownloadManager() }
|
||||
single { providePathManager() }
|
||||
singleOf(::InstallerManager)
|
||||
})
|
||||
|
||||
// Installers
|
||||
modules(module {
|
||||
singleOf(::PMInstaller)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
package com.aliucord.manager.installer.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import com.aliucord.manager.BuildConfig
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.util.showToast
|
||||
|
||||
class InstallService : Service() {
|
||||
private val errorMessages = mapOf(
|
||||
PackageInstaller.STATUS_FAILURE to R.string.install_error_unknown,
|
||||
PackageInstaller.STATUS_FAILURE_BLOCKED to R.string.install_error_blocked,
|
||||
PackageInstaller.STATUS_FAILURE_INVALID to R.string.install_error_invalid,
|
||||
PackageInstaller.STATUS_FAILURE_CONFLICT to R.string.install_error_conflict,
|
||||
PackageInstaller.STATUS_FAILURE_STORAGE to R.string.install_error_storage,
|
||||
PackageInstaller.STATUS_FAILURE_INCOMPATIBLE to R.string.install_error_incompatible,
|
||||
PackageInstaller.STATUS_FAILURE_TIMEOUT to R.string.install_error_timeout
|
||||
)
|
||||
|
||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||
when (val statusCode = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)) {
|
||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||
@Suppress("DEPRECATION") // No.
|
||||
val confirmationIntent = intent.getParcelableExtra<Intent>(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
|
||||
}
|
||||
|
|
@ -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<CopyDependenciesStep>().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 -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<File>, 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<File>, silent: Boolean = true): InstallerResult
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<File>, silent: Boolean) {
|
||||
startInstall(createInstallSession(silent), apks, false)
|
||||
}
|
||||
|
||||
override suspend fun waitInstall(apks: List<File>, 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<File>, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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>(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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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<InstallerResult>,
|
||||
) : 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<InstallerResult>(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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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<out Installer>,
|
||||
) {
|
||||
PM(PMInstaller::class),
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// If install step is marked skipped then the installation was manually aborted
|
||||
// and if so, immediately close install screen
|
||||
if (runner.getStep<InstallStep>().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
|
||||
// Wait 20s before returning to Home
|
||||
delay(5000)
|
||||
mutableState.value = InstallScreenState.CloseScreen
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,12 +89,11 @@
|
|||
<string name="installer_file_save_success">Saved to %s</string>
|
||||
<string name="installer_file_save_failed">Failed to save %s</string>
|
||||
<string name="installer_success">Successfully installed Aliucord</string>
|
||||
<string name="installer_aborted">Aborted Aliucord installation</string>
|
||||
<string name="installer_aborted">Cancelled Aliucord installation</string>
|
||||
<string name="installer_dl_verify_fail">Failed to verify download</string>
|
||||
<string name="installer_uninstall_new">Please uninstall your current version of Aliucord in order to continue!</string>
|
||||
|
||||
<string name="install_error_unknown">Failed to install (Unknown reason)</string>
|
||||
<string name="install_error_code">Failed to install (error code %d)</string>
|
||||
<string name="install_error_blocked">Installation was blocked</string>
|
||||
<string name="install_error_invalid">One or more APKs were invalid or corrupt</string>
|
||||
<string name="install_error_conflict">Conflicts with an existing app, usually due to mismatched signatures</string>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue