diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e11dd0e607..5e8aa3938e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -149,6 +149,7 @@ dependencies { // ReVanced implementation(libs.revanced.patcher) implementation(libs.revanced.library) + implementation(libs.revanced.multidexlib2) // Native processes implementation(libs.kotlin.process) diff --git a/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt b/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt index 9b6d1d60fb..d3b7cfb441 100644 --- a/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt +++ b/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt @@ -20,6 +20,6 @@ class LocalPatchBundle(name: String, id: Int, directory: File) : PatchBundleSour } } - reload() + refresh() } } diff --git a/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt b/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt index ebff077c6c..e35e4a36f3 100644 --- a/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt +++ b/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt @@ -1,9 +1,7 @@ package app.revanced.manager.domain.bundles -import android.util.Log import androidx.compose.runtime.Stable import app.revanced.manager.patcher.patch.PatchBundle -import app.revanced.manager.util.tag import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.flowOf @@ -18,7 +16,7 @@ sealed class PatchBundleSource(val name: String, val uid: Int, directory: File) protected val patchesFile = directory.resolve("patches.jar") protected val integrationsFile = directory.resolve("integrations.apk") - private val _state = MutableStateFlow(load()) + private val _state = MutableStateFlow(getPatchBundle()) val state = _state.asStateFlow() /** @@ -36,19 +34,16 @@ sealed class PatchBundleSource(val name: String, val uid: Int, directory: File) } } - private fun load(): State { - if (!hasInstalled()) return State.Missing + private fun getPatchBundle() = + if (!hasInstalled()) State.Missing + else State.Available(PatchBundle(patchesFile, integrationsFile.takeIf(File::exists))) - return try { - State.Loaded(PatchBundle(patchesFile, integrationsFile.takeIf(File::exists))) - } catch (t: Throwable) { - Log.e(tag, "Failed to load patch bundle $name", t) - State.Failed(t) - } + fun refresh() { + _state.value = getPatchBundle() } - fun reload() { - _state.value = load() + fun markAsFailed(e: Throwable) { + _state.value = State.Failed(e) } sealed interface State { @@ -56,7 +51,7 @@ sealed class PatchBundleSource(val name: String, val uid: Int, directory: File) data object Missing : State data class Failed(val throwable: Throwable) : State - data class Loaded(val bundle: PatchBundle) : State { + data class Available(val bundle: PatchBundle) : State { override fun patchBundleOrNull() = bundle } } diff --git a/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt b/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt index 1c857051d3..67f43d77fe 100644 --- a/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt +++ b/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt @@ -49,7 +49,7 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo } saveVersion(patches.version, integrations.version) - reload() + refresh() } suspend fun downloadLatest() { @@ -76,7 +76,7 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo suspend fun deleteLocalFiles() = withContext(Dispatchers.Default) { arrayOf(patchesFile, integrationsFile).forEach(File::delete) - reload() + refresh() } fun propsFlow() = configRepository.getProps(uid) diff --git a/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt index 73debb820d..cc7af3e1cc 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt @@ -15,6 +15,8 @@ import app.revanced.manager.domain.bundles.RemotePatchBundle import app.revanced.manager.domain.bundles.PatchBundleSource import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.patcher.patch.PatchInfo +import app.revanced.manager.patcher.patch.PatchBundleInfo +import app.revanced.manager.patcher.patch.PatchBundleLoader import app.revanced.manager.util.flatMapLatestAndCombine import app.revanced.manager.util.tag import app.revanced.manager.util.uiSafe @@ -22,6 +24,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -51,7 +54,46 @@ class PatchBundleRepository( it.state.map { state -> it.uid to state } } - val suggestedVersions = bundles.map { + val bundleInfoFlow = sources.flatMapLatestAndCombine( + transformer = { source -> + source.state.map { + source to it + } + }, + combiner = { states -> + val patchBundleLoader by lazy { + PatchBundleLoader(states.mapNotNull { (_, state) -> state.patchBundleOrNull() }) + } + + states.mapNotNull { (source, state) -> + val bundle = state.patchBundleOrNull() ?: return@mapNotNull null + + try { + source.uid to PatchBundleInfo.Global( + source.name, + source.uid, + patchBundleLoader.loadMetadata(bundle) + ) + } catch (t: Throwable) { + Log.e(tag, "Failed to load patches from ${source.name}", t) + source.markAsFailed(t) + + null + } + }.toMap() + } + ).flowOn(Dispatchers.Default) + + fun scopedBundleInfoFlow(packageName: String, version: String) = bundleInfoFlow.map { + it.map { (_, bundle) -> + bundle.forPackage( + packageName, + version + ) + } + } + + val suggestedVersions = bundleInfoFlow.map { val allPatches = it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet() diff --git a/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt b/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt index a463998f75..cc2faf2860 100644 --- a/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt +++ b/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt @@ -1,43 +1,8 @@ package app.revanced.manager.patcher.patch -import android.util.Log -import app.revanced.manager.util.tag -import app.revanced.patcher.PatchBundleLoader -import app.revanced.patcher.patch.Patch +import android.os.Parcelable +import kotlinx.parcelize.Parcelize import java.io.File -class PatchBundle(val patchesJar: File, val integrations: File?) { - private val loader = object : Iterable> { - private fun load(): Iterable> { - patchesJar.setReadOnly() - return PatchBundleLoader.Dex(patchesJar, optimizedDexDirectory = null) - } - - override fun iterator(): Iterator> = load().iterator() - } - - init { - Log.d(tag, "Loaded patch bundle: $patchesJar") - } - - /** - * A list containing the metadata of every patch inside this bundle. - */ - val patches = loader.map(::PatchInfo) - - /** - * Load all patches compatible with the specified package. - */ - fun patchClasses(packageName: String) = loader.filter { patch -> - val compatiblePackages = patch.compatiblePackages - ?: // The patch has no compatibility constraints, which means it is universal. - return@filter true - - if (!compatiblePackages.any { it.name == packageName }) { - // Patch is not compatible with this package. - return@filter false - } - - true - } -} \ No newline at end of file +@Parcelize +data class PatchBundle(val patchesJar: File, val integrations: File?) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundleInfo.kt b/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundleInfo.kt new file mode 100644 index 0000000000..344b0c1a4d --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundleInfo.kt @@ -0,0 +1,94 @@ +package app.revanced.manager.patcher.patch + +import app.revanced.manager.util.PatchSelection + +/** + * A base class for storing [PatchBundle] metadata. + * + * @param name The name of the bundle. + * @param uid The unique ID of the bundle. + * @param patches The patch list. + */ +sealed class PatchBundleInfo(val name: String, val uid: Int, val patches: List) { + /** + * Information about a bundle and all the patches it contains. + * + * @see [PatchBundleInfo] + */ + class Global(name: String, uid: Int, patches: List) : + PatchBundleInfo(name, uid, patches) { + + /** + * Create a [PatchBundleInfo.Scoped] that only contains information about patches that are relevant for a specific [packageName]. + */ + fun forPackage(packageName: String, version: String): Scoped { + val relevantPatches = patches.filter { it.compatibleWith(packageName) } + val supported = mutableListOf() + val unsupported = mutableListOf() + val universal = mutableListOf() + + relevantPatches.forEach { + val targetList = when { + it.compatiblePackages == null -> universal + it.supportsVersion( + packageName, + version + ) -> supported + + else -> unsupported + } + + targetList.add(it) + } + + return Scoped(name, uid, relevantPatches, supported, unsupported, universal) + } + } + + /** + * Contains information about a bundle that is relevant for a specific package name. + * + * @param supportedPatches Patches that are compatible with the specified package name and version. + * @param unsupportedPatches Patches that are compatible with the specified package name but not version. + * @param universalPatches Patches that are compatible with all packages. + * @see [PatchBundleInfo.Global.forPackage] + * @see [PatchBundleInfo] + */ + class Scoped( + name: String, + uid: Int, + patches: List, + val supportedPatches: List, + val unsupportedPatches: List, + val universalPatches: List + ) : PatchBundleInfo(name, uid, patches) { + fun patchSequence(allowUnsupported: Boolean) = if (allowUnsupported) { + patches.asSequence() + } else { + sequence { + yieldAll(supportedPatches) + yieldAll(universalPatches) + } + } + } + + companion object Extensions { + inline fun Iterable.toPatchSelection( + allowUnsupported: Boolean, + condition: (Int, PatchInfo) -> Boolean + ): PatchSelection = this.associate { bundle -> + val patches = + bundle.patchSequence(allowUnsupported) + .mapNotNullTo(mutableSetOf()) { patch -> + patch.name.takeIf { + condition( + bundle.uid, + patch + ) + } + } + + bundle.uid to patches + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundleLoader.kt b/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundleLoader.kt new file mode 100644 index 0000000000..c6c93c67e2 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundleLoader.kt @@ -0,0 +1,99 @@ +package app.revanced.manager.patcher.patch + +import android.os.Build +import app.revanced.patcher.patch.Patch +import dalvik.system.DelegateLastClassLoader +import dalvik.system.PathClassLoader +import lanchon.multidexlib2.BasicDexFileNamer +import lanchon.multidexlib2.MultiDexIO +import java.io.File + +class PatchBundleLoader() : ClassLoader(Patch::class.java.classLoader) { + private val registry = mutableMapOf() + + constructor(bundles: Iterable) : this() { + bundles.forEach(::register) + } + + override fun findClass(name: String?): Class<*> { + registry.values.find { entry -> name in entry.classes }?.let { + return it.classLoader.loadClass(name) + } + + return super.findClass(name) + } + + // Taken from: https://github.com/ReVanced/revanced-patcher/blob/f57e571a147d33eed189b533eee3aa62388fb354/src/main/kotlin/app/revanced/patcher/PatchBundleLoader.kt#L127-L130 + private fun readClassNames(bundlePath: File): Set = MultiDexIO.readDexFile( + true, + bundlePath, + BasicDexFileNamer(), + null, + null + ).classes.map { classDef -> + classDef.type.substring(1, classDef.length - 1).replace('/', '.') + }.toSet() + + private fun createClassLoader(bundlePath: File) = + bundlePath.also(File::setReadOnly).absolutePath.let { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + // We need the delegate last policy for cross-bundle dependencies. + DelegateLastClassLoader(it, this) + } else { + PathClassLoader(it, parent) + } + } + + fun register(bundle: PatchBundle) { + registry[bundle] = + Entry(readClassNames(bundle.patchesJar), createClassLoader(bundle.patchesJar)) + } + + private fun loadPatches(bundle: PatchBundle): List> { + val entry = registry[bundle] + ?: throw Exception("Attempted to load classes from a patch bundle that has not been registered.") + + // Taken from: https://github.com/ReVanced/revanced-patcher/blob/f57e571a147d33eed189b533eee3aa62388fb354/src/main/kotlin/app/revanced/patcher/PatchBundleLoader.kt#L48-L54 + return entry.classes + .map { entry.classLoader.loadClass(it) } + .filter { Patch::class.java.isAssignableFrom(it) } + .mapNotNull { it.getInstance() } + .filter { it.name != null } + } + + fun loadPatches(bundle: PatchBundle, packageName: String) = + loadPatches(bundle).filter { patch -> + val compatiblePackages = patch.compatiblePackages + ?: // The patch has no compatibility constraints, which means it is universal. + return@filter true + + if (!compatiblePackages.any { it.name == packageName }) { + // Patch is not compatible with this package. + return@filter false + } + + true + } + + fun loadMetadata(bundle: PatchBundle) = loadPatches(bundle).map(::PatchInfo) + + private companion object { + fun Class<*>.getInstance(): Patch<*>? { + try { + // Get the Kotlin singleton instance. + return getField("INSTANCE").get(null) as Patch<*> + } catch (_: NoSuchFieldException) { + } + + try { + // Try to instantiate the class. + return getDeclaredConstructor().newInstance() as Patch<*> + } catch (_: Exception) { + } + + return null + } + } + + private data class Entry(val classes: Set, val classLoader: ClassLoader) +} \ No newline at end of file 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..e422351891 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 @@ -3,6 +3,7 @@ package app.revanced.manager.patcher.runtime import android.content.Context import app.revanced.manager.patcher.Session import app.revanced.manager.patcher.logger.Logger +import app.revanced.manager.patcher.patch.PatchBundleLoader import app.revanced.manager.patcher.worker.ProgressEventHandler import app.revanced.manager.ui.model.State import app.revanced.manager.util.Options @@ -26,8 +27,11 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) { val bundles = bundles() val selectedBundles = selectedPatches.keys - val allPatches = bundles.filterKeys { selectedBundles.contains(it) } - .mapValues { (_, bundle) -> bundle.patchClasses(packageName) } + val allPatches = with(PatchBundleLoader(bundles.values)) { + bundles + .filterKeys { selectedBundles.contains(it) } + .mapValues { (_, bundle) -> loadPatches(bundle, packageName) } + } val patchList = selectedPatches.flatMap { (bundle, selected) -> allPatches[bundle]?.filter { selected.contains(it.name) } 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..cc2330ab89 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 @@ -150,11 +150,8 @@ class ProcessRuntime(private val context: Context) : Runtime(context) { outputFile = outputFile, enableMultithrededDexWriter = enableMultithreadedDexWriter(), configurations = selectedPatches.map { (id, patches) -> - val bundle = bundles[id]!! - PatchConfiguration( - bundle.patchesJar.absolutePath, - bundle.integrations?.absolutePath, + bundles[id]!!, patches, options[id].orEmpty() ) diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/process/Parameters.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/process/Parameters.kt index c669c87507..9f4e183478 100644 --- a/app/src/main/java/app/revanced/manager/patcher/runtime/process/Parameters.kt +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/process/Parameters.kt @@ -1,6 +1,7 @@ package app.revanced.manager.patcher.runtime.process import android.os.Parcelable +import app.revanced.manager.patcher.patch.PatchBundle import kotlinx.parcelize.Parcelize import kotlinx.parcelize.RawValue @@ -18,8 +19,7 @@ data class Parameters( @Parcelize data class PatchConfiguration( - val bundlePath: String, - val integrationsPath: String?, + val bundle: PatchBundle, val patches: Set, val options: @RawValue Map> ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt index 4467f3aea3..7bdbabc98e 100644 --- a/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt @@ -9,7 +9,7 @@ import app.revanced.manager.BuildConfig import app.revanced.manager.patcher.Session import app.revanced.manager.patcher.logger.LogLevel import app.revanced.manager.patcher.logger.Logger -import app.revanced.manager.patcher.patch.PatchBundle +import app.revanced.manager.patcher.patch.PatchBundleLoader import app.revanced.manager.patcher.runtime.ProcessRuntime import app.revanced.manager.ui.model.State import kotlinx.coroutines.CoroutineExceptionHandler @@ -55,12 +55,14 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() { logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB") val integrations = - parameters.configurations.mapNotNull { it.integrationsPath?.let(::File) } - val patchList = parameters.configurations.flatMap { config -> - val bundle = PatchBundle(File(config.bundlePath), null) + parameters.configurations.mapNotNull { it.bundle.integrations } + val patchBundleLoader = PatchBundleLoader(parameters.configurations.map { it.bundle }) + val patchList = parameters.configurations.flatMap { config -> val patches = - bundle.patchClasses(parameters.packageName).filter { it.name in config.patches } + patchBundleLoader + .loadPatches(config.bundle, parameters.packageName) + .filter { it.name in config.patches } .associateBy { it.name } config.options.forEach { (patchName, opts) -> diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt index 59b119f37b..88da150f5c 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt @@ -32,27 +32,25 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable fun BundleInformationDialog( + bundle: PatchBundleSource, + patchCount: Int, onDismissRequest: () -> Unit, onDeleteRequest: () -> Unit, - bundle: PatchBundleSource, onRefreshButton: () -> Unit, ) { val composableScope = rememberCoroutineScope() var viewCurrentBundlePatches by remember { mutableStateOf(false) } val isLocal = bundle is LocalPatchBundle - val patchCount by remember(bundle) { - bundle.state.map { it.patchBundleOrNull()?.patches?.size ?: 0 } - }.collectAsStateWithLifecycle(0) val props by remember(bundle) { bundle.propsOrNullFlow() }.collectAsStateWithLifecycle(null) if (viewCurrentBundlePatches) { BundlePatchesDialog( + bundle = bundle, onDismissRequest = { viewCurrentBundlePatches = false - }, - bundle = bundle, + } ) } @@ -110,7 +108,7 @@ fun BundleInformationDialog( }, onPatchesClick = { viewCurrentBundlePatches = true - }, + } ) } } diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt index 6cb13891bc..c053c7c707 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt @@ -34,12 +34,13 @@ import kotlinx.coroutines.flow.map @Composable fun BundleItem( bundle: PatchBundleSource, - onDelete: () -> Unit, - onUpdate: () -> Unit, + patchCount: Int, selectable: Boolean, - onSelect: () -> Unit, isBundleSelected: Boolean, toggleSelection: (Boolean) -> Unit, + onDelete: () -> Unit, + onUpdate: () -> Unit, + onSelect: () -> Unit, ) { var viewBundleDialogPage by rememberSaveable { mutableStateOf(false) } val state by bundle.state.collectAsStateWithLifecycle() @@ -50,12 +51,13 @@ fun BundleItem( if (viewBundleDialogPage) { BundleInformationDialog( + bundle = bundle, + patchCount = patchCount, onDismissRequest = { viewBundleDialogPage = false }, onDeleteRequest = { viewBundleDialogPage = false onDelete() }, - bundle = bundle, onRefreshButton = onUpdate, ) } @@ -79,9 +81,7 @@ fun BundleItem( headlineContent = { Text(text = bundle.name) }, supportingContent = { - state.patchBundleOrNull()?.patches?.size?.let { patchCount -> - Text(text = pluralStringResource(R.plurals.patch_count, patchCount, patchCount)) - } + Text(text = pluralStringResource(R.plurals.patch_count, patchCount, patchCount)) }, trailingContent = { Row { @@ -89,7 +89,7 @@ fun BundleItem( when (state) { is PatchBundleSource.State.Failed -> Icons.Outlined.ErrorOutline to R.string.bundle_error is PatchBundleSource.State.Missing -> Icons.Outlined.Warning to R.string.bundle_missing - is PatchBundleSource.State.Loaded -> null + is PatchBundleSource.State.Available -> null } } diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt index df2e8e6733..1dfa07246f 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt @@ -26,17 +26,23 @@ import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R import app.revanced.manager.domain.bundles.PatchBundleSource +import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.ui.component.LazyColumnWithScrollbar import app.revanced.manager.ui.component.NotificationCard +import kotlinx.coroutines.flow.mapNotNull +import org.koin.compose.koinInject @OptIn(ExperimentalMaterial3Api::class) @Composable fun BundlePatchesDialog( - onDismissRequest: () -> Unit, bundle: PatchBundleSource, + onDismissRequest: () -> Unit, ) { var informationCardVisible by remember { mutableStateOf(true) } - val state by bundle.state.collectAsStateWithLifecycle() + val patchBundleRepository: PatchBundleRepository = koinInject() + val patches by remember(bundle.uid) { + patchBundleRepository.bundleInfoFlow.mapNotNull { it[bundle.uid]?.patches } + }.collectAsStateWithLifecycle(initialValue = emptyList()) Dialog( onDismissRequest = onDismissRequest, @@ -75,29 +81,28 @@ fun BundlePatchesDialog( } } - state.patchBundleOrNull()?.let { bundle -> - items(bundle.patches.size) { bundleIndex -> - val patch = bundle.patches[bundleIndex] - ListItem( - headlineContent = { + items(patches.size) { index -> + val patch = patches[index] + + ListItem( + headlineContent = { + Text( + text = patch.name, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface + ) + }, + supportingContent = { + patch.description?.let { Text( - text = patch.name, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface + text = it, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant ) - }, - supportingContent = { - patch.description?.let { - Text( - text = it, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } } - ) - HorizontalDivider() - } + } + ) + HorizontalDivider() } } } diff --git a/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt b/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt deleted file mode 100644 index db29572444..0000000000 --- a/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt +++ /dev/null @@ -1,87 +0,0 @@ -package app.revanced.manager.ui.model - -import app.revanced.manager.domain.repository.PatchBundleRepository -import app.revanced.manager.patcher.patch.PatchInfo -import app.revanced.manager.util.PatchSelection -import app.revanced.manager.util.flatMapLatestAndCombine -import kotlinx.coroutines.flow.map - -/** - * A data class that contains patch bundle metadata for use by UI code. - */ -data class BundleInfo( - val name: String, - val uid: Int, - val supported: List, - val unsupported: List, - val universal: List -) { - val all = sequence { - yieldAll(supported) - yieldAll(unsupported) - yieldAll(universal) - } - - val patchCount get() = supported.size + unsupported.size + universal.size - - fun patchSequence(allowUnsupported: Boolean) = if (allowUnsupported) { - all - } else { - sequence { - yieldAll(supported) - yieldAll(universal) - } - } - - companion object Extensions { - inline fun Iterable.toPatchSelection(allowUnsupported: Boolean, condition: (Int, PatchInfo) -> Boolean): PatchSelection = this.associate { bundle -> - val patches = - bundle.patchSequence(allowUnsupported) - .mapNotNullTo(mutableSetOf()) { patch -> - patch.name.takeIf { - condition( - bundle.uid, - patch - ) - } - } - - bundle.uid to patches - } - - fun PatchBundleRepository.bundleInfoFlow(packageName: String, version: String) = - sources.flatMapLatestAndCombine( - combiner = { it.filterNotNull() } - ) { source -> - // Regenerate bundle information whenever this source updates. - source.state.map { state -> - val bundle = state.patchBundleOrNull() ?: return@map null - - val supported = mutableListOf() - val unsupported = mutableListOf() - val universal = mutableListOf() - - bundle.patches.filter { it.compatibleWith(packageName) }.forEach { - val targetList = when { - it.compatiblePackages == null -> universal - it.supportsVersion( - packageName, - version - ) -> supported - - else -> unsupported - } - - targetList.add(it) - } - - BundleInfo(source.name, source.uid, supported, unsupported, universal) - } - } - } -} - -enum class BundleType { - Local, - Remote -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/model/BundleType.kt b/app/src/main/java/app/revanced/manager/ui/model/BundleType.kt new file mode 100644 index 0000000000..d3281a91a7 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/model/BundleType.kt @@ -0,0 +1,6 @@ +package app.revanced.manager.ui.model + +enum class BundleType { + Local, + Remote +} \ No newline at end of file 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 03b0e26c84..4a03c7c779 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 @@ -79,7 +79,7 @@ fun DashboardScreen( onAppClick: (InstalledApp) -> Unit ) { val bundlesSelectable by remember { derivedStateOf { vm.selectedSources.size > 0 } } - val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0) + val availablePatches by vm.availablePatchesCountFlow.collectAsStateWithLifecycle(0) val androidContext = LocalContext.current val composableScope = rememberCoroutineScope() val pagerState = rememberPagerState( @@ -269,6 +269,9 @@ fun DashboardScreen( } val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList()) + val patchCounts by vm.bundlePatchCountsFlow.collectAsStateWithLifecycle( + initialValue = emptyMap() + ) Column( modifier = Modifier.fillMaxSize(), @@ -276,23 +279,24 @@ fun DashboardScreen( sources.forEach { BundleItem( bundle = it, - onDelete = { - vm.delete(it) - }, - onUpdate = { - vm.update(it) - }, + patchCount = patchCounts[it.uid] ?: 0, + isBundleSelected = vm.selectedSources.contains(it), selectable = bundlesSelectable, onSelect = { vm.selectedSources.add(it) }, - isBundleSelected = vm.selectedSources.contains(it), toggleSelection = { bundleIsNotSelected -> if (bundleIsNotSelected) { vm.selectedSources.add(it) } else { vm.selectedSources.remove(it) } + }, + onDelete = { + vm.delete(it) + }, + onUpdate = { + vm.update(it) } ) } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt index 8a7988ff1a..8b4b5f7a31 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt @@ -224,13 +224,13 @@ fun PatchesSelectorScreen( patchList( uid = bundle.uid, - patches = bundle.supported.searched(), + patches = bundle.supportedPatches.searched(), filterFlag = SHOW_SUPPORTED, supported = true ) patchList( uid = bundle.uid, - patches = bundle.universal.searched(), + patches = bundle.universalPatches.searched(), filterFlag = SHOW_UNIVERSAL, supported = true ) { @@ -242,13 +242,13 @@ fun PatchesSelectorScreen( if (!vm.allowIncompatiblePatches) return@LazyColumnWithScrollbar patchList( uid = bundle.uid, - patches = bundle.unsupported.searched(), + patches = bundle.unsupportedPatches.searched(), filterFlag = SHOW_UNSUPPORTED, supported = true ) { ListHeader( title = stringResource(R.string.unsupported_patches), - onHelpClick = { vm.openUnsupportedDialog(bundle.unsupported) } + onHelpClick = { vm.openUnsupportedDialog(bundle.unsupportedPatches) } ) } } @@ -332,13 +332,13 @@ fun PatchesSelectorScreen( ) { patchList( uid = bundle.uid, - patches = bundle.supported, + patches = bundle.supportedPatches, filterFlag = SHOW_SUPPORTED, supported = true ) patchList( uid = bundle.uid, - patches = bundle.universal, + patches = bundle.universalPatches, filterFlag = SHOW_UNIVERSAL, supported = true ) { @@ -348,13 +348,13 @@ fun PatchesSelectorScreen( } patchList( uid = bundle.uid, - patches = bundle.unsupported, + patches = bundle.unsupportedPatches, filterFlag = SHOW_UNSUPPORTED, supported = vm.allowIncompatiblePatches ) { ListHeader( title = stringResource(R.string.unsupported_patches), - onHelpClick = { vm.openUnsupportedDialog(bundle.unsupported) } + onHelpClick = { vm.openUnsupportedDialog(bundle.unsupportedPatches) } ) } } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt index f38aa45741..9643da33f0 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt @@ -27,7 +27,6 @@ import app.revanced.manager.R import app.revanced.manager.ui.component.AppInfo import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.destination.SelectedAppInfoDestination -import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel @@ -53,7 +52,7 @@ fun SelectedAppInfoScreen( val packageName = vm.selectedApp.packageName val version = vm.selectedApp.version val bundles by remember(packageName, version) { - vm.bundlesRepo.bundleInfoFlow(packageName, version) + vm.bundlesRepo.scopedBundleInfoFlow(packageName, version) }.collectAsStateWithLifecycle(initialValue = emptyList()) val allowIncompatiblePatches by vm.prefs.disablePatchVersionCompatCheck.getAsState() @@ -69,7 +68,7 @@ fun SelectedAppInfoScreen( } val availablePatchCount by remember { derivedStateOf { - bundles.sumOf { it.patchCount } + bundles.sumOf { it.patches.size } } } 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 22b83770f3..5039d3de84 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 @@ -30,13 +30,12 @@ class DashboardViewModel( private val networkInfo: NetworkInfo, val prefs: PreferencesManager ) : ViewModel() { - val availablePatches = - patchBundleRepository.bundles.map { it.values.sumOf { bundle -> bundle.patches.size } } + val bundlePatchCountsFlow = patchBundleRepository.bundleInfoFlow.map { it.mapValues { (_, bundle) -> bundle.patches.size } } + val availablePatchesCountFlow = bundlePatchCountsFlow.map { it.values.sum() } private val contentResolver: ContentResolver = app.contentResolver val sources = patchBundleRepository.sources val selectedSources = mutableStateListOf() - var updatedManagerVersion: String? by mutableStateOf(null) private set @@ -76,10 +75,10 @@ class DashboardViewModel( } } - fun cancelSourceSelection() { selectedSources.clear() } + fun createLocalSource(name: String, patchBundle: Uri, integrations: Uri?) = viewModelScope.launch { contentResolver.openInputStream(patchBundle)!!.use { patchesStream -> diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt index 47fbc556ad..2d6549fe7b 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt @@ -18,10 +18,9 @@ import androidx.lifecycle.viewmodel.compose.saveable import app.revanced.manager.R import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.patcher.patch.PatchBundleInfo +import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection import app.revanced.manager.patcher.patch.PatchInfo -import app.revanced.manager.ui.model.BundleInfo -import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow -import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.util.Options import app.revanced.manager.util.PatchSelection @@ -55,7 +54,7 @@ class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent { val allowIncompatiblePatches = get().disablePatchVersionCompatCheck.getBlocking() val bundlesFlow = - get().bundleInfoFlow(packageName, input.app.version) + get().scopedBundleInfoFlow(packageName, input.app.version) init { viewModelScope.launch { @@ -64,11 +63,11 @@ class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent { return@launch } - fun BundleInfo.hasDefaultPatches() = + fun PatchBundleInfo.Scoped.hasDefaultPatches() = patchSequence(allowIncompatiblePatches).any { it.include } // Don't show the warning if there are no default patches. - selectionWarningEnabled = bundlesFlow.first().any(BundleInfo::hasDefaultPatches) + selectionWarningEnabled = bundlesFlow.first().any(PatchBundleInfo.Scoped::hasDefaultPatches) } } @@ -107,7 +106,7 @@ class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent { return generatedSelection.toPersistentPatchSelection() } - fun selectionIsValid(bundles: List) = bundles.any { bundle -> + fun selectionIsValid(bundles: List) = bundles.any { bundle -> bundle.patchSequence(allowIncompatiblePatches).any { patch -> isSelected(bundle.uid, patch) } 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 92910b8bf0..9419d3f003 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 @@ -15,8 +15,8 @@ import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchOptionsRepository import app.revanced.manager.domain.repository.PatchSelectionRepository -import app.revanced.manager.ui.model.BundleInfo -import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection +import app.revanced.manager.patcher.patch.PatchBundleInfo +import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.util.Options import app.revanced.manager.util.PM @@ -101,13 +101,13 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent { selectedAppInfo = info } - fun getOptionsFiltered(bundles: List) = options.filtered(bundles) + fun getOptionsFiltered(bundles: List) = options.filtered(bundles) - fun getPatches(bundles: List, allowUnsupported: Boolean) = + fun getPatches(bundles: List, allowUnsupported: Boolean) = selectionState.patches(bundles, allowUnsupported) fun getCustomPatches( - bundles: List, + bundles: List, allowUnsupported: Boolean ): PatchSelection? = (selectionState as? SelectionState.Customized)?.patches(bundles, allowUnsupported) @@ -115,7 +115,7 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent { fun updateConfiguration( selection: PatchSelection?, options: Options, - bundles: List + bundles: List ) { selectionState = selection?.let(SelectionState::Customized) ?: SelectionState.Default @@ -142,11 +142,11 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent { /** * Returns a copy with all nonexistent options removed. */ - private fun Options.filtered(bundles: List): Options = buildMap options@{ + private fun Options.filtered(bundles: List): Options = buildMap options@{ bundles.forEach bundles@{ bundle -> val bundleOptions = this@filtered[bundle.uid] ?: return@bundles - val patches = bundle.all.associateBy { it.name } + val patches = bundle.patches.associateBy { it.name } this@options[bundle.uid] = buildMap bundleOptions@{ bundleOptions.forEach patch@{ (patchName, values) -> @@ -165,11 +165,11 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent { } private sealed interface SelectionState : Parcelable { - fun patches(bundles: List, allowUnsupported: Boolean): PatchSelection + fun patches(bundles: List, allowUnsupported: Boolean): PatchSelection @Parcelize data class Customized(val patchSelection: PatchSelection) : SelectionState { - override fun patches(bundles: List, allowUnsupported: Boolean) = + override fun patches(bundles: List, allowUnsupported: Boolean) = bundles.toPatchSelection( allowUnsupported ) { uid, patch -> @@ -179,7 +179,7 @@ private sealed interface SelectionState : Parcelable { @Parcelize data object Default : SelectionState { - override fun patches(bundles: List, allowUnsupported: Boolean) = + override fun patches(bundles: List, allowUnsupported: Boolean) = bundles.toPatchSelection(allowUnsupported) { _, patch -> patch.include } } } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt index 306397ad4b..54f1127a71 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt @@ -64,37 +64,38 @@ class VersionSelectorViewModel( patchBundleRepository.suggestedVersions.first()[packageName] } - val supportedVersions = patchBundleRepository.bundles.map supportedVersions@{ bundles -> - requiredVersionAsync.await()?.let { version -> - // It is mandatory to use the suggested version if the safeguard is enabled. - return@supportedVersions mapOf( - version to bundles - .asSequence() - .flatMap { (_, bundle) -> bundle.patches } - .flatMap { it.compatiblePackages.orEmpty() } - .filter { it.packageName == packageName } - .count { it.versions.isNullOrEmpty() || version in it.versions } - ) - } + val supportedVersions = + patchBundleRepository.bundleInfoFlow.map supportedVersions@{ bundles -> + requiredVersionAsync.await()?.let { version -> + // It is mandatory to use the suggested version if the safeguard is enabled. + return@supportedVersions mapOf( + version to bundles + .asSequence() + .flatMap { (_, bundle) -> bundle.patches } + .flatMap { it.compatiblePackages.orEmpty() } + .filter { it.packageName == packageName } + .count { it.versions.isNullOrEmpty() || version in it.versions } + ) + } - var patchesWithoutVersions = 0 + var patchesWithoutVersions = 0 - bundles.flatMap { (_, bundle) -> - bundle.patches.flatMap { patch -> - patch.compatiblePackages.orEmpty() - .filter { it.packageName == packageName } - .onEach { if (it.versions == null) patchesWithoutVersions++ } - .flatMap { it.versions.orEmpty() } - } - }.groupingBy { it } - .eachCount() - .toMutableMap() - .apply { - replaceAll { _, count -> - count + patchesWithoutVersions + bundles.flatMap { (_, bundle) -> + bundle.patches.flatMap { patch -> + patch.compatiblePackages.orEmpty() + .filter { it.packageName == packageName } + .onEach { if (it.versions == null) patchesWithoutVersions++ } + .flatMap { it.versions.orEmpty() } } - } - }.flowOn(Dispatchers.Default) + }.groupingBy { it } + .eachCount() + .toMutableMap() + .apply { + replaceAll { _, count -> + count + patchesWithoutVersions + } + } + }.flowOn(Dispatchers.Default) init { viewModelScope.launch { 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 21a60b97c2..97bfcf5df1 100644 --- a/app/src/main/java/app/revanced/manager/util/PM.kt +++ b/app/src/main/java/app/revanced/manager/util/PM.kt @@ -43,10 +43,10 @@ class PM( ) { private val scope = CoroutineScope(Dispatchers.IO) - val appList = patchBundleRepository.bundles.map { bundles -> + val appList = patchBundleRepository.bundleInfoFlow.map { bundles -> val compatibleApps = scope.async { - val compatiblePackages = bundles.values - .flatMap { it.patches } + val compatiblePackages = bundles + .flatMap { (_, bundle) -> bundle.patches } .flatMap { it.compatiblePackages.orEmpty() } .groupingBy { it.packageName } .eachCount() @@ -80,7 +80,7 @@ class PM( (compatibleApps.await() + installedApps.await()) .distinctBy { it.packageName } .sortedWith( - compareByDescending{ + compareByDescending { it.packageInfo != null && (it.patches ?: 0) > 0 }.thenByDescending { it.patches diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b78ce77e3d..730f9de494 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,6 +16,7 @@ collection = "0.3.7" room-version = "2.6.1" revanced-patcher = "19.3.1" revanced-library = "2.2.1" +revanced-multidexlib2 = "3.0.3.r3" koin-version = "3.5.3" koin-version-compose = "3.5.3" reimagined-navigation = "1.5.0" @@ -77,6 +78,7 @@ room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = # Patcher revanced-patcher = { group = "app.revanced", name = "revanced-patcher", version.ref = "revanced-patcher" } revanced-library = { group = "app.revanced", name = "revanced-library", version.ref = "revanced-library" } +revanced-multidexlib2 = { group = "app.revanced", name = "multidexlib2", version.ref = "revanced-multidexlib2" } # Koin koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin-version" }