diff --git a/app/src/main/java/app/revanced/manager/MainActivity.kt b/app/src/main/java/app/revanced/manager/MainActivity.kt index 02ac06b778..6e5dafc494 100644 --- a/app/src/main/java/app/revanced/manager/MainActivity.kt +++ b/app/src/main/java/app/revanced/manager/MainActivity.kt @@ -299,9 +299,12 @@ private fun ReVancedManager(vm: MainViewModel) { composable { val data = it.getComplexArg() + val parentBackStackEntry = navController.navGraphEntry(it) + val parentData = + parentBackStackEntry.getComplexArg() val selectedAppInfoVm = koinViewModel( - viewModelStoreOwner = navController.navGraphEntry(it) - ) + viewModelStoreOwner = parentBackStackEntry + ) { parametersOf(parentData) } PatchesSelectorScreen( onBackClick = navController::popBackStackSafe, @@ -319,9 +322,12 @@ private fun ReVancedManager(vm: MainViewModel) { composable { val data = it.getComplexArg() + val parentBackStackEntry = navController.navGraphEntry(it) + val parentData = + parentBackStackEntry.getComplexArg() val selectedAppInfoVm = koinViewModel( - viewModelStoreOwner = navController.navGraphEntry(it) - ) + viewModelStoreOwner = parentBackStackEntry + ) { parametersOf(parentData) } RequiredOptionsScreen( onBackClick = navController::popBackStackSafe, diff --git a/app/src/main/java/app/revanced/manager/data/room/AppDatabase.kt b/app/src/main/java/app/revanced/manager/data/room/AppDatabase.kt index e69d02b55f..ca5140690e 100644 --- a/app/src/main/java/app/revanced/manager/data/room/AppDatabase.kt +++ b/app/src/main/java/app/revanced/manager/data/room/AppDatabase.kt @@ -22,7 +22,7 @@ import app.revanced.manager.data.room.downloader.DownloaderEntity import app.revanced.manager.data.room.options.Option import app.revanced.manager.data.room.options.OptionDao import app.revanced.manager.data.room.options.OptionGroup -import kotlin.random.Random +import java.util.UUID @Database( entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class, InstalledPatchBundle::class, OptionGroup::class, Option::class, DownloaderEntity::class], @@ -51,6 +51,6 @@ abstract class AppDatabase : RoomDatabase() { class DeleteTrustedDownloaders : AutoMigrationSpec companion object { - fun generateUid() = Random.Default.nextInt() + fun generateUid() = UUID.randomUUID().mostSignificantBits.toInt() } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/room/options/OptionDao.kt b/app/src/main/java/app/revanced/manager/data/room/options/OptionDao.kt index 66c69b43c1..dc8c62cf27 100644 --- a/app/src/main/java/app/revanced/manager/data/room/options/OptionDao.kt +++ b/app/src/main/java/app/revanced/manager/data/room/options/OptionDao.kt @@ -3,8 +3,10 @@ package app.revanced.manager.data.room.options import androidx.room.Dao import androidx.room.Insert import androidx.room.MapColumn +import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Transaction +import app.revanced.manager.data.room.AppDatabase import kotlinx.coroutines.flow.Flow @Dao @@ -23,8 +25,8 @@ abstract class OptionDao { @Query("SELECT package_name FROM option_groups") abstract fun getPackagesWithOptions(): Flow> - @Insert - abstract suspend fun createOptionGroup(group: OptionGroup) + @Insert(onConflict = OnConflictStrategy.IGNORE) + protected abstract suspend fun createOptionGroupIfMissing(group: OptionGroup) @Query("DELETE FROM option_groups WHERE patch_bundle = :uid") abstract suspend fun resetOptionsForPatchBundle(uid: Int) @@ -47,4 +49,18 @@ abstract class OptionDao { clearGroup(groupId) insertOptions(options) } + + @Transaction + open suspend fun getOrCreateGroupId(bundleUid: Int, packageName: String): Int { + getGroupId(bundleUid, packageName)?.let { return it } + createOptionGroupIfMissing( + OptionGroup( + uid = AppDatabase.generateUid(), + patchBundle = bundleUid, + packageName = packageName + ) + ) + return getGroupId(bundleUid, packageName) + ?: throw IllegalStateException("Failed to create options group for $packageName") + } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/room/selection/SelectionDao.kt b/app/src/main/java/app/revanced/manager/data/room/selection/SelectionDao.kt index 5c32b8fe80..796e65a565 100644 --- a/app/src/main/java/app/revanced/manager/data/room/selection/SelectionDao.kt +++ b/app/src/main/java/app/revanced/manager/data/room/selection/SelectionDao.kt @@ -3,8 +3,10 @@ package app.revanced.manager.data.room.selection import androidx.room.Dao import androidx.room.Insert import androidx.room.MapColumn +import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Transaction +import app.revanced.manager.data.room.AppDatabase import kotlinx.coroutines.flow.Flow @Dao @@ -32,8 +34,8 @@ abstract class SelectionDao { @Query("SELECT uid FROM patch_selections WHERE patch_bundle = :bundleUid AND package_name = :packageName") abstract suspend fun getSelectionId(bundleUid: Int, packageName: String): Int? - @Insert - abstract suspend fun createSelection(selection: PatchSelection) + @Insert(onConflict = OnConflictStrategy.IGNORE) + protected abstract suspend fun createSelectionIfMissing(selection: PatchSelection) @Query("SELECT package_name FROM patch_selections") abstract fun getPackagesWithSelection(): Flow> @@ -65,4 +67,28 @@ abstract class SelectionDao { clearSelection(selectionUid) selectPatches(patches.map { SelectedPatch(selectionUid, it) }) } + + @Transaction + open suspend fun getOrCreateSelectionId(bundleUid: Int, packageName: String): Int { + getSelectionId(bundleUid, packageName)?.let { return it } + createSelectionIfMissing( + PatchSelection( + uid = AppDatabase.generateUid(), + patchBundle = bundleUid, + packageName = packageName + ) + ) + return getSelectionId(bundleUid, packageName) + ?: throw IllegalStateException("Failed to create selection for $packageName") + } + + @Transaction + open suspend fun replaceForPatchBundle(bundleUid: Int, selections: Map>) { + resetForPatchBundle(bundleUid) + selections.forEach { (packageName, patches) -> + val selectionUid = getOrCreateSelectionId(bundleUid, packageName) + clearSelection(selectionUid) + selectPatches(patches.map { SelectedPatch(selectionUid, it) }) + } + } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt index b7fbf19c7a..383e07737f 100644 --- a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt +++ b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt @@ -5,6 +5,7 @@ import android.content.ComponentName import android.content.Intent import android.content.ServiceConnection import android.os.IBinder +import android.os.Process import app.revanced.manager.IRootSystemService import app.revanced.manager.service.ManagerRootService import app.revanced.manager.util.PM @@ -71,7 +72,8 @@ class RootInstaller( suspend fun isAppMounted(packageName: String) = withContext(Dispatchers.IO) { pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir?.let { - execute("mount | grep \"$it\"").isSuccess + // Use -F to ensure the path isn't treated like a regex + execute("mount | grep -F \"$it\"").isSuccess } ?: false } @@ -112,10 +114,11 @@ class RootInstaller( unmount(packageName) stockAPK?.let { stockApp -> - // TODO: get user id programmatically - execute("pm uninstall -k --user 0 $packageName") + // Android assigns 100000 UIDs per user (https://android.googlesource.com/platform/frameworks/base/+/HEAD/core/java/android/os/UserHandle.java#47) + val userId = Process.myUid() / 100000 + execute("pm uninstall -k --user ${userId} \"$packageName\"") - execute("pm install -r -d --user 0 \"${stockApp.absolutePath}\"") + execute("pm install -r -d --user ${userId} \"${stockApp.absolutePath}\"") .assertSuccess("Failed to install stock app") stockApp.delete() diff --git a/app/src/main/java/app/revanced/manager/domain/manager/KeystoreManager.kt b/app/src/main/java/app/revanced/manager/domain/manager/KeystoreManager.kt index b8b92fbbce..51aff92d65 100644 --- a/app/src/main/java/app/revanced/manager/domain/manager/KeystoreManager.kt +++ b/app/src/main/java/app/revanced/manager/domain/manager/KeystoreManager.kt @@ -21,7 +21,7 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) { * Default alias and password for the keystore. */ const val DEFAULT = "ReVanced" - private val eightYearsFromNow get() = Date(System.currentTimeMillis() + (365.days * 8).inWholeMilliseconds * 24) + private val eightYearsFromNow get() = Date(System.currentTimeMillis() + (365.days * 8).inWholeMilliseconds) } private val keystorePath = diff --git a/app/src/main/java/app/revanced/manager/domain/manager/base/BasePreferencesManager.kt b/app/src/main/java/app/revanced/manager/domain/manager/base/BasePreferencesManager.kt index c3de2c1bca..09b34a17a9 100644 --- a/app/src/main/java/app/revanced/manager/domain/manager/base/BasePreferencesManager.kt +++ b/app/src/main/java/app/revanced/manager/domain/manager/base/BasePreferencesManager.kt @@ -139,7 +139,10 @@ class LongSetPreference( ) : Preference>(dataStore, default) { private val key = stringSetPreferencesKey(key) - override fun Preferences.read() = this[key]?.mapTo(mutableSetOf()) { it.toLong() } ?: default + override fun Preferences.read() = this[key]?.mapNotNullTo(mutableSetOf()) { + it.toLongOrNull() + } ?: default + override fun MutablePreferences.write(value: Set) { this[key] = value.mapTo(mutableSetOf()) { it.toString() } } diff --git a/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt index c40d160535..3a7dc1dc8b 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt @@ -28,6 +28,10 @@ class DownloadedAppRepository( db: AppDatabase, private val pm: PM ) { + companion object { + private const val CACHE_TTL_MS = 6 * 60 * 60 * 1_000L + } + private val dir = app.getDir("downloaded-apps", Context.MODE_PRIVATE) private val dao = db.downloadedAppDao() @@ -39,11 +43,10 @@ class DownloadedAppRepository( private fun getApkFileForDir(directory: File) = directory.listFiles()!!.first() suspend fun cleanUp() { - val threshold = 1000 * 60 * 60 * 6 val now = System.currentTimeMillis() val targets = getAll().first().filter { - (now - it.lastUsed) > threshold + (now - it.lastUsed) > CACHE_TTL_MS } delete(targets) } @@ -98,9 +101,7 @@ class DownloadedAppRepository( override fun write(b: ByteArray?, off: Int, len: Int) = out.write(b, off, len).also { - emitProgress( - (len - off).toLong() - ) + emitProgress(len.toLong()) } } downloader.impl.download(scope, data, stream) @@ -114,15 +115,16 @@ class DownloadedAppRepository( val pkgInfo = pm.getPackageInfo(targetFile.toFile()) ?: error("Downloaded APK file is invalid") if (pkgInfo.packageName != expectedPackageName) error("Downloaded APK has the wrong package name. Expected: $expectedPackageName, Actual: ${pkgInfo.packageName}") + val versionName = pkgInfo.versionName ?: error("Downloaded APK has no version name") expectedVersion?.let { if ( - pkgInfo.versionName != expectedVersion && + versionName != expectedVersion && (appCompatibilityCheck || patchesCompatibilityCheck) - ) error("The selected app version ($pkgInfo.versionName) doesn't match the suggested version. Please use the suggested version ($expectedVersion), or adjust your settings by disabling \"Require suggested app version\" and enabling \"Disable version compatibility check\".") + ) error("The selected app version ($versionName) doesn't match the suggested version. Please use the suggested version ($expectedVersion), or adjust your settings by disabling \"Require suggested app version\" and enabling \"Disable version compatibility check\".") } // Delete the previous copy (if present). - dao.get(pkgInfo.packageName, pkgInfo.versionName!!)?.directory?.let { + dao.get(pkgInfo.packageName, versionName)?.directory?.let { if (!dir.resolve(it) .deleteRecursively() ) throw Exception("Failed to delete existing directory") @@ -130,7 +132,7 @@ class DownloadedAppRepository( dao.upsert( DownloadedApp( packageName = pkgInfo.packageName, - version = pkgInfo.versionName!!, + version = versionName, directory = relativePath, ) ) diff --git a/app/src/main/java/app/revanced/manager/domain/repository/PatchOptionsRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/PatchOptionsRepository.kt index 0e55b2d8d5..cad6d776b2 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/PatchOptionsRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/PatchOptionsRepository.kt @@ -3,7 +3,6 @@ package app.revanced.manager.domain.repository import android.util.Log import app.revanced.manager.data.room.AppDatabase import app.revanced.manager.data.room.options.Option -import app.revanced.manager.data.room.options.OptionGroup import app.revanced.manager.patcher.patch.PatchInfo import app.revanced.manager.util.Options import app.revanced.manager.util.tag @@ -13,13 +12,6 @@ import kotlinx.coroutines.flow.map class PatchOptionsRepository(db: AppDatabase) { private val dao = db.optionDao() - private suspend fun getOrCreateGroup(bundleUid: Int, packageName: String) = - dao.getGroupId(bundleUid, packageName) ?: OptionGroup( - uid = AppDatabase.generateUid(), - patchBundle = bundleUid, - packageName = packageName - ).also { dao.createOptionGroup(it) }.uid - suspend fun getOptions( packageName: String, bundlePatches: Map> @@ -57,7 +49,7 @@ class PatchOptionsRepository(db: AppDatabase) { suspend fun saveOptions(packageName: String, options: Options) = dao.updateOptions(options.entries.associate { (sourceUid, bundlePatchOptions) -> - val groupId = getOrCreateGroup(sourceUid, packageName) + val groupId = dao.getOrCreateGroupId(sourceUid, packageName) groupId to bundlePatchOptions.flatMap { (patchName, patchOptions) -> patchOptions.mapNotNull { (key, value) -> diff --git a/app/src/main/java/app/revanced/manager/domain/repository/PatchSelectionRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/PatchSelectionRepository.kt index e9c448d0e8..b67e69a352 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/PatchSelectionRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/PatchSelectionRepository.kt @@ -1,27 +1,18 @@ package app.revanced.manager.domain.repository import app.revanced.manager.data.room.AppDatabase -import app.revanced.manager.data.room.AppDatabase.Companion.generateUid -import app.revanced.manager.data.room.selection.PatchSelection import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map class PatchSelectionRepository(db: AppDatabase) { private val dao = db.selectionDao() - private suspend fun getOrCreateSelection(bundleUid: Int, packageName: String) = - dao.getSelectionId(bundleUid, packageName) ?: PatchSelection( - uid = generateUid(), - patchBundle = bundleUid, - packageName = packageName - ).also { dao.createSelection(it) }.uid - suspend fun getSelection(packageName: String): Map> = dao.getSelectedPatches(packageName).mapValues { it.value.toSet() } suspend fun updateSelection(packageName: String, selection: Map>) = dao.updateSelections(selection.mapKeys { (sourceUid, _) -> - getOrCreateSelection( + dao.getOrCreateSelectionId( sourceUid, packageName ) @@ -47,10 +38,10 @@ class PatchSelectionRepository(db: AppDatabase) { suspend fun export(bundleUid: Int): SerializedSelection = dao.exportSelection(bundleUid) suspend fun import(bundleUid: Int, selection: SerializedSelection) { - dao.resetForPatchBundle(bundleUid) - dao.updateSelections(selection.entries.associate { (packageName, patches) -> - getOrCreateSelection(bundleUid, packageName) to patches.toSet() - }) + dao.replaceForPatchBundle( + bundleUid, + selection.mapValues { (_, patches) -> patches.toSet() } + ) } } diff --git a/app/src/main/java/app/revanced/manager/domain/worker/WorkerRepository.kt b/app/src/main/java/app/revanced/manager/domain/worker/WorkerRepository.kt index 222a31c48b..b90e4729f0 100644 --- a/app/src/main/java/app/revanced/manager/domain/worker/WorkerRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/worker/WorkerRepository.kt @@ -6,6 +6,7 @@ import androidx.work.OneTimeWorkRequest import androidx.work.OutOfQuotaPolicy import androidx.work.WorkManager import java.util.UUID +import java.util.concurrent.ConcurrentHashMap class WorkerRepository(app: Application) { val workManager = WorkManager.getInstance(app) @@ -14,7 +15,7 @@ class WorkerRepository(app: Application) { * The standard WorkManager communication APIs use [androidx.work.Data], which has too many limitations. * We can get around those limits by passing inputs using global variables instead. */ - val workerInputs = mutableMapOf() + val workerInputs = ConcurrentHashMap() @Suppress("UNCHECKED_CAST") fun > claimInput(worker: W): A { @@ -30,7 +31,7 @@ class WorkerRepository(app: Application) { .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) .build() workerInputs[request.id] = input - workManager.enqueueUniqueWork(name, ExistingWorkPolicy.REPLACE, request) + workManager.enqueueUniqueWork(name, ExistingWorkPolicy.KEEP, request) return request.id } } \ 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 99f73f31ee..301a89f180 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 @@ -1,5 +1,6 @@ package app.revanced.manager.patcher.worker +import android.annotation.SuppressLint import android.app.Activity import android.app.Notification import android.app.NotificationChannel @@ -48,10 +49,12 @@ import app.revanced.manager.util.tag import com.topjohnwu.superuser.Shell import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent import org.koin.core.component.inject import java.io.File +import java.io.IOException @OptIn(DownloaderHostApi::class) class PatcherWorker( @@ -120,14 +123,19 @@ class PatcherWorker( // This does not always show up for some reason. setForeground(getForegroundInfo()) } catch (e: Exception) { - Log.d(tag, "Failed to set foreground info:", e) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + throw e + } + Log.w(tag, "Failed to set foreground info", e) } - + @SuppressLint("WakelockTimeout") val wakeLock: PowerManager.WakeLock = (applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager) .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::Patcher") .apply { - acquire(10 * 60 * 1000L) + // acquire without a timeout as we're managing the wakelock ourselves + // in the finally-block, and therefore, it will be released regardless (of failure/success eventually). + acquire() Log.d(tag, "Acquired wakelock.") } @@ -142,6 +150,7 @@ class PatcherWorker( private suspend fun runPatcher(args: Args): Result { val patchedApk = fs.tempDir.resolve("patched.apk") + var success = false return try { if (args.input is SelectedApp.Installed) { @@ -184,7 +193,9 @@ class PatcherWorker( is SelectedApp.Search -> { runStep(StepId.DownloadAPK, args.onEvent) { - downloaderRepository.loadedDownloadersFlow.first() + withTimeout(30_000L) { + downloaderRepository.loadedDownloadersFlow.first() + } .firstNotNullOfOrNull { downloader -> try { val getScope = object : GetScope, Scope by downloader.scopeImpl { @@ -210,7 +221,11 @@ class PatcherWorker( }?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version } } catch (e: UserInteractionException.Activity.NotCompleted) { throw e - } catch (_: UserInteractionException) { + } catch (e: UserInteractionException) { + Log.i(tag, "User interaction cancelled downloader flow", e) + throw e + } catch (e: IOException) { + Log.w(tag, "Downloader ${downloader.name} failed with an IO error, trying next downloader", e) null }?.let { (data, _) -> download(downloader, data) } } ?: throw Exception("App is not available.") @@ -218,7 +233,11 @@ class PatcherWorker( } is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) } - is SelectedApp.Installed -> File(pm.getPackageInfo(selectedApp.packageName)!!.applicationInfo!!.sourceDir) + is SelectedApp.Installed -> { + val pkgInfo = pm.getPackageInfo(selectedApp.packageName) ?: throw IllegalStateException("Package ${selectedApp.packageName} is not installed.") + val appInfo = pkgInfo.applicationInfo ?: throw IllegalStateException("Failed to retrieve application info for ${selectedApp.packageName}.") + File(appInfo.sourceDir) + } } val runtime = if (prefs.useProcessRuntime.get()) { @@ -238,10 +257,14 @@ class PatcherWorker( ) runStep(StepId.SignAPK, args.onEvent) { + require(patchedApk.exists() && patchedApk.length() > 0L) { + "Patched APK was not generated" + } keystoreManager.sign(patchedApk, File(args.output)) } Log.i(tag, "Patching succeeded".logFmt()) + success = true Result.success() } catch (e: ProcessRuntime.RemoteFailureException) { Log.e( @@ -266,6 +289,9 @@ class PatcherWorker( Result.failure() } finally { patchedApk.delete() + if (!success) { + File(args.output).delete() + } // Only delete the input APK right after patching finishes when the user isn't rooted, since it would be needed for mounting // (it would be deleted right after installing with root) if (args.input is SelectedApp.Local && args.input.temporary && Shell.isAppGrantedRoot() == false) { 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 32d2098270..409ee7c192 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 @@ -65,16 +65,16 @@ import app.revanced.manager.util.toast import app.revanced.manager.util.uiSafe import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers -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.BufferOverflow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.time.withTimeout import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent @@ -141,6 +141,10 @@ class PatcherViewModel( private var launchedActivity: CompletableDeferred? = null private val launchActivityChannel = Channel() val launchActivityFlow = launchActivityChannel.receiveAsFlow() + private val progressEventChannel = Channel( + capacity = 10000, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) private val tempDir = savedStateHandle.saveable(key = "tempDir") { fs.uiTempDir.resolve("installer").also { @@ -154,6 +158,7 @@ class PatcherViewModel( * It should not be cancelled on system-initiated process death since that would cancel the installation process. */ private val installerCoroutineScope = CoroutineScope(Dispatchers.Main) + private val cleanupScope = CoroutineScope(Dispatchers.Main) /** * Holds the package name of the Apk we are trying to install. @@ -271,17 +276,22 @@ class PatcherViewModel( viewModelScope.launch { installedApp = installedAppRepository.get(packageName) } + + viewModelScope.launch { + for (event in progressEventChannel) { + applyProgressEvent(event) + } + } } - @OptIn(DelicateCoroutinesApi::class) override fun onCleared() { super.onCleared() workManager.cancelWorkById(patcherWorkerId.uuid) if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.MOUNT) { - GlobalScope.launch(Dispatchers.Main) { + cleanupScope.launch { uiSafe(app, R.string.failed_to_mount, "Failed to mount") { - withTimeout(Duration.ofMinutes(1L)) { + withTimeout(Duration.ofSeconds(10L)) { rootInstaller.mount(packageName) } } @@ -289,9 +299,13 @@ class PatcherViewModel( } } - private fun handleProgressEvent(event: ProgressEvent) = viewModelScope.launch { + private fun handleProgressEvent(event: ProgressEvent) { + progressEventChannel.trySend(event) + } + + private fun applyProgressEvent(event: ProgressEvent) { if (event is ProgressEvent.Failed && event.stepId == null && steps.any { it.state == State.FAILED }) { - return@launch + return } val stepIndex = steps.indexOfFirst { @@ -554,7 +568,7 @@ class PatcherViewModel( installerPkgName, packageName, input.selectedApp.version ?: withContext(Dispatchers.IO) { - pm.getPackageInfo(outputFile)?.versionName!! + pm.getPackageInfo(outputFile)?.versionName ?: "unknown" }, InstallType.DEFAULT, input.selectedPatches, diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt index 981143c301..2181211125 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt @@ -126,7 +126,7 @@ class SelectedAppInfoViewModel( if (it.isSplitApk()) return@let null SelectedApp.Installed( packageName, - it.versionName!! + it.versionName ?: "unknown" ) to installedAppDeferred.await() } @@ -288,7 +288,7 @@ class SelectedAppInfoViewModel( pm.getPackageInfo(this)?.let { info -> SelectedApp.Local( packageName = info.packageName, - version = info.versionName!!, + version = info.versionName ?: "unknown", file = this, temporary = true ) 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 49239d7ac9..3909a8014b 100644 --- a/app/src/main/java/app/revanced/manager/util/Util.kt +++ b/app/src/main/java/app/revanced/manager/util/Util.kt @@ -6,7 +6,8 @@ import android.content.Context import android.content.Intent import android.content.pm.ApplicationInfo import android.graphics.Bitmap -import android.os.Build +import android.os.Handler +import android.os.Looper import android.renderscript.Allocation import android.renderscript.Element import android.renderscript.RenderScript @@ -23,7 +24,6 @@ import androidx.compose.material3.ListItemDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState import androidx.compose.runtime.rememberUpdatedState @@ -48,10 +48,7 @@ import app.revanced.manager.BuildConfig import app.revanced.manager.R import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest @@ -97,15 +94,13 @@ fun Context.toast(@StringRes stringRes: Int, duration: Int = Toast.LENGTH_SHORT) * @param logMsg The log message. * @param block The code to execute. */ -@OptIn(DelicateCoroutinesApi::class) inline fun uiSafe(context: Context, @StringRes toastMsg: Int, logMsg: String, block: () -> Unit) { try { block() } catch (e: CancellationException) { throw e } catch (error: Exception) { - // You can only toast on the main thread. - GlobalScope.launch(Dispatchers.Main) { + val showToast = { context.toast( context.getString( toastMsg, @@ -113,6 +108,8 @@ inline fun uiSafe(context: Context, @StringRes toastMsg: Int, logMsg: String, bl ) ) } + if (Looper.myLooper() == Looper.getMainLooper()) showToast() + else Handler(Looper.getMainLooper()).post(showToast) Log.e(tag, logMsg, error) }