From 0930b9fda7d7bd512645d384e43848d93daadc3f Mon Sep 17 00:00:00 2001 From: Ax333l Date: Sat, 17 Aug 2024 21:25:13 +0200 Subject: [PATCH 1/7] WIP --- .../revanced/manager/ManagerApplication.kt | 36 ++++++ .../manager/data/platform/Filesystem.kt | 26 ++++- .../app/revanced/manager/patcher/Session.kt | 2 +- .../patcher/runtime/CoroutineRuntime.kt | 2 +- .../manager/patcher/runtime/ProcessRuntime.kt | 6 +- .../manager/patcher/runtime/Runtime.kt | 2 +- .../manager/patcher/worker/PatcherWorker.kt | 12 +- .../manager/ui/component/patcher/Steps.kt | 37 +++--- .../revanced/manager/ui/model/PatcherStep.kt | 15 ++- .../manager/ui/screen/PatcherScreen.kt | 23 +--- .../ui/viewmodel/AppSelectorViewModel.kt | 6 +- .../manager/ui/viewmodel/PatcherViewModel.kt | 107 +++++++++++++----- .../java/app/revanced/manager/util/Util.kt | 44 ++++++- 13 files changed, 227 insertions(+), 91 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/ManagerApplication.kt b/app/src/main/java/app/revanced/manager/ManagerApplication.kt index 66ab2483eb..8a37cbe256 100644 --- a/app/src/main/java/app/revanced/manager/ManagerApplication.kt +++ b/app/src/main/java/app/revanced/manager/ManagerApplication.kt @@ -1,9 +1,14 @@ package app.revanced.manager +import android.app.Activity import android.app.Application +import android.os.Bundle +import android.util.Log +import app.revanced.manager.data.platform.Filesystem import app.revanced.manager.di.* import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.util.tag import kotlinx.coroutines.Dispatchers import coil.Coil import coil.ImageLoader @@ -23,6 +28,8 @@ class ManagerApplication : Application() { private val scope = MainScope() private val prefs: PreferencesManager by inject() private val patchBundleRepository: PatchBundleRepository by inject() + private val fs: Filesystem by inject() + override fun onCreate() { super.onCreate() @@ -65,5 +72,34 @@ class ManagerApplication : Application() { updateCheck() } } + registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { + private var firstActivityCreated = false + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + if (firstActivityCreated) return + firstActivityCreated = true + + // We do not want to call onFreshProcessStart() if there is state to restore. + // This can happen on system-initiated process death. + if (savedInstanceState == null) { + Log.d(tag, "Fresh process created") + onFreshProcessStart() + } else Log.d(tag, "System-initiated process death detected") + } + + override fun onActivityStarted(activity: Activity) {} + override fun onActivityResumed(activity: Activity) {} + override fun onActivityPaused(activity: Activity) {} + override fun onActivityStopped(activity: Activity) {} + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} + override fun onActivityDestroyed(activity: Activity) {} + }) + } + + private fun onFreshProcessStart() { + fs.uiTempDir.apply { + deleteRecursively() + mkdirs() + } } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/platform/Filesystem.kt b/app/src/main/java/app/revanced/manager/data/platform/Filesystem.kt index 3afbe6e8e5..97087f3e52 100644 --- a/app/src/main/java/app/revanced/manager/data/platform/Filesystem.kt +++ b/app/src/main/java/app/revanced/manager/data/platform/Filesystem.kt @@ -9,6 +9,8 @@ import android.os.Environment import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContracts import app.revanced.manager.util.RequestManageStorageContract +import java.io.File +import java.nio.file.Path class Filesystem(private val app: Application) { val contentResolver = app.contentResolver // TODO: move Content Resolver operations to here. @@ -17,21 +19,35 @@ class Filesystem(private val app: Application) { * A directory that gets cleared when the app restarts. * Do not store paths to this directory in a parcel. */ - val tempDir = app.getDir("ephemeral", Context.MODE_PRIVATE).apply { + val tempDir: File = app.getDir("ephemeral", Context.MODE_PRIVATE).apply { deleteRecursively() mkdirs() } - fun externalFilesDir() = Environment.getExternalStorageDirectory().toPath() + /** + * A directory for storing temporary files related to UI. + * This is the same as [tempDir], but does not get cleared on system-initiated process death. + * Paths to this directory can be safely stored in parcels. + */ + val uiTempDir: File = app.getDir("ui_ephemeral", Context.MODE_PRIVATE).apply { + mkdirs() + } + + fun externalFilesDir(): Path = Environment.getExternalStorageDirectory().toPath() private fun usesManagePermission() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R - private val storagePermissionName = if (usesManagePermission()) Manifest.permission.MANAGE_EXTERNAL_STORAGE else Manifest.permission.READ_EXTERNAL_STORAGE + private val storagePermissionName = + if (usesManagePermission()) Manifest.permission.MANAGE_EXTERNAL_STORAGE else Manifest.permission.READ_EXTERNAL_STORAGE fun permissionContract(): Pair, String> { - val contract = if (usesManagePermission()) RequestManageStorageContract() else ActivityResultContracts.RequestPermission() + val contract = + if (usesManagePermission()) RequestManageStorageContract() else ActivityResultContracts.RequestPermission() return contract to storagePermissionName } - fun hasStoragePermission() = if (usesManagePermission()) Environment.isExternalStorageManager() else app.checkSelfPermission(storagePermissionName) == PackageManager.PERMISSION_GRANTED + fun hasStoragePermission() = + if (usesManagePermission()) Environment.isExternalStorageManager() else app.checkSelfPermission( + storagePermissionName + ) == PackageManager.PERMISSION_GRANTED } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/Session.kt b/app/src/main/java/app/revanced/manager/patcher/Session.kt index 4393794db2..a50fd7f933 100644 --- a/app/src/main/java/app/revanced/manager/patcher/Session.kt +++ b/app/src/main/java/app/revanced/manager/patcher/Session.kt @@ -26,7 +26,7 @@ class Session( private val androidContext: Context, private val logger: Logger, private val input: File, - private val onPatchCompleted: () -> Unit, + private val onPatchCompleted: suspend () -> Unit, private val onProgress: (name: String?, state: State?, message: String?) -> Unit ) : Closeable { private fun updateProgress(name: String? = null, state: State? = null, message: String? = null) = diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt index e2aed2eeb5..59a4fd32c0 100644 --- a/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt @@ -20,7 +20,7 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) { selectedPatches: PatchSelection, options: Options, logger: Logger, - onPatchCompleted: () -> Unit, + onPatchCompleted: suspend () -> Unit, onProgress: ProgressEventHandler, ) { val bundles = bundles() diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt index 389d5201bb..103dbe0748 100644 --- a/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt @@ -66,7 +66,7 @@ class ProcessRuntime(private val context: Context) : Runtime(context) { selectedPatches: PatchSelection, options: Options, logger: Logger, - onPatchCompleted: () -> Unit, + onPatchCompleted: suspend () -> Unit, onProgress: ProgressEventHandler, ) = coroutineScope { // Get the location of our own Apk. @@ -123,7 +123,9 @@ class ProcessRuntime(private val context: Context) : Runtime(context) { val eventHandler = object : IPatcherEvents.Stub() { override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg) - override fun patchSucceeded() = onPatchCompleted() + override fun patchSucceeded() { + launch { onPatchCompleted() } + } override fun progress(name: String?, state: String?, msg: String?) = onProgress(name, state?.let { enumValueOf(it) }, msg) diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt index fd39c3f305..e02a6f8645 100644 --- a/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt @@ -35,7 +35,7 @@ sealed class Runtime(context: Context) : KoinComponent { selectedPatches: PatchSelection, options: Options, logger: Logger, - onPatchCompleted: () -> Unit, + onPatchCompleted: suspend () -> Unit, onProgress: ProgressEventHandler, ) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt index 0e779df790..17bca981cd 100644 --- a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt +++ b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt @@ -61,9 +61,9 @@ class PatcherWorker( val selectedPatches: PatchSelection, val options: Options, val logger: Logger, - val downloadProgress: MutableStateFlow?>, - val patchesProgress: MutableStateFlow>, - val setInputFile: (File) -> Unit, + val onDownloadProgress: suspend (Pair?) -> Unit, + val onPatchCompleted: suspend () -> Unit, + val setInputFile: suspend (File) -> Unit, val onProgress: ProgressEventHandler ) { val packageName get() = input.packageName @@ -146,7 +146,7 @@ class PatcherWorker( downloadedAppRepository.download( selectedApp.app, prefs.preferSplits.get(), - onDownload = { args.downloadProgress.emit(it) } + onDownload = args.onDownloadProgress ).also { args.setInputFile(it) updateProgress(state = State.COMPLETED) // Download APK @@ -170,11 +170,13 @@ class PatcherWorker( args.selectedPatches, args.options, args.logger, + /* onPatchCompleted = { args.patchesProgress.update { (completed, total) -> completed + 1 to total } - }, + },*/ + args.onPatchCompleted, args.onProgress ) diff --git a/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt b/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt index 6840837b7c..209521686b 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt @@ -36,13 +36,14 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R import app.revanced.manager.ui.component.ArrowButton import app.revanced.manager.ui.component.LoadingIndicator +import app.revanced.manager.ui.model.ProgressKey import app.revanced.manager.ui.model.State import app.revanced.manager.ui.model.Step import app.revanced.manager.ui.model.StepCategory +import app.revanced.manager.ui.model.StepProgressProvider import kotlin.math.floor // Credits: https://github.com/Aliucord/AliucordManager/blob/main/app/src/main/kotlin/com/aliucord/manager/ui/component/installer/InstallGroup.kt @@ -51,6 +52,7 @@ fun Steps( category: StepCategory, steps: List, stepCount: Pair? = null, + stepProgressProvider: StepProgressProvider ) { var expanded by rememberSaveable { mutableStateOf(true) } @@ -115,13 +117,17 @@ fun Steps( modifier = Modifier.fillMaxWidth() ) { steps.forEach { step -> - val downloadProgress = step.downloadProgress?.collectAsStateWithLifecycle() + val (progress, progressText) = when (step.progressKey) { + null -> null + ProgressKey.DOWNLOAD -> stepProgressProvider.downloadProgress?.let { (downloaded, total) -> downloaded / total to "$downloaded/$total MB" } + } ?: (null to null) SubStep( name = step.name, state = step.state, message = step.message, - downloadProgress = downloadProgress?.value + progress = progress, + progressText = progressText ) } } @@ -134,7 +140,8 @@ fun SubStep( name: String, state: State, message: String? = null, - downloadProgress: Pair? = null + progress: Float? = null, + progressText: String? = null ) { var messageExpanded by rememberSaveable { mutableStateOf(true) } @@ -155,7 +162,7 @@ fun SubStep( modifier = Modifier.size(24.dp), contentAlignment = Alignment.Center ) { - StepIcon(state, downloadProgress, size = 20.dp) + StepIcon(state, progress, size = 20.dp) } Text( @@ -166,8 +173,8 @@ fun SubStep( modifier = Modifier.weight(1f, true), ) - if (message != null) { - Box( + when { + message != null -> Box( modifier = Modifier.size(24.dp), contentAlignment = Alignment.Center ) { @@ -177,13 +184,11 @@ fun SubStep( onClick = null ) } - } else { - downloadProgress?.let { (current, total) -> - Text( - "$current/$total MB", - style = MaterialTheme.typography.labelSmall - ) - } + + progressText != null -> Text( + progressText, + style = MaterialTheme.typography.labelSmall + ) } } @@ -199,7 +204,7 @@ fun SubStep( } @Composable -fun StepIcon(state: State, progress: Pair? = null, size: Dp) { +fun StepIcon(state: State, progress: Float? = null, size: Dp) { val strokeWidth = Dp(floor(size.value / 10) + 1) when (state) { @@ -233,7 +238,7 @@ fun StepIcon(state: State, progress: Pair? = null, size: Dp) { contentDescription = description } }, - progress = { progress?.let { (current, total) -> current / total } }, + progress = { progress }, strokeWidth = strokeWidth ) } diff --git a/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt b/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt index 4c7fc417e8..e2f3ea2b49 100644 --- a/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt +++ b/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt @@ -1,8 +1,10 @@ package app.revanced.manager.ui.model +import android.os.Parcelable import androidx.annotation.StringRes import app.revanced.manager.R import kotlinx.coroutines.flow.StateFlow +import kotlinx.parcelize.Parcelize enum class StepCategory(@StringRes val displayName: Int) { PREPARING(R.string.patcher_step_group_preparing), @@ -14,10 +16,19 @@ enum class State { WAITING, RUNNING, FAILED, COMPLETED } +enum class ProgressKey { + DOWNLOAD +} + +interface StepProgressProvider { + val downloadProgress: Pair? +} + +@Parcelize data class Step( val name: String, val category: StepCategory, val state: State = State.WAITING, val message: String? = null, - val downloadProgress: StateFlow?>? = null -) \ No newline at end of file + val progressKey: ProgressKey? = null +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt index 163dfbc652..6483b96e85 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt @@ -36,13 +36,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R import app.revanced.manager.ui.component.AppScaffold import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.patcher.InstallPickerDialog import app.revanced.manager.ui.component.patcher.Steps -import app.revanced.manager.ui.model.State import app.revanced.manager.ui.model.StepCategory import app.revanced.manager.ui.viewmodel.PatcherViewModel import app.revanced.manager.util.APK_MIMETYPE @@ -69,22 +67,6 @@ fun PatcherScreen( } } - val patchesProgress by vm.patchesProgress.collectAsStateWithLifecycle() - - val progress by remember { - derivedStateOf { - val (patchesCompleted, patchesTotal) = patchesProgress - - val current = vm.steps.count { - it.state == State.COMPLETED && it.category != StepCategory.PATCHING - } + patchesCompleted - - val total = vm.steps.size - 1 + patchesTotal - - current.toFloat() / total.toFloat() - } - } - if (showInstallPicker) InstallPickerDialog( onDismiss = { showInstallPicker = false }, @@ -150,7 +132,7 @@ fun PatcherScreen( .fillMaxSize() ) { LinearProgressIndicator( - progress = { progress }, + progress = { vm.progress }, modifier = Modifier.fillMaxWidth() ) @@ -166,7 +148,8 @@ fun PatcherScreen( Steps( category = category, steps = steps, - stepCount = if (category == StepCategory.PATCHING) patchesProgress else null + stepCount = if (category == StepCategory.PATCHING) vm.patchesProgress else null, + stepProgressProvider = vm ) } } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt index 85cee8d1db..f1941394d8 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt @@ -9,6 +9,7 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.revanced.manager.R +import app.revanced.manager.data.platform.Filesystem import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.util.PM @@ -23,11 +24,10 @@ import java.nio.file.Files class AppSelectorViewModel( private val app: Application, private val pm: PM, + fs: Filesystem, private val patchBundleRepository: PatchBundleRepository ) : ViewModel() { - private val inputFile = File(app.filesDir, "input.apk").also { - it.delete() - } + private val inputFile = File(fs.uiTempDir, "input.apk").also(File::delete) val appList = pm.appList var onStorageClick: (SelectedApp.Local) -> Unit = {} diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt index e099591126..afd86052e4 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt @@ -9,14 +9,19 @@ import android.content.pm.PackageInstaller import android.net.Uri import android.util.Log import androidx.compose.runtime.Stable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.autoSaver import androidx.compose.runtime.setValue import androidx.compose.runtime.toMutableStateList import androidx.core.content.ContextCompat +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.map import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi +import androidx.lifecycle.viewmodel.compose.saveable import androidx.work.WorkInfo import androidx.work.WorkManager import app.revanced.manager.R @@ -31,11 +36,15 @@ import app.revanced.manager.patcher.logger.Logger import app.revanced.manager.patcher.worker.PatcherWorker import app.revanced.manager.service.InstallService import app.revanced.manager.ui.destination.Destination +import app.revanced.manager.ui.model.ProgressKey import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.State import app.revanced.manager.ui.model.Step import app.revanced.manager.ui.model.StepCategory +import app.revanced.manager.ui.model.StepProgressProvider import app.revanced.manager.util.PM +import app.revanced.manager.util.saveableVar +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 @@ -43,8 +52,6 @@ import app.revanced.manager.util.uiSafe import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.time.withTimeout import kotlinx.coroutines.withContext @@ -55,32 +62,47 @@ import java.nio.file.Files import java.time.Duration import java.util.UUID + +// @SuppressLint("AutoboxingStateCreation") @Stable +@OptIn(SavedStateHandleSaveableApi::class) class PatcherViewModel( private val input: Destination.Patcher -) : ViewModel(), KoinComponent { +) : ViewModel(), KoinComponent, StepProgressProvider { private val app: Application by inject() private val fs: Filesystem by inject() private val pm: PM by inject() private val workerRepository: WorkerRepository by inject() private val installedAppRepository: InstalledAppRepository by inject() private val rootInstaller: RootInstaller by inject() + private val savedStateHandle: SavedStateHandle by inject() private var installedApp: InstalledApp? = null - val packageName: String = input.selectedApp.packageName - var installedPackageName by mutableStateOf(null) + val packageName = input.selectedApp.packageName + + var installedPackageName by savedStateHandle.saveable( + key = "installedPackageName", + // Force Kotlin to select the correct overload. + stateSaver = autoSaver() + ) { + mutableStateOf(null) + } private set - var isInstalling by mutableStateOf(false) + private var ongoingPmSession: Boolean by savedStateHandle.saveableVar { false } + + var isInstalling by mutableStateOf(ongoingPmSession) private set - private val tempDir = fs.tempDir.resolve("installer").also { - it.deleteRecursively() - it.mkdirs() + private val tempDir = savedStateHandle.saveable(key = "tempDir") { + fs.uiTempDir.resolve("installer").also { + it.deleteRecursively() + it.mkdirs() + } } - private var inputFile: File? = null + private var inputFile: File? by savedStateHandle.saveableVar() private val outputFile = tempDir.resolve("output.apk") - private val logs = mutableListOf>() + private val logs by savedStateHandle.saveable>> { mutableListOf() } private val logger = object : Logger() { override fun log(level: LogLevel, message: String) { level.androidLog(message) @@ -92,18 +114,43 @@ class PatcherViewModel( } } - val patchesProgress = MutableStateFlow(Pair(0, input.selectedPatches.values.sumOf { it.size })) - private val downloadProgress = MutableStateFlow?>(null) - val steps = generateSteps( - app, - input.selectedApp, - downloadProgress - ).toMutableStateList() + private val patchCount = input.selectedPatches.values.sumOf { it.size } + private var completedPatchCount by savedStateHandle.saveable { + // SavedStateHandle.saveable only supports the boxed version. + @Suppress("AutoboxingStateCreation") mutableStateOf( + 0 + ) + } + val patchesProgress get() = completedPatchCount to patchCount + override var downloadProgress by savedStateHandle.saveable( + key = "downloadProgress", + stateSaver = autoSaver() + ) { + viewModelScope + mutableStateOf?>(null) + } + private set + val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) { + generateSteps( + app, + input.selectedApp + ).toMutableStateList() + } private var currentStepIndex = 0 + val progress by derivedStateOf { + val current = steps.count { + it.state == State.COMPLETED && it.category != StepCategory.PATCHING + } + completedPatchCount + + val total = steps.size - 1 + patchCount + + current.toFloat() / total.toFloat() + } + private val workManager = WorkManager.getInstance(app) - private val patcherWorkerId: UUID = + private val patcherWorkerId by savedStateHandle.saveable { workerRepository.launchExpedited( "patching", PatcherWorker.Args( input.selectedApp, @@ -111,9 +158,9 @@ class PatcherViewModel( input.selectedPatches, input.options, logger, - downloadProgress, - patchesProgress, - setInputFile = { inputFile = it }, + onDownloadProgress = { withContext(Dispatchers.Main) { downloadProgress = it } }, + onPatchCompleted = { withContext(Dispatchers.Main) { completedPatchCount += 1 } }, + setInputFile = { withContext(Dispatchers.Main) { inputFile = it } }, onProgress = { name, state, message -> viewModelScope.launch { steps[currentStepIndex] = steps[currentStepIndex].run { @@ -134,6 +181,7 @@ class PatcherViewModel( } ) ) + } val patcherSucceeded = workManager.getWorkInfoByIdLiveData(patcherWorkerId).map { workInfo: WorkInfo -> @@ -172,7 +220,8 @@ class PatcherViewModel( } } - init { // TODO: navigate away when system-initiated process death is detected because it is not possible to recover from it. + init { + // TODO: detect system-initiated process death during the patching process. ContextCompat.registerReceiver(app, installBroadcastReceiver, IntentFilter().apply { addAction(InstallService.APP_INSTALL_ACTION) }, ContextCompat.RECEIVER_NOT_EXPORTED) @@ -278,8 +327,8 @@ class PatcherViewModel( } } - companion object { - private const val TAG = "ReVanced Patcher" + private companion object { + const val TAG = "ReVanced Patcher" fun LogLevel.androidLog(msg: String) = when (this) { LogLevel.TRACE -> Log.v(TAG, msg) @@ -288,11 +337,7 @@ class PatcherViewModel( LogLevel.ERROR -> Log.e(TAG, msg) } - fun generateSteps( - context: Context, - selectedApp: SelectedApp, - downloadProgress: StateFlow?>? = null - ): List { + fun generateSteps(context: Context, selectedApp: SelectedApp): List { val needsDownload = selectedApp is SelectedApp.Download return listOfNotNull( @@ -300,7 +345,7 @@ class PatcherViewModel( context.getString(R.string.download_apk), StepCategory.PREPARING, state = State.RUNNING, - downloadProgress = downloadProgress, + progressKey = ProgressKey.DOWNLOAD, ).takeIf { needsDownload }, Step( context.getString(R.string.patcher_step_load_patches), diff --git a/app/src/main/java/app/revanced/manager/util/Util.kt b/app/src/main/java/app/revanced/manager/util/Util.kt index f1de38fd56..ad292cfe28 100644 --- a/app/src/main/java/app/revanced/manager/util/Util.kt +++ b/app/src/main/java/app/revanced/manager/util/Util.kt @@ -10,6 +10,7 @@ import android.icu.text.CompactDecimalFormat import android.os.Build import android.util.Log import android.widget.Toast +import androidx.annotation.MainThread import androidx.annotation.StringRes import androidx.compose.foundation.ScrollState import androidx.compose.foundation.lazy.LazyListState @@ -24,6 +25,7 @@ import androidx.compose.ui.graphics.Color import androidx.core.net.toUri import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import app.revanced.manager.R @@ -42,6 +44,9 @@ import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.time.format.DateTimeParseException import java.util.Locale +import kotlin.properties.PropertyDelegateProvider +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty typealias PatchSelection = Map> typealias Options = Map>> @@ -156,9 +161,21 @@ fun String.relativeTime(context: Context): String { return when { duration.toMinutes() < 1 -> context.getString(R.string.just_now) - duration.toMinutes() < 60 -> context.getString(R.string.minutes_ago, duration.toMinutes().toString()) - duration.toHours() < 24 -> context.getString(R.string.hours_ago, duration.toHours().toString()) - duration.toDays() < 30 -> context.getString(R.string.days_ago, duration.toDays().toString()) + duration.toMinutes() < 60 -> context.getString( + R.string.minutes_ago, + duration.toMinutes().toString() + ) + + duration.toHours() < 24 -> context.getString( + R.string.hours_ago, + duration.toHours().toString() + ) + + duration.toDays() < 30 -> context.getString( + R.string.days_ago, + duration.toDays().toString() + ) + else -> { val formatter = DateTimeFormatter.ofPattern("MMM d") val formattedDate = inputDateTime.format(formatter) @@ -218,4 +235,23 @@ fun ScrollState.isScrollingUp(): State { } val LazyListState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value -val ScrollState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value \ No newline at end of file +val ScrollState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value + +@MainThread +fun SavedStateHandle.saveableVar(init: () -> T): PropertyDelegateProvider> = + PropertyDelegateProvider { _: Any?, property -> + val name = property.name + if (name !in this) this[name] = init() + object : ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): T = get(name)!! + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) = + set(name, value) + } + } + +fun SavedStateHandle.saveableVar(): ReadWriteProperty = + object : ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): T? = get(property.name) + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) = + set(property.name, value) + } \ No newline at end of file From 00c61b6adc7d6c451c0edd944af2318e5440902d Mon Sep 17 00:00:00 2001 From: Ax333l Date: Sun, 29 Sep 2024 21:02:04 +0200 Subject: [PATCH 2/7] fix patcher screen Remaining WIP: update dashboard screen to feature a dialog --- .../ui/component/InstallerStatusDialog.kt | 30 +++---- .../manager/ui/model/InstallerModel.kt | 6 ++ .../revanced/manager/ui/model/PatcherStep.kt | 1 - .../manager/ui/screen/PatcherScreen.kt | 5 +- .../ui/viewmodel/DashboardViewModel.kt | 13 ++- .../manager/ui/viewmodel/PatcherViewModel.kt | 83 ++++++++++--------- .../main/java/app/revanced/manager/util/PM.kt | 2 + .../util/RequestInstallAppsContract.kt | 22 +++++ 8 files changed, 98 insertions(+), 64 deletions(-) create mode 100644 app/src/main/java/app/revanced/manager/ui/model/InstallerModel.kt create mode 100644 app/src/main/java/app/revanced/manager/util/RequestInstallAppsContract.kt diff --git a/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt index a31a813eab..2ae48ce6ff 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt @@ -6,7 +6,6 @@ import androidx.annotation.StringRes import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Check import androidx.compose.material.icons.outlined.ErrorOutline import androidx.compose.material3.AlertDialog import androidx.compose.material3.Icon @@ -21,35 +20,25 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import app.revanced.manager.R +import app.revanced.manager.ui.model.InstallerModel import com.github.materiiapps.enumutil.FromValue private typealias InstallerStatusDialogButtonHandler = ((model: InstallerModel) -> Unit) -private typealias InstallerStatusDialogButton = @Composable (model: InstallerStatusDialogModel) -> Unit - -interface InstallerModel { - fun reinstall() - fun install() -} - -interface InstallerStatusDialogModel : InstallerModel { - var packageInstallerStatus: Int? -} +private typealias InstallerStatusDialogButton = @Composable (model: InstallerModel, dismiss: () -> Unit) -> Unit @Composable -fun InstallerStatusDialog(model: InstallerStatusDialogModel) { +fun InstallerStatusDialog(installerStatus: Int, model: InstallerModel, onDismiss: () -> Unit) { val dialogKind = remember { - DialogKind.fromValue(model.packageInstallerStatus!!) ?: DialogKind.FAILURE + DialogKind.fromValue(installerStatus) ?: DialogKind.FAILURE } AlertDialog( - onDismissRequest = { - model.packageInstallerStatus = null - }, + onDismissRequest = onDismiss, confirmButton = { - dialogKind.confirmButton(model) + dialogKind.confirmButton(model, onDismiss) }, dismissButton = { - dialogKind.dismissButton?.invoke(model) + dialogKind.dismissButton?.invoke(model, onDismiss) }, icon = { Icon(dialogKind.icon, null) @@ -75,10 +64,10 @@ fun InstallerStatusDialog(model: InstallerStatusDialogModel) { private fun installerStatusDialogButton( @StringRes buttonStringResId: Int, buttonHandler: InstallerStatusDialogButtonHandler = { }, -): InstallerStatusDialogButton = { model -> +): InstallerStatusDialogButton = { model, dismiss -> TextButton( onClick = { - model.packageInstallerStatus = null + dismiss() buttonHandler(model) } ) { @@ -154,6 +143,7 @@ enum class DialogKind( model.install() }, ); + // Needed due to the @FromValue annotation. companion object } diff --git a/app/src/main/java/app/revanced/manager/ui/model/InstallerModel.kt b/app/src/main/java/app/revanced/manager/ui/model/InstallerModel.kt new file mode 100644 index 0000000000..410b64c1bd --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/model/InstallerModel.kt @@ -0,0 +1,6 @@ +package app.revanced.manager.ui.model + +interface InstallerModel { + fun reinstall() + fun install() +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt b/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt index e2f3ea2b49..e1662c14d5 100644 --- a/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt +++ b/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt @@ -3,7 +3,6 @@ package app.revanced.manager.ui.model import android.os.Parcelable import androidx.annotation.StringRes import app.revanced.manager.R -import kotlinx.coroutines.flow.StateFlow import kotlinx.parcelize.Parcelize enum class StepCategory(@StringRes val displayName: Int) { diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt index 3ae7a43748..04b0792cd3 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt @@ -74,8 +74,9 @@ fun PatcherScreen( onConfirm = vm::install ) - if (vm.installerStatusDialogModel.packageInstallerStatus != null) - InstallerStatusDialog(vm.installerStatusDialogModel) + vm.packageInstallerStatus?.let { + InstallerStatusDialog(it, vm, vm::dismissPackageInstallerDialog) + } AppScaffold( topBar = { diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt index ce68249d27..48a6b02f8e 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt @@ -3,6 +3,7 @@ package app.revanced.manager.ui.viewmodel import android.app.Application import android.content.ContentResolver import android.net.Uri +import android.os.Build import android.os.PowerManager import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf @@ -19,6 +20,7 @@ import app.revanced.manager.domain.bundles.RemotePatchBundle import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.network.api.ReVancedAPI +import app.revanced.manager.util.PM import app.revanced.manager.util.toast import app.revanced.manager.util.uiSafe import kotlinx.coroutines.flow.first @@ -30,7 +32,8 @@ class DashboardViewModel( private val patchBundleRepository: PatchBundleRepository, private val reVancedAPI: ReVancedAPI, private val networkInfo: NetworkInfo, - val prefs: PreferencesManager + val prefs: PreferencesManager, + private val pm: PM, ) : ViewModel() { val availablePatches = patchBundleRepository.bundles.map { it.values.sumOf { bundle -> bundle.patches.size } } @@ -44,6 +47,14 @@ class DashboardViewModel( var showBatteryOptimizationsWarning by mutableStateOf(false) private set + /** + * Android 11 kills the app process after granting the "install apps" permission, which is a problem for the patcher screen. + * This value is true when the conditions that trigger the bug are met. + * + * See: https://github.com/ReVanced/revanced-manager/issues/2138 + */ + val android11BugActive get() = Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !pm.canInstallPackages() + init { viewModelScope.launch { checkForManagerUpdates() diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt index a4c92c288d..5e0900e00a 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt @@ -7,6 +7,7 @@ import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageInstaller import android.net.Uri +import android.os.ParcelUuid import android.util.Log import androidx.compose.runtime.Stable import androidx.compose.runtime.derivedStateOf @@ -36,8 +37,8 @@ import app.revanced.manager.patcher.logger.Logger import app.revanced.manager.patcher.worker.PatcherWorker import app.revanced.manager.service.InstallService import app.revanced.manager.service.UninstallService -import app.revanced.manager.ui.component.InstallerStatusDialogModel import app.revanced.manager.ui.destination.Destination +import app.revanced.manager.ui.model.InstallerModel import app.revanced.manager.ui.model.ProgressKey import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.State @@ -62,15 +63,12 @@ import org.koin.core.component.inject import java.io.File import java.nio.file.Files import java.time.Duration -import java.util.UUID - -// @SuppressLint("AutoboxingStateCreation") @Stable @OptIn(SavedStateHandleSaveableApi::class) class PatcherViewModel( private val input: Destination.Patcher -) : ViewModel(), KoinComponent, StepProgressProvider { +) : ViewModel(), KoinComponent, StepProgressProvider, InstallerModel { private val app: Application by inject() private val fs: Filesystem by inject() private val pm: PM by inject() @@ -79,20 +77,6 @@ class PatcherViewModel( private val rootInstaller: RootInstaller by inject() private val savedStateHandle: SavedStateHandle by inject() - val installerStatusDialogModel : InstallerStatusDialogModel = object : InstallerStatusDialogModel { - override var packageInstallerStatus: Int? by mutableStateOf(null) - - override fun reinstall() { - this@PatcherViewModel.reinstall() - } - - override fun install() { - // Since this is a package installer status dialog, - // InstallType.ROOT is never used here. - install(InstallType.DEFAULT) - } - } - private var installedApp: InstalledApp? = null val packageName = input.selectedApp.packageName @@ -105,6 +89,13 @@ class PatcherViewModel( } private set private var ongoingPmSession: Boolean by savedStateHandle.saveableVar { false } + var packageInstallerStatus: Int? by savedStateHandle.saveable( + key = "packageInstallerStatus", + stateSaver = autoSaver() + ) { + mutableStateOf(null) + } + private set var isInstalling by mutableStateOf(ongoingPmSession) private set @@ -142,7 +133,6 @@ class PatcherViewModel( key = "downloadProgress", stateSaver = autoSaver() ) { - viewModelScope mutableStateOf?>(null) } private set @@ -166,15 +156,19 @@ class PatcherViewModel( private val workManager = WorkManager.getInstance(app) - private val patcherWorkerId by savedStateHandle.saveable { - workerRepository.launchExpedited( + private val patcherWorkerId by savedStateHandle.saveable { + ParcelUuid(workerRepository.launchExpedited( "patching", PatcherWorker.Args( input.selectedApp, outputFile.path, input.selectedPatches, input.options, logger, - onDownloadProgress = { withContext(Dispatchers.Main) { downloadProgress = it } }, + onDownloadProgress = { + withContext(Dispatchers.Main) { + downloadProgress = it + } + }, onPatchCompleted = { withContext(Dispatchers.Main) { completedPatchCount += 1 } }, setInputFile = { withContext(Dispatchers.Main) { inputFile = it } }, onProgress = { name, state, message -> @@ -196,11 +190,11 @@ class PatcherViewModel( } } ) - ) + )) } val patcherSucceeded = - workManager.getWorkInfoByIdLiveData(patcherWorkerId).map { workInfo: WorkInfo -> + workManager.getWorkInfoByIdLiveData(patcherWorkerId.uuid).map { workInfo: WorkInfo -> when (workInfo.state) { WorkInfo.State.SUCCEEDED -> true WorkInfo.State.FAILED -> false @@ -234,7 +228,7 @@ class PatcherViewModel( } } - installerStatusDialogModel.packageInstallerStatus = pmStatus + packageInstallerStatus = pmStatus isInstalling = false } @@ -249,7 +243,7 @@ class PatcherViewModel( ?.let(logger::trace) if (pmStatus != PackageInstaller.STATUS_SUCCESS) { - installerStatusDialogModel.packageInstallerStatus = pmStatus + packageInstallerStatus = pmStatus } } } @@ -277,7 +271,7 @@ class PatcherViewModel( override fun onCleared() { super.onCleared() app.unregisterReceiver(installerBroadcastReceiver) - workManager.cancelWorkById(patcherWorkerId) + workManager.cancelWorkById(patcherWorkerId.uuid) if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.ROOT) { GlobalScope.launch(Dispatchers.Main) { @@ -332,7 +326,7 @@ class PatcherViewModel( // 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 - installerStatusDialogModel.packageInstallerStatus = PackageInstaller.STATUS_FAILURE_CONFLICT + packageInstallerStatus = PackageInstaller.STATUS_FAILURE_CONFLICT return@launch } } @@ -357,8 +351,7 @@ class PatcherViewModel( // If the app is not installed, check if the output file is a base apk if (currentPackageInfo.splitNames != null) { // Exit if there is no base APK package - installerStatusDialogModel.packageInstallerStatus = - PackageInstaller.STATUS_FAILURE_INVALID + packageInstallerStatus = PackageInstaller.STATUS_FAILURE_INVALID return@launch } } @@ -400,25 +393,35 @@ class PatcherViewModel( } } } - } catch(e: Exception) { + } catch (e: Exception) { Log.e(tag, "Failed to install", e) app.toast(app.getString(R.string.install_app_fail, e.simpleMessage())) } finally { - if (!pmInstallStarted) - isInstalling = false + if (!pmInstallStarted) isInstalling = false } } - fun reinstall() = viewModelScope.launch { - uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") { - pm.getPackageInfo(outputFile)?.packageName?.let { pm.uninstallPackage(it) } - ?: throw Exception("Failed to load application info") + override fun install() { + // InstallType.ROOT is never used here since this overload is for the package installer status dialog. + install(InstallType.DEFAULT) + } - pm.installApp(listOf(outputFile)) - isInstalling = true + override fun reinstall() { + viewModelScope.launch { + uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") { + pm.getPackageInfo(outputFile)?.packageName?.let { pm.uninstallPackage(it) } + ?: throw Exception("Failed to load application info") + + pm.installApp(listOf(outputFile)) + isInstalling = true + } } } + fun dismissPackageInstallerDialog() { + packageInstallerStatus = null + } + private companion object { const val TAG = "ReVanced Patcher" diff --git a/app/src/main/java/app/revanced/manager/util/PM.kt b/app/src/main/java/app/revanced/manager/util/PM.kt index 0d7a822b96..1bd705d938 100644 --- a/app/src/main/java/app/revanced/manager/util/PM.kt +++ b/app/src/main/java/app/revanced/manager/util/PM.kt @@ -136,6 +136,8 @@ class PM( app.startActivity(it) } + fun canInstallPackages() = app.packageManager.canRequestPackageInstalls() + private fun PackageInstaller.Session.writeApk(apk: File) { apk.inputStream().use { inputStream -> openWrite(apk.name, 0, apk.length()).use { outputStream -> diff --git a/app/src/main/java/app/revanced/manager/util/RequestInstallAppsContract.kt b/app/src/main/java/app/revanced/manager/util/RequestInstallAppsContract.kt new file mode 100644 index 0000000000..bb435a82c8 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/util/RequestInstallAppsContract.kt @@ -0,0 +1,22 @@ +package app.revanced.manager.util + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.Settings +import androidx.activity.result.contract.ActivityResultContract +import androidx.annotation.RequiresApi +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +class RequestInstallAppsContract : ActivityResultContract(), KoinComponent { + private val pm: PM by inject() + override fun createIntent(context: Context, input: String) = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.fromParts("package", input, null)) + + override fun parseResult(resultCode: Int, intent: Intent?): Boolean { + println("Finished") + return pm.canInstallPackages() + } +} \ No newline at end of file From 536a24169c8f1603ea2dac0a82f231500610d77d Mon Sep 17 00:00:00 2001 From: Ax333l Date: Fri, 20 Dec 2024 17:51:44 +0100 Subject: [PATCH 3/7] fix merge issues --- .../revanced/manager/patcher/worker/PatcherWorker.kt | 6 ------ .../revanced/manager/ui/component/patcher/Steps.kt | 12 ++++-------- .../app/revanced/manager/ui/model/PatcherStep.kt | 2 +- .../manager/ui/viewmodel/PatcherViewModel.kt | 2 +- 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt index 7989b3a42f..0e63fbbd7c 100644 --- a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt +++ b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt @@ -223,12 +223,6 @@ class PatcherWorker( args.selectedPatches, args.options, args.logger, - /* - onPatchCompleted = { - args.patchesProgress.update { (completed, total) -> - completed + 1 to total - } - },*/ args.onPatchCompleted, args.onProgress ) diff --git a/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt b/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt index 78d8309ca3..d6c78263c2 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt @@ -120,7 +120,10 @@ fun Steps( steps.forEach { step -> val (progress, progressText) = when (step.progressKey) { null -> null - ProgressKey.DOWNLOAD -> stepProgressProvider.downloadProgress?.let { (downloaded, total) -> downloaded / total to "$downloaded/$total MB" } + ProgressKey.DOWNLOAD -> stepProgressProvider.downloadProgress?.let { (downloaded, total) -> + if (total != null) downloaded.toFloat() / total.toFloat() to "${downloaded.megaBytes}/${total.megaBytes} MB" + else null to "${downloaded.megaBytes} MB" + } } ?: (null to null) SubStep( @@ -239,13 +242,6 @@ fun StepIcon(state: State, progress: Float? = null, size: Dp) { contentDescription = description } }, - /* - progress = { - progress?.let { (current, total) -> - if (total == null) return@let null - current / total - }?.toFloat() - },*/ progress = { progress }, strokeWidth = strokeWidth ) diff --git a/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt b/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt index e1662c14d5..3dbb390e1d 100644 --- a/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt +++ b/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt @@ -20,7 +20,7 @@ enum class ProgressKey { } interface StepProgressProvider { - val downloadProgress: Pair? + val downloadProgress: Pair? } @Parcelize diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt index 03a3d985bd..d67f7c9a27 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt @@ -147,7 +147,7 @@ class PatcherViewModel( key = "downloadProgress", stateSaver = autoSaver() ) { - mutableStateOf?>(null) + mutableStateOf?>(null) } private set val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) { From be4066bbf6ad79dba823a6863c14ece5e5fee7da Mon Sep 17 00:00:00 2001 From: Ax333l Date: Fri, 20 Dec 2024 21:16:08 +0100 Subject: [PATCH 4/7] actually fix the bug --- .../manager/ui/screen/DashboardScreen.kt | 50 +++++++++++++++++-- .../ui/viewmodel/DashboardViewModel.kt | 10 ++-- .../util/RequestInstallAppsContract.kt | 5 +- app/src/main/res/values/strings.xml | 3 ++ 4 files changed, 56 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt index 6d3168999c..55fc771bc7 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt @@ -5,6 +5,7 @@ import android.content.Intent import android.net.Uri import android.provider.Settings import androidx.activity.compose.BackHandler +import androidx.activity.compose.rememberLauncherForActivityResult import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.pager.HorizontalPager @@ -27,6 +28,7 @@ import app.revanced.manager.R import app.revanced.manager.data.room.apps.installed.InstalledApp import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault import app.revanced.manager.patcher.aapt.Aapt +import app.revanced.manager.ui.component.AlertDialogExtended import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AutoUpdatesDialog import app.revanced.manager.ui.component.AvailableUpdateDialog @@ -36,6 +38,7 @@ import app.revanced.manager.ui.component.haptics.HapticFloatingActionButton import app.revanced.manager.ui.component.haptics.HapticTab import app.revanced.manager.ui.component.bundle.ImportPatchBundleDialog import app.revanced.manager.ui.viewmodel.DashboardViewModel +import app.revanced.manager.util.RequestInstallAppsContract import app.revanced.manager.util.toast import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel @@ -93,20 +96,36 @@ fun DashboardScreen( ) } - var showDialog by rememberSaveable { mutableStateOf(vm.prefs.showManagerUpdateDialogOnLaunch.getBlocking()) } + var showUpdateDialog by rememberSaveable { mutableStateOf(vm.prefs.showManagerUpdateDialogOnLaunch.getBlocking()) } val availableUpdate by remember { - derivedStateOf { vm.updatedManagerVersion.takeIf { showDialog } } + derivedStateOf { vm.updatedManagerVersion.takeIf { showUpdateDialog } } } availableUpdate?.let { version -> AvailableUpdateDialog( - onDismiss = { showDialog = false }, + onDismiss = { showUpdateDialog = false }, setShowManagerUpdateDialogOnLaunch = vm::setShowManagerUpdateDialogOnLaunch, onConfirm = onUpdateClick, newVersion = version ) } + val context = LocalContext.current + var showAndroid11Dialog by rememberSaveable { mutableStateOf(false) } + val installAppsPermissionLauncher = + rememberLauncherForActivityResult(RequestInstallAppsContract) { granted -> + showAndroid11Dialog = false + if (granted) onAppSelectorClick() + } + if (showAndroid11Dialog) Android11Dialog( + onDismissRequest = { + showAndroid11Dialog = false + }, + onContinue = { + installAppsPermissionLauncher.launch(context.packageName) + } + ) + Scaffold( topBar = { if (bundlesSelectable) { @@ -188,6 +207,10 @@ fun DashboardScreen( } return@HapticFloatingActionButton } + if (vm.android11BugActive) { + showAndroid11Dialog = true + return@HapticFloatingActionButton + } onAppSelectorClick() } @@ -316,4 +339,25 @@ fun Notifications( } } } +} + +@Composable +fun Android11Dialog(onDismissRequest: () -> Unit, onContinue: () -> Unit) { + AlertDialogExtended( + onDismissRequest = onDismissRequest, + confirmButton = { + TextButton(onClick = onContinue) { + Text(stringResource(R.string.continue_)) + } + }, + title = { + Text(stringResource(R.string.android_11_bug_dialog_title)) + }, + icon = { + Icon(Icons.Outlined.BugReport, null) + }, + text = { + Text(stringResource(R.string.android_11_bug_dialog_description)) + } + ) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt index f48c91723c..303bd06a04 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt @@ -46,11 +46,6 @@ class DashboardViewModel( val newDownloaderPluginsAvailable = downloaderPluginRepository.newPluginPackageNames.map { it.isNotEmpty() } - var updatedManagerVersion: String? by mutableStateOf(null) - private set - var showBatteryOptimizationsWarning by mutableStateOf(false) - private set - /** * Android 11 kills the app process after granting the "install apps" permission, which is a problem for the patcher screen. * This value is true when the conditions that trigger the bug are met. @@ -59,6 +54,11 @@ class DashboardViewModel( */ val android11BugActive get() = Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !pm.canInstallPackages() + var updatedManagerVersion: String? by mutableStateOf(null) + private set + var showBatteryOptimizationsWarning by mutableStateOf(false) + private set + init { viewModelScope.launch { checkForManagerUpdates() diff --git a/app/src/main/java/app/revanced/manager/util/RequestInstallAppsContract.kt b/app/src/main/java/app/revanced/manager/util/RequestInstallAppsContract.kt index bb435a82c8..e745f341b6 100644 --- a/app/src/main/java/app/revanced/manager/util/RequestInstallAppsContract.kt +++ b/app/src/main/java/app/revanced/manager/util/RequestInstallAppsContract.kt @@ -3,15 +3,12 @@ package app.revanced.manager.util import android.content.Context import android.content.Intent import android.net.Uri -import android.os.Build -import android.os.Environment import android.provider.Settings import androidx.activity.result.contract.ActivityResultContract -import androidx.annotation.RequiresApi import org.koin.core.component.KoinComponent import org.koin.core.component.inject -class RequestInstallAppsContract : ActivityResultContract(), KoinComponent { +object RequestInstallAppsContract : ActivityResultContract(), KoinComponent { private val pm: PM by inject() override fun createIntent(context: Context, input: String) = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.fromParts("package", input, null)) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b9ba4305c0..f88f1b1cf6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -35,6 +35,9 @@ Default Unnamed + Android 11 bug + The app installation permission must be granted ahead of time to avoid a bug in the Android 11 system that will negatively affect the user experience. + Any available version Select source Auto From 1982ac27e2c398a690a580710f50d1ba9d1a02e4 Mon Sep 17 00:00:00 2001 From: Ax333l Date: Fri, 20 Dec 2024 21:20:17 +0100 Subject: [PATCH 5/7] remove println lol --- .../java/app/revanced/manager/util/RequestInstallAppsContract.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/app/revanced/manager/util/RequestInstallAppsContract.kt b/app/src/main/java/app/revanced/manager/util/RequestInstallAppsContract.kt index e745f341b6..b5060999a1 100644 --- a/app/src/main/java/app/revanced/manager/util/RequestInstallAppsContract.kt +++ b/app/src/main/java/app/revanced/manager/util/RequestInstallAppsContract.kt @@ -13,7 +13,6 @@ object RequestInstallAppsContract : ActivityResultContract(), K override fun createIntent(context: Context, input: String) = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.fromParts("package", input, null)) override fun parseResult(resultCode: Int, intent: Intent?): Boolean { - println("Finished") return pm.canInstallPackages() } } \ No newline at end of file From 1231bc106c7dc22dfd5b7711a5d972542378666e Mon Sep 17 00:00:00 2001 From: Ax333l Date: Sat, 21 Dec 2024 20:56:12 +0100 Subject: [PATCH 6/7] hh --- .../revanced/manager/data/platform/Filesystem.kt | 4 +--- .../revanced/manager/ui/screen/PatcherScreen.kt | 8 ++++++-- .../manager/ui/viewmodel/AppSelectorViewModel.kt | 14 ++++++++++++-- .../manager/ui/viewmodel/PatcherViewModel.kt | 8 +++++--- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/data/platform/Filesystem.kt b/app/src/main/java/app/revanced/manager/data/platform/Filesystem.kt index 97087f3e52..7bad2debc3 100644 --- a/app/src/main/java/app/revanced/manager/data/platform/Filesystem.kt +++ b/app/src/main/java/app/revanced/manager/data/platform/Filesystem.kt @@ -29,9 +29,7 @@ class Filesystem(private val app: Application) { * This is the same as [tempDir], but does not get cleared on system-initiated process death. * Paths to this directory can be safely stored in parcels. */ - val uiTempDir: File = app.getDir("ui_ephemeral", Context.MODE_PRIVATE).apply { - mkdirs() - } + val uiTempDir: File = app.getDir("ui_ephemeral", Context.MODE_PRIVATE) fun externalFilesDir(): Path = Environment.getExternalStorageDirectory().toPath() diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt index 6037218a35..e193d6fa85 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt @@ -45,7 +45,11 @@ fun PatcherScreen( onBackClick: () -> Unit, vm: PatcherViewModel ) { - BackHandler(onBack = onBackClick) + fun leaveScreen() { + vm.onBack() + onBackClick() + } + BackHandler(onBack = ::leaveScreen) val context = LocalContext.current val exportApkLauncher = @@ -107,7 +111,7 @@ fun PatcherScreen( topBar = { AppTopBar( title = stringResource(R.string.patcher), - onBackClick = onBackClick + onBackClick = ::leaveScreen ) }, bottomBar = { diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt index dab47e0a65..538b85e33a 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt @@ -6,8 +6,11 @@ import android.net.Uri import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi +import androidx.lifecycle.viewmodel.compose.saveable import app.revanced.manager.R import app.revanced.manager.data.platform.Filesystem import app.revanced.manager.domain.repository.PatchBundleRepository @@ -23,13 +26,20 @@ import kotlinx.coroutines.withContext import java.io.File import java.nio.file.Files +@OptIn(SavedStateHandleSaveableApi::class) class AppSelectorViewModel( private val app: Application, private val pm: PM, fs: Filesystem, - private val patchBundleRepository: PatchBundleRepository + private val patchBundleRepository: PatchBundleRepository, + savedStateHandle: SavedStateHandle, ) : ViewModel() { - private val inputFile = File(fs.uiTempDir, "input.apk").also(File::delete) + private val inputFile = savedStateHandle.saveable(key = "inputFile") { + File( + fs.uiTempDir, + "input.apk" + ).also(File::delete) + } val appList = pm.appList private val storageSelectionChannel = Channel() diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt index d67f7c9a27..6b6db230cb 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt @@ -10,7 +10,6 @@ import android.net.Uri import android.os.ParcelUuid import android.util.Log import androidx.activity.result.ActivityResult -import androidx.compose.runtime.Stable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -65,12 +64,12 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.time.withTimeout import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent +import org.koin.core.component.get import org.koin.core.component.inject import java.io.File import java.nio.file.Files import java.time.Duration -@Stable @OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class) class PatcherViewModel( private val input: Destination.Patcher @@ -81,7 +80,7 @@ class PatcherViewModel( private val workerRepository: WorkerRepository by inject() private val installedAppRepository: InstalledAppRepository by inject() private val rootInstaller: RootInstaller by inject() - private val savedStateHandle: SavedStateHandle by inject() + private val savedStateHandle: SavedStateHandle = get() private var installedApp: InstalledApp? = null val packageName = input.selectedApp.packageName @@ -325,7 +324,10 @@ class PatcherViewModel( } } } + } + fun onBack() { + // tempDir cannot be deleted inside onCleared because it gets called on system-initiated process death. tempDir.deleteRecursively() } From c4ef7d3b07f5c38e0b428cb06088324313bb2a4b Mon Sep 17 00:00:00 2001 From: Ax333l Date: Sun, 22 Dec 2024 22:18:12 +0100 Subject: [PATCH 7/7] fix dialog issue --- .../revanced/manager/ui/viewmodel/PatcherViewModel.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt index 6b6db230cb..c8577295c9 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt @@ -105,7 +105,9 @@ class PatcherViewModel( var isInstalling by mutableStateOf(ongoingPmSession) private set - private var currentActivityRequest: Pair, String>? by mutableStateOf(null) + private var currentActivityRequest: Pair, String>? by mutableStateOf( + null + ) val activityPromptDialog by derivedStateOf { currentActivityRequest?.second } private var launchedActivity: CompletableDeferred? = null @@ -268,9 +270,7 @@ class PatcherViewModel( input.selectedPatches ) } - } - - packageInstallerStatus = pmStatus + } else packageInstallerStatus = pmStatus isInstalling = false } @@ -284,9 +284,8 @@ class PatcherViewModel( intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE) ?.let(logger::trace) - if (pmStatus != PackageInstaller.STATUS_SUCCESS) { + if (pmStatus != PackageInstaller.STATUS_SUCCESS) packageInstallerStatus = pmStatus - } } } }