mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2026-01-11 13:46:17 +00:00
fix: install dialog getting stuck
This commit is contained in:
@@ -108,6 +108,10 @@ dependencies {
|
|||||||
|
|
||||||
// Compose Icons
|
// Compose Icons
|
||||||
implementation(libs.compose.icons.fontawesome)
|
implementation(libs.compose.icons.fontawesome)
|
||||||
|
|
||||||
|
// Ackpine
|
||||||
|
implementation(libs.ackpine.core)
|
||||||
|
implementation(libs.ackpine.ktx)
|
||||||
}
|
}
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
|
|||||||
@@ -51,9 +51,6 @@
|
|||||||
|
|
||||||
<activity android:name=".plugin.downloader.webview.WebViewActivity" android:exported="false" android:theme="@style/Theme.WebViewActivity" />
|
<activity android:name=".plugin.downloader.webview.WebViewActivity" android:exported="false" android:theme="@style/Theme.WebViewActivity" />
|
||||||
|
|
||||||
<service android:name=".service.InstallService" />
|
|
||||||
<service android:name=".service.UninstallService" />
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||||
android:foregroundServiceType="specialUse"
|
android:foregroundServiceType="specialUse"
|
||||||
@@ -75,5 +72,15 @@
|
|||||||
android:value="androidx.startup"
|
android:value="androidx.startup"
|
||||||
tools:node="remove" />
|
tools:node="remove" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.startup.InitializationProvider"
|
||||||
|
android:authorities="${applicationId}.androidx-startup"
|
||||||
|
android:exported="false"
|
||||||
|
tools:node="merge">
|
||||||
|
<meta-data
|
||||||
|
android:name="ru.solrudev.ackpine.AckpineInitializer"
|
||||||
|
tools:node="remove" />
|
||||||
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -48,7 +48,8 @@ class ManagerApplication : Application() {
|
|||||||
workerModule,
|
workerModule,
|
||||||
viewModelModule,
|
viewModelModule,
|
||||||
databaseModule,
|
databaseModule,
|
||||||
rootModule
|
rootModule,
|
||||||
|
ackpineModule
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
19
app/src/main/java/app/revanced/manager/di/AckpineModule.kt
Normal file
19
app/src/main/java/app/revanced/manager/di/AckpineModule.kt
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package app.revanced.manager.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
import org.koin.dsl.module
|
||||||
|
import ru.solrudev.ackpine.installer.PackageInstaller
|
||||||
|
import ru.solrudev.ackpine.uninstaller.PackageUninstaller
|
||||||
|
|
||||||
|
val ackpineModule = module {
|
||||||
|
fun provideInstaller(context: Context) = PackageInstaller.getInstance(context)
|
||||||
|
fun provideUninstaller(context: Context) = PackageUninstaller.getInstance(context)
|
||||||
|
|
||||||
|
single {
|
||||||
|
provideInstaller(androidContext())
|
||||||
|
}
|
||||||
|
single {
|
||||||
|
provideUninstaller(androidContext())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package app.revanced.manager.service
|
|
||||||
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.IBinder
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
class InstallService : Service() {
|
|
||||||
|
|
||||||
override fun onStartCommand(
|
|
||||||
intent: Intent, flags: Int, startId: Int
|
|
||||||
): Int {
|
|
||||||
val extraStatus = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)
|
|
||||||
val extraStatusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
|
|
||||||
val extraPackageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
|
|
||||||
when (extraStatus) {
|
|
||||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
|
||||||
startActivity(if (Build.VERSION.SDK_INT >= 33) {
|
|
||||||
intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
|
|
||||||
} else {
|
|
||||||
intent.getParcelableExtra(Intent.EXTRA_INTENT)
|
|
||||||
}.apply {
|
|
||||||
this?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
sendBroadcast(Intent().apply {
|
|
||||||
action = APP_INSTALL_ACTION
|
|
||||||
`package` = packageName
|
|
||||||
putExtra(EXTRA_INSTALL_STATUS, extraStatus)
|
|
||||||
putExtra(EXTRA_INSTALL_STATUS_MESSAGE, extraStatusMessage)
|
|
||||||
putExtra(EXTRA_PACKAGE_NAME, extraPackageName)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stopSelf()
|
|
||||||
return START_NOT_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? = null
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val APP_INSTALL_ACTION = "APP_INSTALL_ACTION"
|
|
||||||
|
|
||||||
const val EXTRA_INSTALL_STATUS = "EXTRA_INSTALL_STATUS"
|
|
||||||
const val EXTRA_INSTALL_STATUS_MESSAGE = "EXTRA_INSTALL_STATUS_MESSAGE"
|
|
||||||
const val EXTRA_PACKAGE_NAME = "EXTRA_PACKAGE_NAME"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.revanced.manager.ui.component
|
package app.revanced.manager.ui.component
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.pm.PackageInstaller
|
import android.content.pm.PackageInstaller
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
@@ -79,7 +80,7 @@ private fun installerStatusDialogButton(
|
|||||||
enum class DialogKind(
|
enum class DialogKind(
|
||||||
val flag: Int,
|
val flag: Int,
|
||||||
val title: Int,
|
val title: Int,
|
||||||
@StringRes val contentStringResId: Int,
|
@param:StringRes val contentStringResId: Int,
|
||||||
val icon: ImageVector = Icons.Outlined.ErrorOutline,
|
val icon: ImageVector = Icons.Outlined.ErrorOutline,
|
||||||
val confirmButton: InstallerStatusDialogButton = installerStatusDialogButton(R.string.ok),
|
val confirmButton: InstallerStatusDialogButton = installerStatusDialogButton(R.string.ok),
|
||||||
val dismissButton: InstallerStatusDialogButton? = null,
|
val dismissButton: InstallerStatusDialogButton? = null,
|
||||||
@@ -133,10 +134,8 @@ enum class DialogKind(
|
|||||||
title = R.string.installation_storage_issue_dialog_title,
|
title = R.string.installation_storage_issue_dialog_title,
|
||||||
contentStringResId = R.string.installation_storage_issue_description,
|
contentStringResId = R.string.installation_storage_issue_description,
|
||||||
),
|
),
|
||||||
|
|
||||||
@RequiresApi(34)
|
|
||||||
FAILURE_TIMEOUT(
|
FAILURE_TIMEOUT(
|
||||||
flag = PackageInstaller.STATUS_FAILURE_TIMEOUT,
|
flag = @SuppressLint("InlinedApi") PackageInstaller.STATUS_FAILURE_TIMEOUT,
|
||||||
title = R.string.installation_timeout_dialog_title,
|
title = R.string.installation_timeout_dialog_title,
|
||||||
contentStringResId = R.string.installation_timeout_description,
|
contentStringResId = R.string.installation_timeout_description,
|
||||||
confirmButton = installerStatusDialogButton(R.string.install_app) { model ->
|
confirmButton = installerStatusDialogButton(R.string.install_app) { model ->
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
package app.revanced.manager.ui.viewmodel
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
@@ -19,7 +13,6 @@ import app.revanced.manager.data.room.apps.installed.InstallType
|
|||||||
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
||||||
import app.revanced.manager.domain.installer.RootInstaller
|
import app.revanced.manager.domain.installer.RootInstaller
|
||||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||||
import app.revanced.manager.service.UninstallService
|
|
||||||
import app.revanced.manager.util.PM
|
import app.revanced.manager.util.PM
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
import app.revanced.manager.util.simpleMessage
|
import app.revanced.manager.util.simpleMessage
|
||||||
@@ -30,6 +23,8 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
import ru.solrudev.ackpine.session.Session
|
||||||
|
import ru.solrudev.ackpine.uninstaller.UninstallFailure
|
||||||
|
|
||||||
class InstalledAppInfoViewModel(
|
class InstalledAppInfoViewModel(
|
||||||
packageName: String
|
packageName: String
|
||||||
@@ -87,51 +82,28 @@ class InstalledAppInfoViewModel(
|
|||||||
|
|
||||||
fun uninstall() {
|
fun uninstall() {
|
||||||
val app = installedApp ?: return
|
val app = installedApp ?: return
|
||||||
when (app.installType) {
|
viewModelScope.launch {
|
||||||
InstallType.DEFAULT -> pm.uninstallPackage(app.currentPackageName)
|
when (app.installType) {
|
||||||
|
InstallType.DEFAULT -> {
|
||||||
InstallType.MOUNT -> viewModelScope.launch {
|
when (val result = pm.uninstallPackage(app.currentPackageName)) {
|
||||||
rootInstaller.uninstall(app.currentPackageName)
|
is Session.State.Failed<UninstallFailure> -> {
|
||||||
installedAppRepository.delete(app)
|
val msg = result.failure.message.orEmpty()
|
||||||
onBackClick()
|
context.toast(
|
||||||
}
|
this@InstalledAppInfoViewModel.context.getString(
|
||||||
}
|
R.string.uninstall_app_fail,
|
||||||
}
|
msg
|
||||||
|
)
|
||||||
private val uninstallBroadcastReceiver = object : BroadcastReceiver() {
|
)
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
return@launch
|
||||||
when (intent?.action) {
|
|
||||||
UninstallService.APP_UNINSTALL_ACTION -> {
|
|
||||||
val extraStatus =
|
|
||||||
intent.getIntExtra(UninstallService.EXTRA_UNINSTALL_STATUS, -999)
|
|
||||||
val extraStatusMessage =
|
|
||||||
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
|
|
||||||
|
|
||||||
if (extraStatus == PackageInstaller.STATUS_SUCCESS) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
installedApp?.let {
|
|
||||||
installedAppRepository.delete(it)
|
|
||||||
}
|
|
||||||
onBackClick()
|
|
||||||
}
|
}
|
||||||
} else if (extraStatus != PackageInstaller.STATUS_FAILURE_ABORTED) {
|
Session.State.Succeeded -> {}
|
||||||
this@InstalledAppInfoViewModel.context.toast(this@InstalledAppInfoViewModel.context.getString(R.string.uninstall_app_fail, extraStatusMessage))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}.also {
|
|
||||||
ContextCompat.registerReceiver(
|
|
||||||
context,
|
|
||||||
it,
|
|
||||||
IntentFilter(UninstallService.APP_UNINSTALL_ACTION),
|
|
||||||
ContextCompat.RECEIVER_NOT_EXPORTED
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleared() {
|
InstallType.MOUNT -> rootInstaller.uninstall(app.currentPackageName)
|
||||||
super.onCleared()
|
}
|
||||||
context.unregisterReceiver(uninstallBroadcastReceiver)
|
installedAppRepository.delete(app)
|
||||||
|
onBackClick()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
package app.revanced.manager.ui.viewmodel
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.pm.PackageInstaller as AndroidPackageInstaller
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.ParcelUuid
|
import android.os.ParcelUuid
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -16,7 +14,6 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.saveable.autoSaver
|
import androidx.compose.runtime.saveable.autoSaver
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.toMutableStateList
|
import androidx.compose.runtime.toMutableStateList
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.map
|
import androidx.lifecycle.map
|
||||||
@@ -37,8 +34,6 @@ import app.revanced.manager.patcher.logger.Logger
|
|||||||
import app.revanced.manager.patcher.worker.PatcherWorker
|
import app.revanced.manager.patcher.worker.PatcherWorker
|
||||||
import app.revanced.manager.plugin.downloader.PluginHostApi
|
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||||
import app.revanced.manager.plugin.downloader.UserInteractionException
|
import app.revanced.manager.plugin.downloader.UserInteractionException
|
||||||
import app.revanced.manager.service.InstallService
|
|
||||||
import app.revanced.manager.service.UninstallService
|
|
||||||
import app.revanced.manager.ui.model.InstallerModel
|
import app.revanced.manager.ui.model.InstallerModel
|
||||||
import app.revanced.manager.ui.model.ProgressKey
|
import app.revanced.manager.ui.model.ProgressKey
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
@@ -48,16 +43,19 @@ import app.revanced.manager.ui.model.StepCategory
|
|||||||
import app.revanced.manager.ui.model.StepProgressProvider
|
import app.revanced.manager.ui.model.StepProgressProvider
|
||||||
import app.revanced.manager.ui.model.navigation.Patcher
|
import app.revanced.manager.ui.model.navigation.Patcher
|
||||||
import app.revanced.manager.util.PM
|
import app.revanced.manager.util.PM
|
||||||
|
import app.revanced.manager.util.asCode
|
||||||
import app.revanced.manager.util.saveableVar
|
import app.revanced.manager.util.saveableVar
|
||||||
import app.revanced.manager.util.saver.snapshotStateListSaver
|
import app.revanced.manager.util.saver.snapshotStateListSaver
|
||||||
import app.revanced.manager.util.simpleMessage
|
|
||||||
import app.revanced.manager.util.tag
|
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
import app.revanced.manager.util.uiSafe
|
import app.revanced.manager.util.uiSafe
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.NonCancellable
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -66,6 +64,15 @@ import kotlinx.coroutines.withContext
|
|||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
import ru.solrudev.ackpine.installer.InstallFailure
|
||||||
|
import ru.solrudev.ackpine.installer.PackageInstaller
|
||||||
|
import ru.solrudev.ackpine.installer.createSession
|
||||||
|
import ru.solrudev.ackpine.installer.getSession
|
||||||
|
import ru.solrudev.ackpine.session.ProgressSession
|
||||||
|
import ru.solrudev.ackpine.session.Session
|
||||||
|
import ru.solrudev.ackpine.session.await
|
||||||
|
import ru.solrudev.ackpine.session.parameters.Confirmation
|
||||||
|
import ru.solrudev.ackpine.uninstaller.UninstallFailure
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
@@ -81,6 +88,7 @@ class PatcherViewModel(
|
|||||||
private val installedAppRepository: InstalledAppRepository by inject()
|
private val installedAppRepository: InstalledAppRepository by inject()
|
||||||
private val rootInstaller: RootInstaller by inject()
|
private val rootInstaller: RootInstaller by inject()
|
||||||
private val savedStateHandle: SavedStateHandle = get()
|
private val savedStateHandle: SavedStateHandle = get()
|
||||||
|
private val ackpineInstaller: PackageInstaller = get()
|
||||||
|
|
||||||
private var installedApp: InstalledApp? = null
|
private var installedApp: InstalledApp? = null
|
||||||
private val selectedApp = input.selectedApp
|
private val selectedApp = input.selectedApp
|
||||||
@@ -95,7 +103,6 @@ class PatcherViewModel(
|
|||||||
mutableStateOf<String?>(null)
|
mutableStateOf<String?>(null)
|
||||||
}
|
}
|
||||||
private set
|
private set
|
||||||
private var ongoingPmSession: Boolean by savedStateHandle.saveableVar { false }
|
|
||||||
var packageInstallerStatus: Int? by savedStateHandle.saveable(
|
var packageInstallerStatus: Int? by savedStateHandle.saveable(
|
||||||
key = "packageInstallerStatus",
|
key = "packageInstallerStatus",
|
||||||
stateSaver = autoSaver()
|
stateSaver = autoSaver()
|
||||||
@@ -104,7 +111,7 @@ class PatcherViewModel(
|
|||||||
}
|
}
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var isInstalling by mutableStateOf(ongoingPmSession)
|
var isInstalling by mutableStateOf(false)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
private var currentActivityRequest: Pair<CompletableDeferred<Boolean>, String>? by mutableStateOf(
|
private var currentActivityRequest: Pair<CompletableDeferred<Boolean>, String>? by mutableStateOf(
|
||||||
@@ -123,6 +130,18 @@ class PatcherViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This coroutine scope is used to await installations.
|
||||||
|
* It should not be cancelled on system-initiated process death since that would cancel the installation process.
|
||||||
|
*/
|
||||||
|
private val installerCoroutineScope = CoroutineScope(Dispatchers.Main)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the package name of the Apk we are trying to install.
|
||||||
|
*/
|
||||||
|
private var installerPkgName: String by savedStateHandle.saveableVar { "" }
|
||||||
|
private var installerSessionId: ParcelUuid? by savedStateHandle.saveableVar()
|
||||||
|
|
||||||
private var inputFile: File? by savedStateHandle.saveableVar()
|
private var inputFile: File? by savedStateHandle.saveableVar()
|
||||||
private val outputFile = tempDir.resolve("output.apk")
|
private val outputFile = tempDir.resolve("output.apk")
|
||||||
|
|
||||||
@@ -174,67 +193,68 @@ class PatcherViewModel(
|
|||||||
private val workManager = WorkManager.getInstance(app)
|
private val workManager = WorkManager.getInstance(app)
|
||||||
|
|
||||||
private val patcherWorkerId by savedStateHandle.saveable<ParcelUuid> {
|
private val patcherWorkerId by savedStateHandle.saveable<ParcelUuid> {
|
||||||
ParcelUuid(workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
ParcelUuid(
|
||||||
"patching", PatcherWorker.Args(
|
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
||||||
input.selectedApp,
|
"patching", PatcherWorker.Args(
|
||||||
outputFile.path,
|
input.selectedApp,
|
||||||
input.selectedPatches,
|
outputFile.path,
|
||||||
input.options,
|
input.selectedPatches,
|
||||||
logger,
|
input.options,
|
||||||
onDownloadProgress = {
|
logger,
|
||||||
withContext(Dispatchers.Main) {
|
onDownloadProgress = {
|
||||||
downloadProgress = it
|
withContext(Dispatchers.Main) {
|
||||||
}
|
downloadProgress = it
|
||||||
},
|
}
|
||||||
onPatchCompleted = { withContext(Dispatchers.Main) { completedPatchCount += 1 } },
|
},
|
||||||
setInputFile = { withContext(Dispatchers.Main) { inputFile = it } },
|
onPatchCompleted = { withContext(Dispatchers.Main) { completedPatchCount += 1 } },
|
||||||
handleStartActivityRequest = { plugin, intent ->
|
setInputFile = { withContext(Dispatchers.Main) { inputFile = it } },
|
||||||
withContext(Dispatchers.Main) {
|
handleStartActivityRequest = { plugin, intent ->
|
||||||
if (currentActivityRequest != null) throw Exception("Another request is already pending.")
|
withContext(Dispatchers.Main) {
|
||||||
try {
|
if (currentActivityRequest != null) throw Exception("Another request is already pending.")
|
||||||
// Wait for the dialog interaction.
|
|
||||||
val accepted = with(CompletableDeferred<Boolean>()) {
|
|
||||||
currentActivityRequest = this to plugin.name
|
|
||||||
|
|
||||||
await()
|
|
||||||
}
|
|
||||||
if (!accepted) throw UserInteractionException.RequestDenied()
|
|
||||||
|
|
||||||
// Launch the activity and wait for the result.
|
|
||||||
try {
|
try {
|
||||||
with(CompletableDeferred<ActivityResult>()) {
|
// Wait for the dialog interaction.
|
||||||
launchedActivity = this
|
val accepted = with(CompletableDeferred<Boolean>()) {
|
||||||
launchActivityChannel.send(intent)
|
currentActivityRequest = this to plugin.name
|
||||||
|
|
||||||
await()
|
await()
|
||||||
}
|
}
|
||||||
|
if (!accepted) throw UserInteractionException.RequestDenied()
|
||||||
|
|
||||||
|
// Launch the activity and wait for the result.
|
||||||
|
try {
|
||||||
|
with(CompletableDeferred<ActivityResult>()) {
|
||||||
|
launchedActivity = this
|
||||||
|
launchActivityChannel.send(intent)
|
||||||
|
await()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
launchedActivity = null
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
launchedActivity = null
|
currentActivityRequest = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onProgress = { name, state, message ->
|
||||||
|
viewModelScope.launch {
|
||||||
|
steps[currentStepIndex] = steps[currentStepIndex].run {
|
||||||
|
copy(
|
||||||
|
name = name ?: this.name,
|
||||||
|
state = state ?: this.state,
|
||||||
|
message = message ?: this.message
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == State.COMPLETED && currentStepIndex != steps.lastIndex) {
|
||||||
|
currentStepIndex++
|
||||||
|
|
||||||
|
steps[currentStepIndex] =
|
||||||
|
steps[currentStepIndex].copy(state = State.RUNNING)
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
currentActivityRequest = null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
)
|
||||||
onProgress = { name, state, message ->
|
))
|
||||||
viewModelScope.launch {
|
|
||||||
steps[currentStepIndex] = steps[currentStepIndex].run {
|
|
||||||
copy(
|
|
||||||
name = name ?: this.name,
|
|
||||||
state = state ?: this.state,
|
|
||||||
message = message ?: this.message
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == State.COMPLETED && currentStepIndex != steps.lastIndex) {
|
|
||||||
currentStepIndex++
|
|
||||||
|
|
||||||
steps[currentStepIndex] =
|
|
||||||
steps[currentStepIndex].copy(state = State.RUNNING)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val patcherSucceeded =
|
val patcherSucceeded =
|
||||||
@@ -246,64 +266,26 @@ class PatcherViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val installerBroadcastReceiver = object : BroadcastReceiver() {
|
init {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
// TODO: detect system-initiated process death during the patching process.
|
||||||
when (intent?.action) {
|
|
||||||
InstallService.APP_INSTALL_ACTION -> {
|
|
||||||
val pmStatus = intent.getIntExtra(
|
|
||||||
InstallService.EXTRA_INSTALL_STATUS,
|
|
||||||
PackageInstaller.STATUS_FAILURE
|
|
||||||
)
|
|
||||||
|
|
||||||
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
|
installerSessionId?.uuid?.let { id ->
|
||||||
?.let(logger::trace)
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
if (pmStatus == PackageInstaller.STATUS_SUCCESS) {
|
isInstalling = true
|
||||||
app.toast(app.getString(R.string.install_app_success))
|
uiSafe(app, R.string.install_app_fail, "Failed to install") {
|
||||||
installedPackageName =
|
// The process was killed during installation. Await the session again.
|
||||||
intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME)
|
withContext(Dispatchers.IO) {
|
||||||
viewModelScope.launch {
|
ackpineInstaller.getSession(id)
|
||||||
installedAppRepository.addOrUpdate(
|
}?.let {
|
||||||
installedPackageName!!,
|
awaitInstallation(it)
|
||||||
packageName,
|
|
||||||
input.selectedApp.version
|
|
||||||
?: pm.getPackageInfo(outputFile)?.versionName!!,
|
|
||||||
InstallType.DEFAULT,
|
|
||||||
input.selectedPatches
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} else packageInstallerStatus = pmStatus
|
}
|
||||||
|
} finally {
|
||||||
isInstalling = false
|
isInstalling = false
|
||||||
}
|
}
|
||||||
|
|
||||||
UninstallService.APP_UNINSTALL_ACTION -> {
|
|
||||||
val pmStatus = intent.getIntExtra(
|
|
||||||
UninstallService.EXTRA_UNINSTALL_STATUS,
|
|
||||||
PackageInstaller.STATUS_FAILURE
|
|
||||||
)
|
|
||||||
|
|
||||||
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
|
|
||||||
?.let(logger::trace)
|
|
||||||
|
|
||||||
if (pmStatus != PackageInstaller.STATUS_SUCCESS)
|
|
||||||
packageInstallerStatus = pmStatus
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
// TODO: detect system-initiated process death during the patching process.
|
|
||||||
ContextCompat.registerReceiver(
|
|
||||||
app,
|
|
||||||
installerBroadcastReceiver,
|
|
||||||
IntentFilter().apply {
|
|
||||||
addAction(InstallService.APP_INSTALL_ACTION)
|
|
||||||
addAction(UninstallService.APP_UNINSTALL_ACTION)
|
|
||||||
},
|
|
||||||
ContextCompat.RECEIVER_NOT_EXPORTED
|
|
||||||
)
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
installedApp = installedAppRepository.get(packageName)
|
installedApp = installedAppRepository.get(packageName)
|
||||||
@@ -313,7 +295,6 @@ class PatcherViewModel(
|
|||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
app.unregisterReceiver(installerBroadcastReceiver)
|
|
||||||
workManager.cancelWorkById(patcherWorkerId.uuid)
|
workManager.cancelWorkById(patcherWorkerId.uuid)
|
||||||
|
|
||||||
if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.MOUNT) {
|
if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.MOUNT) {
|
||||||
@@ -328,6 +309,7 @@ class PatcherViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onBack() {
|
fun onBack() {
|
||||||
|
installerCoroutineScope.cancel()
|
||||||
// tempDir cannot be deleted inside onCleared because it gets called on system-initiated process death.
|
// tempDir cannot be deleted inside onCleared because it gets called on system-initiated process death.
|
||||||
tempDir.deleteRecursively()
|
tempDir.deleteRecursively()
|
||||||
}
|
}
|
||||||
@@ -372,44 +354,93 @@ class PatcherViewModel(
|
|||||||
|
|
||||||
fun open() = installedPackageName?.let(pm::launch)
|
fun open() = installedPackageName?.let(pm::launch)
|
||||||
|
|
||||||
fun install(installType: InstallType) = viewModelScope.launch {
|
private suspend fun startInstallation(file: File, packageName: String) {
|
||||||
var pmInstallStarted = false
|
val session = withContext(Dispatchers.IO) {
|
||||||
try {
|
ackpineInstaller.createSession(Uri.fromFile(file)) {
|
||||||
isInstalling = true
|
confirmation = Confirmation.IMMEDIATE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
installerPkgName = packageName
|
||||||
|
}
|
||||||
|
awaitInstallation(session)
|
||||||
|
}
|
||||||
|
|
||||||
val currentPackageInfo = pm.getPackageInfo(outputFile)
|
private suspend fun awaitInstallation(session: ProgressSession<InstallFailure>) = withContext(
|
||||||
?: throw Exception("Failed to load application info")
|
Dispatchers.Main
|
||||||
|
) {
|
||||||
// If the app is currently installed
|
val result = installerCoroutineScope.async {
|
||||||
val existingPackageInfo = pm.getPackageInfo(currentPackageInfo.packageName)
|
try {
|
||||||
if (existingPackageInfo != null) {
|
installerSessionId = ParcelUuid(session.id)
|
||||||
// Check if the app version is less than the installed version
|
withContext(Dispatchers.IO) {
|
||||||
if (pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(existingPackageInfo)) {
|
session.await()
|
||||||
// Exit if the selected app version is less than the installed version
|
|
||||||
packageInstallerStatus = PackageInstaller.STATUS_FAILURE_CONFLICT
|
|
||||||
return@launch
|
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
installerSessionId = null
|
||||||
|
}
|
||||||
|
}.await()
|
||||||
|
|
||||||
|
when (result) {
|
||||||
|
is Session.State.Failed<InstallFailure> -> {
|
||||||
|
result.failure.message?.let(logger::trace)
|
||||||
|
packageInstallerStatus = result.failure.asCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
when (installType) {
|
Session.State.Succeeded -> {
|
||||||
InstallType.DEFAULT -> {
|
app.toast(app.getString(R.string.install_app_success))
|
||||||
// Check if the app is mounted as root
|
installedPackageName = installerPkgName
|
||||||
// If it is, unmount it first, silently
|
installedAppRepository.addOrUpdate(
|
||||||
if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) {
|
installerPkgName,
|
||||||
rootInstaller.unmount(packageName)
|
packageName,
|
||||||
}
|
input.selectedApp.version
|
||||||
|
?: withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile)?.versionName!! },
|
||||||
|
InstallType.DEFAULT,
|
||||||
|
input.selectedPatches
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Install regularly
|
fun install(installType: InstallType) = viewModelScope.launch {
|
||||||
pm.installApp(listOf(outputFile))
|
isInstalling = true
|
||||||
pmInstallStarted = true
|
var needsRootUninstall = false
|
||||||
|
try {
|
||||||
|
uiSafe(app, R.string.install_app_fail, "Failed to install") {
|
||||||
|
val currentPackageInfo =
|
||||||
|
withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile) }
|
||||||
|
?: throw Exception("Failed to load application info")
|
||||||
|
|
||||||
|
// If the app is currently installed
|
||||||
|
val existingPackageInfo =
|
||||||
|
withContext(Dispatchers.IO) { pm.getPackageInfo(currentPackageInfo.packageName) }
|
||||||
|
if (existingPackageInfo != null) {
|
||||||
|
// Check if the app version is less than the installed version
|
||||||
|
if (
|
||||||
|
pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(
|
||||||
|
existingPackageInfo
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// Exit if the selected app version is less than the installed version
|
||||||
|
packageInstallerStatus = AndroidPackageInstaller.STATUS_FAILURE_CONFLICT
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
InstallType.MOUNT -> {
|
when (installType) {
|
||||||
try {
|
InstallType.DEFAULT -> {
|
||||||
val packageInfo = pm.getPackageInfo(outputFile)
|
// Check if the app is mounted as root
|
||||||
?: throw Exception("Failed to load application info")
|
// If it is, unmount it first, silently
|
||||||
|
if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) {
|
||||||
|
rootInstaller.unmount(packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install regularly
|
||||||
|
startInstallation(outputFile, currentPackageInfo.packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
InstallType.MOUNT -> {
|
||||||
val label = with(pm) {
|
val label = with(pm) {
|
||||||
packageInfo.label()
|
currentPackageInfo.label()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for base APK, first check if the app is already installed
|
// Check for base APK, first check if the app is already installed
|
||||||
@@ -417,15 +448,17 @@ class PatcherViewModel(
|
|||||||
// If the app is not installed, check if the output file is a base apk
|
// If the app is not installed, check if the output file is a base apk
|
||||||
if (currentPackageInfo.splitNames.isNotEmpty()) {
|
if (currentPackageInfo.splitNames.isNotEmpty()) {
|
||||||
// Exit if there is no base APK package
|
// Exit if there is no base APK package
|
||||||
packageInstallerStatus = PackageInstaller.STATUS_FAILURE_INVALID
|
packageInstallerStatus =
|
||||||
|
AndroidPackageInstaller.STATUS_FAILURE_INVALID
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val inputVersion = input.selectedApp.version
|
val inputVersion = input.selectedApp.version
|
||||||
?: inputFile?.let(pm::getPackageInfo)?.versionName
|
?: withContext(Dispatchers.IO) { inputFile?.let(pm::getPackageInfo)?.versionName }
|
||||||
?: throw Exception("Failed to determine input APK version")
|
?: throw Exception("Failed to determine input APK version")
|
||||||
|
|
||||||
|
needsRootUninstall = true
|
||||||
// Install as root
|
// Install as root
|
||||||
rootInstaller.install(
|
rootInstaller.install(
|
||||||
outputFile,
|
outputFile,
|
||||||
@@ -436,7 +469,7 @@ class PatcherViewModel(
|
|||||||
)
|
)
|
||||||
|
|
||||||
installedAppRepository.addOrUpdate(
|
installedAppRepository.addOrUpdate(
|
||||||
packageInfo.packageName,
|
currentPackageInfo.packageName,
|
||||||
packageName,
|
packageName,
|
||||||
inputVersion,
|
inputVersion,
|
||||||
InstallType.MOUNT,
|
InstallType.MOUNT,
|
||||||
@@ -448,21 +481,20 @@ class PatcherViewModel(
|
|||||||
installedPackageName = packageName
|
installedPackageName = packageName
|
||||||
|
|
||||||
app.toast(app.getString(R.string.install_app_success))
|
app.toast(app.getString(R.string.install_app_success))
|
||||||
} catch (e: Exception) {
|
needsRootUninstall = false
|
||||||
Log.e(tag, "Failed to install as root", e)
|
|
||||||
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
|
|
||||||
try {
|
|
||||||
rootInstaller.uninstall(packageName)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(tag, "Failed to install", e)
|
|
||||||
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
|
|
||||||
} finally {
|
} finally {
|
||||||
if (!pmInstallStarted) isInstalling = false
|
isInstalling = false
|
||||||
|
if (needsRootUninstall) {
|
||||||
|
try {
|
||||||
|
withContext(NonCancellable) {
|
||||||
|
rootInstaller.uninstall(packageName)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,12 +505,27 @@ class PatcherViewModel(
|
|||||||
|
|
||||||
override fun reinstall() {
|
override fun reinstall() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") {
|
try {
|
||||||
pm.getPackageInfo(outputFile)?.packageName?.let { pm.uninstallPackage(it) }
|
|
||||||
?: throw Exception("Failed to load application info")
|
|
||||||
|
|
||||||
pm.installApp(listOf(outputFile))
|
|
||||||
isInstalling = true
|
isInstalling = true
|
||||||
|
uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") {
|
||||||
|
val pkgName = withContext(Dispatchers.IO) {
|
||||||
|
pm.getPackageInfo(outputFile)?.packageName
|
||||||
|
?: throw Exception("Failed to load application info")
|
||||||
|
}
|
||||||
|
|
||||||
|
when (val result = pm.uninstallPackage(pkgName)) {
|
||||||
|
is Session.State.Failed<UninstallFailure> -> {
|
||||||
|
result.failure.message?.let(logger::trace)
|
||||||
|
packageInstallerStatus = result.failure.asCode()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
Session.State.Succeeded -> {}
|
||||||
|
}
|
||||||
|
startInstallation(outputFile, pkgName)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isInstalling = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
package app.revanced.manager.ui.viewmodel
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.BroadcastReceiver
|
import android.net.Uri
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableLongStateOf
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
@@ -21,8 +16,6 @@ import app.revanced.manager.data.platform.NetworkInfo
|
|||||||
import app.revanced.manager.network.api.ReVancedAPI
|
import app.revanced.manager.network.api.ReVancedAPI
|
||||||
import app.revanced.manager.network.dto.ReVancedAsset
|
import app.revanced.manager.network.dto.ReVancedAsset
|
||||||
import app.revanced.manager.network.service.HttpService
|
import app.revanced.manager.network.service.HttpService
|
||||||
import app.revanced.manager.service.InstallService
|
|
||||||
import app.revanced.manager.util.PM
|
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
import app.revanced.manager.util.uiSafe
|
import app.revanced.manager.util.uiSafe
|
||||||
import io.ktor.client.plugins.onDownload
|
import io.ktor.client.plugins.onDownload
|
||||||
@@ -31,7 +24,14 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.get
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
import ru.solrudev.ackpine.installer.InstallFailure
|
||||||
|
import ru.solrudev.ackpine.installer.PackageInstaller
|
||||||
|
import ru.solrudev.ackpine.installer.createSession
|
||||||
|
import ru.solrudev.ackpine.session.Session
|
||||||
|
import ru.solrudev.ackpine.session.await
|
||||||
|
import ru.solrudev.ackpine.session.parameters.Confirmation
|
||||||
|
|
||||||
class UpdateViewModel(
|
class UpdateViewModel(
|
||||||
private val downloadOnScreenEntry: Boolean
|
private val downloadOnScreenEntry: Boolean
|
||||||
@@ -39,10 +39,11 @@ class UpdateViewModel(
|
|||||||
private val app: Application by inject()
|
private val app: Application by inject()
|
||||||
private val reVancedAPI: ReVancedAPI by inject()
|
private val reVancedAPI: ReVancedAPI by inject()
|
||||||
private val http: HttpService by inject()
|
private val http: HttpService by inject()
|
||||||
private val pm: PM by inject()
|
|
||||||
private val networkInfo: NetworkInfo by inject()
|
private val networkInfo: NetworkInfo by inject()
|
||||||
private val fs: Filesystem by inject()
|
private val fs: Filesystem by inject()
|
||||||
|
private val ackpineInstaller: PackageInstaller = get()
|
||||||
|
|
||||||
|
// TODO: save state to handle process death.
|
||||||
var downloadedSize by mutableLongStateOf(0L)
|
var downloadedSize by mutableLongStateOf(0L)
|
||||||
private set
|
private set
|
||||||
var totalSize by mutableLongStateOf(0L)
|
var totalSize by mutableLongStateOf(0L)
|
||||||
@@ -62,14 +63,17 @@ class UpdateViewModel(
|
|||||||
private set
|
private set
|
||||||
|
|
||||||
private val location = fs.tempDir.resolve("updater.apk")
|
private val location = fs.tempDir.resolve("updater.apk")
|
||||||
private val job = viewModelScope.launch {
|
|
||||||
uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") {
|
|
||||||
releaseInfo = reVancedAPI.getAppUpdate() ?: throw Exception("No update available")
|
|
||||||
|
|
||||||
if (downloadOnScreenEntry) {
|
init {
|
||||||
downloadUpdate()
|
viewModelScope.launch {
|
||||||
} else {
|
uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") {
|
||||||
state = State.CAN_DOWNLOAD
|
releaseInfo = reVancedAPI.getAppUpdate() ?: throw Exception("No update available")
|
||||||
|
|
||||||
|
if (downloadOnScreenEntry) {
|
||||||
|
downloadUpdate()
|
||||||
|
} else {
|
||||||
|
state = State.CAN_DOWNLOAD
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,50 +102,36 @@ class UpdateViewModel(
|
|||||||
|
|
||||||
fun installUpdate() = viewModelScope.launch {
|
fun installUpdate() = viewModelScope.launch {
|
||||||
state = State.INSTALLING
|
state = State.INSTALLING
|
||||||
|
val result = withContext(Dispatchers.IO) {
|
||||||
|
ackpineInstaller.createSession(Uri.fromFile(location)) {
|
||||||
|
confirmation = Confirmation.IMMEDIATE
|
||||||
|
}.await()
|
||||||
|
}
|
||||||
|
|
||||||
pm.installApp(listOf(location))
|
when (result) {
|
||||||
}
|
is Session.State.Failed<InstallFailure> -> when (val failure = result.failure) {
|
||||||
|
is InstallFailure.Aborted -> state = State.CAN_INSTALL
|
||||||
private val installBroadcastReceiver = object : BroadcastReceiver() {
|
else -> {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
val msg = failure.message.orEmpty()
|
||||||
intent?.let {
|
app.toast(app.getString(R.string.install_app_fail, msg))
|
||||||
val pmStatus = intent.getIntExtra(InstallService.EXTRA_INSTALL_STATUS, -999)
|
installError = msg
|
||||||
val extra =
|
state = State.FAILED
|
||||||
intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE)!!
|
|
||||||
|
|
||||||
when(pmStatus) {
|
|
||||||
PackageInstaller.STATUS_SUCCESS -> {
|
|
||||||
app.toast(app.getString(R.string.install_app_success))
|
|
||||||
state = State.SUCCESS
|
|
||||||
}
|
|
||||||
PackageInstaller.STATUS_FAILURE_ABORTED -> {
|
|
||||||
state = State.CAN_INSTALL
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
app.toast(app.getString(R.string.install_app_fail, extra))
|
|
||||||
installError = extra
|
|
||||||
state = State.FAILED
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Session.State.Succeeded -> {
|
||||||
|
app.toast(app.getString(R.string.install_app_success))
|
||||||
|
state = State.SUCCESS
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
|
||||||
ContextCompat.registerReceiver(app, installBroadcastReceiver, IntentFilter().apply {
|
|
||||||
addAction(InstallService.APP_INSTALL_ACTION)
|
|
||||||
}, ContextCompat.RECEIVER_NOT_EXPORTED)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
app.unregisterReceiver(installBroadcastReceiver)
|
|
||||||
|
|
||||||
job.cancel()
|
|
||||||
location.delete()
|
location.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class State(@StringRes val title: Int) {
|
enum class State(@param:StringRes val title: Int) {
|
||||||
CAN_DOWNLOAD(R.string.update_available),
|
CAN_DOWNLOAD(R.string.update_available),
|
||||||
DOWNLOADING(R.string.downloading_manager_update),
|
DOWNLOADING(R.string.downloading_manager_update),
|
||||||
CAN_INSTALL(R.string.ready_to_install_update),
|
CAN_INSTALL(R.string.ready_to_install_update),
|
||||||
|
|||||||
30
app/src/main/java/app/revanced/manager/util/Ackpine.kt
Normal file
30
app/src/main/java/app/revanced/manager/util/Ackpine.kt
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package app.revanced.manager.util
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
|
import ru.solrudev.ackpine.installer.InstallFailure
|
||||||
|
import ru.solrudev.ackpine.uninstaller.UninstallFailure
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an Ackpine installation failure into a PM status code
|
||||||
|
*/
|
||||||
|
fun InstallFailure.asCode() = when (this) {
|
||||||
|
is InstallFailure.Aborted -> PackageInstaller.STATUS_FAILURE_ABORTED
|
||||||
|
is InstallFailure.Blocked -> PackageInstaller.STATUS_FAILURE_BLOCKED
|
||||||
|
is InstallFailure.Conflict -> PackageInstaller.STATUS_FAILURE_CONFLICT
|
||||||
|
is InstallFailure.Incompatible -> PackageInstaller.STATUS_FAILURE_INCOMPATIBLE
|
||||||
|
is InstallFailure.Invalid -> PackageInstaller.STATUS_FAILURE_INVALID
|
||||||
|
is InstallFailure.Storage -> PackageInstaller.STATUS_FAILURE_STORAGE
|
||||||
|
is InstallFailure.Timeout -> @SuppressLint("InlinedApi") PackageInstaller.STATUS_FAILURE_TIMEOUT
|
||||||
|
else -> PackageInstaller.STATUS_FAILURE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an Ackpine uninstallation failure into a PM status code
|
||||||
|
*/
|
||||||
|
fun UninstallFailure.asCode() = when (this) {
|
||||||
|
is UninstallFailure.Aborted -> PackageInstaller.STATUS_FAILURE_ABORTED
|
||||||
|
is UninstallFailure.Blocked -> PackageInstaller.STATUS_FAILURE_BLOCKED
|
||||||
|
is UninstallFailure.Conflict -> PackageInstaller.STATUS_FAILURE_CONFLICT
|
||||||
|
else -> PackageInstaller.STATUS_FAILURE
|
||||||
|
}
|
||||||
@@ -2,11 +2,8 @@ package app.revanced.manager.util
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.pm.PackageManager.PackageInfoFlags
|
import android.content.pm.PackageManager.PackageInfoFlags
|
||||||
import android.content.pm.PackageManager.NameNotFoundException
|
import android.content.pm.PackageManager.NameNotFoundException
|
||||||
@@ -16,8 +13,6 @@ import android.os.Build
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
import app.revanced.manager.service.InstallService
|
|
||||||
import app.revanced.manager.service.UninstallService
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
@@ -25,10 +20,13 @@ import kotlinx.coroutines.flow.flowOn
|
|||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import ru.solrudev.ackpine.session.await
|
||||||
|
import ru.solrudev.ackpine.session.parameters.Confirmation
|
||||||
|
import ru.solrudev.ackpine.uninstaller.PackageUninstaller
|
||||||
|
import ru.solrudev.ackpine.uninstaller.createSession
|
||||||
|
import ru.solrudev.ackpine.uninstaller.parameters.UninstallParametersDsl
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable
|
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class AppInfo(
|
data class AppInfo(
|
||||||
@@ -40,7 +38,8 @@ data class AppInfo(
|
|||||||
@SuppressLint("QueryPermissionsNeeded")
|
@SuppressLint("QueryPermissionsNeeded")
|
||||||
class PM(
|
class PM(
|
||||||
private val app: Application,
|
private val app: Application,
|
||||||
patchBundleRepository: PatchBundleRepository
|
patchBundleRepository: PatchBundleRepository,
|
||||||
|
private val uninstaller: PackageUninstaller
|
||||||
) {
|
) {
|
||||||
private val scope = CoroutineScope(Dispatchers.IO)
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
@@ -145,17 +144,11 @@ class PM(
|
|||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun installApp(apks: List<File>) = withContext(Dispatchers.IO) {
|
suspend fun uninstallPackage(pkg: String, config: UninstallParametersDsl.() -> Unit = {}) = withContext(Dispatchers.IO) {
|
||||||
val packageInstaller = app.packageManager.packageInstaller
|
uninstaller.createSession(pkg) {
|
||||||
packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session ->
|
confirmation = Confirmation.IMMEDIATE
|
||||||
apks.forEach { apk -> session.writeApk(apk) }
|
config()
|
||||||
session.commit(app.installIntentSender)
|
}.await()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun uninstallPackage(pkg: String) {
|
|
||||||
val packageInstaller = app.packageManager.packageInstaller
|
|
||||||
packageInstaller.uninstall(pkg, app.uninstallIntentSender)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launch(pkg: String) = app.packageManager.getLaunchIntentForPackage(pkg)?.let {
|
fun launch(pkg: String) = app.packageManager.getLaunchIntentForPackage(pkg)?.let {
|
||||||
@@ -164,44 +157,4 @@ class PM(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun canInstallPackages() = app.packageManager.canRequestPackageInstalls()
|
fun canInstallPackages() = app.packageManager.canRequestPackageInstalls()
|
||||||
|
|
||||||
private fun PackageInstaller.Session.writeApk(apk: File) {
|
|
||||||
apk.inputStream().use { inputStream ->
|
|
||||||
openWrite(apk.name, 0, apk.length()).use { outputStream ->
|
|
||||||
inputStream.copyTo(outputStream, byteArraySize)
|
|
||||||
fsync(outputStream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val intentFlags
|
|
||||||
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
|
||||||
PendingIntent.FLAG_MUTABLE
|
|
||||||
else
|
|
||||||
0
|
|
||||||
|
|
||||||
private val sessionParams
|
|
||||||
get() = PackageInstaller.SessionParams(
|
|
||||||
PackageInstaller.SessionParams.MODE_FULL_INSTALL
|
|
||||||
).apply {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
||||||
setRequestUpdateOwnership(true)
|
|
||||||
setInstallReason(PackageManager.INSTALL_REASON_USER)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val Context.installIntentSender
|
|
||||||
get() = PendingIntent.getService(
|
|
||||||
this,
|
|
||||||
0,
|
|
||||||
Intent(this, InstallService::class.java),
|
|
||||||
intentFlags
|
|
||||||
).intentSender
|
|
||||||
|
|
||||||
private val Context.uninstallIntentSender
|
|
||||||
get() = PendingIntent.getService(
|
|
||||||
this,
|
|
||||||
0,
|
|
||||||
Intent(this, UninstallService::class.java),
|
|
||||||
intentFlags
|
|
||||||
).intentSender
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import androidx.lifecycle.SavedStateHandle
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -82,6 +83,8 @@ fun Context.toast(string: String, duration: Int = Toast.LENGTH_SHORT) {
|
|||||||
inline fun uiSafe(context: Context, @StringRes toastMsg: Int, logMsg: String, block: () -> Unit) {
|
inline fun uiSafe(context: Context, @StringRes toastMsg: Int, logMsg: String, block: () -> Unit) {
|
||||||
try {
|
try {
|
||||||
block()
|
block()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (error: Exception) {
|
} catch (error: Exception) {
|
||||||
// You can only toast on the main thread.
|
// You can only toast on the main thread.
|
||||||
GlobalScope.launch(Dispatchers.Main) {
|
GlobalScope.launch(Dispatchers.Main) {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ kotlin-process = "1.5.1"
|
|||||||
hidden-api-stub = "4.3.3"
|
hidden-api-stub = "4.3.3"
|
||||||
binary-compatibility-validator = "0.17.0"
|
binary-compatibility-validator = "0.17.0"
|
||||||
semver-parser = "3.0.0"
|
semver-parser = "3.0.0"
|
||||||
|
ackpine = "0.18.5"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
# AndroidX Core
|
# AndroidX Core
|
||||||
@@ -133,6 +134,10 @@ compose-icons-fontawesome = { group = "com.github.BenjaminHalko.compose-icons",
|
|||||||
# Semantic versioning parser
|
# Semantic versioning parser
|
||||||
semver-parser = { module = "io.github.z4kn4fein:semver", version.ref = "semver-parser" }
|
semver-parser = { module = "io.github.z4kn4fein:semver", version.ref = "semver-parser" }
|
||||||
|
|
||||||
|
# Ackpine
|
||||||
|
ackpine-core = { module = "ru.solrudev.ackpine:ackpine-core", version.ref = "ackpine" }
|
||||||
|
ackpine-ktx = { module = "ru.solrudev.ackpine:ackpine-ktx", version.ref = "ackpine" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
|
android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
|
||||||
android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" }
|
android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" }
|
||||||
|
|||||||
Reference in New Issue
Block a user