feat: allow bundles to use classes from other bundles

This commit is contained in:
Ax333l 2024-06-16 21:55:22 +02:00
parent 413083c58d
commit 8b9314078c
No known key found for this signature in database
GPG key ID: D2B4D85271127D23
26 changed files with 393 additions and 268 deletions

View file

@ -149,6 +149,7 @@ dependencies {
// ReVanced
implementation(libs.revanced.patcher)
implementation(libs.revanced.library)
implementation(libs.revanced.multidexlib2)
// Native processes
implementation(libs.kotlin.process)

View file

@ -20,6 +20,6 @@ class LocalPatchBundle(name: String, id: Int, directory: File) : PatchBundleSour
}
}
reload()
refresh()
}
}

View file

@ -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
}
}

View file

@ -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)

View file

@ -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()

View file

@ -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<Patch<*>> {
private fun load(): Iterable<Patch<*>> {
patchesJar.setReadOnly()
return PatchBundleLoader.Dex(patchesJar, optimizedDexDirectory = null)
}
override fun iterator(): Iterator<Patch<*>> = 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
}
}
@Parcelize
data class PatchBundle(val patchesJar: File, val integrations: File?) : Parcelable

View file

@ -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<PatchInfo>) {
/**
* Information about a bundle and all the patches it contains.
*
* @see [PatchBundleInfo]
*/
class Global(name: String, uid: Int, patches: List<PatchInfo>) :
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<PatchInfo>()
val unsupported = mutableListOf<PatchInfo>()
val universal = mutableListOf<PatchInfo>()
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<PatchInfo>,
val supportedPatches: List<PatchInfo>,
val unsupportedPatches: List<PatchInfo>,
val universalPatches: List<PatchInfo>
) : PatchBundleInfo(name, uid, patches) {
fun patchSequence(allowUnsupported: Boolean) = if (allowUnsupported) {
patches.asSequence()
} else {
sequence {
yieldAll(supportedPatches)
yieldAll(universalPatches)
}
}
}
companion object Extensions {
inline fun Iterable<Scoped>.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
}
}
}

View file

@ -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<PatchBundle, Entry>()
constructor(bundles: Iterable<PatchBundle>) : 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<String> = 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<Patch<*>> {
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<String>, val classLoader: ClassLoader)
}

View file

@ -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) }

View file

@ -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()
)

View file

@ -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<String>,
val options: @RawValue Map<String, Map<String, Any?>>
) : Parcelable

View file

@ -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) ->

View file

@ -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
},
}
)
}
}

View file

@ -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
}
}

View file

@ -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()
}
}
}

View file

@ -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<PatchInfo>,
val unsupported: List<PatchInfo>,
val universal: List<PatchInfo>
) {
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<BundleInfo>.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<PatchInfo>()
val unsupported = mutableListOf<PatchInfo>()
val universal = mutableListOf<PatchInfo>()
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
}

View file

@ -0,0 +1,6 @@
package app.revanced.manager.ui.model
enum class BundleType {
Local,
Remote
}

View file

@ -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)
}
)
}

View file

@ -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) }
)
}
}

View file

@ -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 }
}
}

View file

@ -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<PatchBundleSource>()
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 ->

View file

@ -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<PreferencesManager>().disablePatchVersionCompatCheck.getBlocking()
val bundlesFlow =
get<PatchBundleRepository>().bundleInfoFlow(packageName, input.app.version)
get<PatchBundleRepository>().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<BundleInfo>) = bundles.any { bundle ->
fun selectionIsValid(bundles: List<PatchBundleInfo.Scoped>) = bundles.any { bundle ->
bundle.patchSequence(allowIncompatiblePatches).any { patch ->
isSelected(bundle.uid, patch)
}

View file

@ -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<BundleInfo>) = options.filtered(bundles)
fun getOptionsFiltered(bundles: List<PatchBundleInfo.Scoped>) = options.filtered(bundles)
fun getPatches(bundles: List<BundleInfo>, allowUnsupported: Boolean) =
fun getPatches(bundles: List<PatchBundleInfo.Scoped>, allowUnsupported: Boolean) =
selectionState.patches(bundles, allowUnsupported)
fun getCustomPatches(
bundles: List<BundleInfo>,
bundles: List<PatchBundleInfo.Scoped>,
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<BundleInfo>
bundles: List<PatchBundleInfo.Scoped>
) {
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<BundleInfo>): Options = buildMap options@{
private fun Options.filtered(bundles: List<PatchBundleInfo.Scoped>): 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<BundleInfo>, allowUnsupported: Boolean): PatchSelection
fun patches(bundles: List<PatchBundleInfo.Scoped>, allowUnsupported: Boolean): PatchSelection
@Parcelize
data class Customized(val patchSelection: PatchSelection) : SelectionState {
override fun patches(bundles: List<BundleInfo>, allowUnsupported: Boolean) =
override fun patches(bundles: List<PatchBundleInfo.Scoped>, 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<BundleInfo>, allowUnsupported: Boolean) =
override fun patches(bundles: List<PatchBundleInfo.Scoped>, allowUnsupported: Boolean) =
bundles.toPatchSelection(allowUnsupported) { _, patch -> patch.include }
}
}

View file

@ -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 }
)
}
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() }
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 }
)
}
}.groupingBy { it }
.eachCount()
.toMutableMap()
.apply {
replaceAll { _, count ->
count + patchesWithoutVersions
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() }
}
}
}.flowOn(Dispatchers.Default)
}.groupingBy { it }
.eachCount()
.toMutableMap()
.apply {
replaceAll { _, count ->
count + patchesWithoutVersions
}
}
}.flowOn(Dispatchers.Default)
init {
viewModelScope.launch {

View file

@ -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<AppInfo>{
compareByDescending<AppInfo> {
it.packageInfo != null && (it.patches ?: 0) > 0
}.thenByDescending {
it.patches

View file

@ -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" }