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 // ReVanced
implementation(libs.revanced.patcher) implementation(libs.revanced.patcher)
implementation(libs.revanced.library) implementation(libs.revanced.library)
implementation(libs.revanced.multidexlib2)
// Native processes // Native processes
implementation(libs.kotlin.process) 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 package app.revanced.manager.domain.bundles
import android.util.Log
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import app.revanced.manager.patcher.patch.PatchBundle import app.revanced.manager.patcher.patch.PatchBundle
import app.revanced.manager.util.tag
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flowOf 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 patchesFile = directory.resolve("patches.jar")
protected val integrationsFile = directory.resolve("integrations.apk") protected val integrationsFile = directory.resolve("integrations.apk")
private val _state = MutableStateFlow(load()) private val _state = MutableStateFlow(getPatchBundle())
val state = _state.asStateFlow() val state = _state.asStateFlow()
/** /**
@ -36,19 +34,16 @@ sealed class PatchBundleSource(val name: String, val uid: Int, directory: File)
} }
} }
private fun load(): State { private fun getPatchBundle() =
if (!hasInstalled()) return State.Missing if (!hasInstalled()) State.Missing
else State.Available(PatchBundle(patchesFile, integrationsFile.takeIf(File::exists)))
return try { fun refresh() {
State.Loaded(PatchBundle(patchesFile, integrationsFile.takeIf(File::exists))) _state.value = getPatchBundle()
} catch (t: Throwable) {
Log.e(tag, "Failed to load patch bundle $name", t)
State.Failed(t)
}
} }
fun reload() { fun markAsFailed(e: Throwable) {
_state.value = load() _state.value = State.Failed(e)
} }
sealed interface State { sealed interface State {
@ -56,7 +51,7 @@ sealed class PatchBundleSource(val name: String, val uid: Int, directory: File)
data object Missing : State data object Missing : State
data class Failed(val throwable: Throwable) : 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 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) saveVersion(patches.version, integrations.version)
reload() refresh()
} }
suspend fun downloadLatest() { suspend fun downloadLatest() {
@ -76,7 +76,7 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
suspend fun deleteLocalFiles() = withContext(Dispatchers.Default) { suspend fun deleteLocalFiles() = withContext(Dispatchers.Default) {
arrayOf(patchesFile, integrationsFile).forEach(File::delete) arrayOf(patchesFile, integrationsFile).forEach(File::delete)
reload() refresh()
} }
fun propsFlow() = configRepository.getProps(uid) 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.bundles.PatchBundleSource
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.patcher.patch.PatchInfo 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.flatMapLatestAndCombine
import app.revanced.manager.util.tag import app.revanced.manager.util.tag
import app.revanced.manager.util.uiSafe import app.revanced.manager.util.uiSafe
@ -22,6 +24,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -51,7 +54,46 @@ class PatchBundleRepository(
it.state.map { state -> it.uid to state } 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 = val allPatches =
it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet() it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet()

View file

@ -1,43 +1,8 @@
package app.revanced.manager.patcher.patch package app.revanced.manager.patcher.patch
import android.util.Log import android.os.Parcelable
import app.revanced.manager.util.tag import kotlinx.parcelize.Parcelize
import app.revanced.patcher.PatchBundleLoader
import app.revanced.patcher.patch.Patch
import java.io.File import java.io.File
class PatchBundle(val patchesJar: File, val integrations: File?) { @Parcelize
private val loader = object : Iterable<Patch<*>> { data class PatchBundle(val patchesJar: File, val integrations: File?) : Parcelable
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
}
}

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 android.content.Context
import app.revanced.manager.patcher.Session import app.revanced.manager.patcher.Session
import app.revanced.manager.patcher.logger.Logger 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.patcher.worker.ProgressEventHandler
import app.revanced.manager.ui.model.State import app.revanced.manager.ui.model.State
import app.revanced.manager.util.Options import app.revanced.manager.util.Options
@ -26,8 +27,11 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) {
val bundles = bundles() val bundles = bundles()
val selectedBundles = selectedPatches.keys val selectedBundles = selectedPatches.keys
val allPatches = bundles.filterKeys { selectedBundles.contains(it) } val allPatches = with(PatchBundleLoader(bundles.values)) {
.mapValues { (_, bundle) -> bundle.patchClasses(packageName) } bundles
.filterKeys { selectedBundles.contains(it) }
.mapValues { (_, bundle) -> loadPatches(bundle, packageName) }
}
val patchList = selectedPatches.flatMap { (bundle, selected) -> val patchList = selectedPatches.flatMap { (bundle, selected) ->
allPatches[bundle]?.filter { selected.contains(it.name) } allPatches[bundle]?.filter { selected.contains(it.name) }

View file

@ -150,11 +150,8 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
outputFile = outputFile, outputFile = outputFile,
enableMultithrededDexWriter = enableMultithreadedDexWriter(), enableMultithrededDexWriter = enableMultithreadedDexWriter(),
configurations = selectedPatches.map { (id, patches) -> configurations = selectedPatches.map { (id, patches) ->
val bundle = bundles[id]!!
PatchConfiguration( PatchConfiguration(
bundle.patchesJar.absolutePath, bundles[id]!!,
bundle.integrations?.absolutePath,
patches, patches,
options[id].orEmpty() options[id].orEmpty()
) )

View file

@ -1,6 +1,7 @@
package app.revanced.manager.patcher.runtime.process package app.revanced.manager.patcher.runtime.process
import android.os.Parcelable import android.os.Parcelable
import app.revanced.manager.patcher.patch.PatchBundle
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue import kotlinx.parcelize.RawValue
@ -18,8 +19,7 @@ data class Parameters(
@Parcelize @Parcelize
data class PatchConfiguration( data class PatchConfiguration(
val bundlePath: String, val bundle: PatchBundle,
val integrationsPath: String?,
val patches: Set<String>, val patches: Set<String>,
val options: @RawValue Map<String, Map<String, Any?>> val options: @RawValue Map<String, Map<String, Any?>>
) : Parcelable ) : Parcelable

View file

@ -9,7 +9,7 @@ import app.revanced.manager.BuildConfig
import app.revanced.manager.patcher.Session import app.revanced.manager.patcher.Session
import app.revanced.manager.patcher.logger.LogLevel import app.revanced.manager.patcher.logger.LogLevel
import app.revanced.manager.patcher.logger.Logger 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.patcher.runtime.ProcessRuntime
import app.revanced.manager.ui.model.State import app.revanced.manager.ui.model.State
import kotlinx.coroutines.CoroutineExceptionHandler 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") logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB")
val integrations = val integrations =
parameters.configurations.mapNotNull { it.integrationsPath?.let(::File) } parameters.configurations.mapNotNull { it.bundle.integrations }
val patchList = parameters.configurations.flatMap { config -> val patchBundleLoader = PatchBundleLoader(parameters.configurations.map { it.bundle })
val bundle = PatchBundle(File(config.bundlePath), null)
val patchList = parameters.configurations.flatMap { config ->
val patches = 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 } .associateBy { it.name }
config.options.forEach { (patchName, opts) -> config.options.forEach { (patchName, opts) ->

View file

@ -32,27 +32,25 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun BundleInformationDialog( fun BundleInformationDialog(
bundle: PatchBundleSource,
patchCount: Int,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onDeleteRequest: () -> Unit, onDeleteRequest: () -> Unit,
bundle: PatchBundleSource,
onRefreshButton: () -> Unit, onRefreshButton: () -> Unit,
) { ) {
val composableScope = rememberCoroutineScope() val composableScope = rememberCoroutineScope()
var viewCurrentBundlePatches by remember { mutableStateOf(false) } var viewCurrentBundlePatches by remember { mutableStateOf(false) }
val isLocal = bundle is LocalPatchBundle 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) { val props by remember(bundle) {
bundle.propsOrNullFlow() bundle.propsOrNullFlow()
}.collectAsStateWithLifecycle(null) }.collectAsStateWithLifecycle(null)
if (viewCurrentBundlePatches) { if (viewCurrentBundlePatches) {
BundlePatchesDialog( BundlePatchesDialog(
bundle = bundle,
onDismissRequest = { onDismissRequest = {
viewCurrentBundlePatches = false viewCurrentBundlePatches = false
}, }
bundle = bundle,
) )
} }
@ -110,7 +108,7 @@ fun BundleInformationDialog(
}, },
onPatchesClick = { onPatchesClick = {
viewCurrentBundlePatches = true viewCurrentBundlePatches = true
}, }
) )
} }
} }

View file

@ -34,12 +34,13 @@ import kotlinx.coroutines.flow.map
@Composable @Composable
fun BundleItem( fun BundleItem(
bundle: PatchBundleSource, bundle: PatchBundleSource,
onDelete: () -> Unit, patchCount: Int,
onUpdate: () -> Unit,
selectable: Boolean, selectable: Boolean,
onSelect: () -> Unit,
isBundleSelected: Boolean, isBundleSelected: Boolean,
toggleSelection: (Boolean) -> Unit, toggleSelection: (Boolean) -> Unit,
onDelete: () -> Unit,
onUpdate: () -> Unit,
onSelect: () -> Unit,
) { ) {
var viewBundleDialogPage by rememberSaveable { mutableStateOf(false) } var viewBundleDialogPage by rememberSaveable { mutableStateOf(false) }
val state by bundle.state.collectAsStateWithLifecycle() val state by bundle.state.collectAsStateWithLifecycle()
@ -50,12 +51,13 @@ fun BundleItem(
if (viewBundleDialogPage) { if (viewBundleDialogPage) {
BundleInformationDialog( BundleInformationDialog(
bundle = bundle,
patchCount = patchCount,
onDismissRequest = { viewBundleDialogPage = false }, onDismissRequest = { viewBundleDialogPage = false },
onDeleteRequest = { onDeleteRequest = {
viewBundleDialogPage = false viewBundleDialogPage = false
onDelete() onDelete()
}, },
bundle = bundle,
onRefreshButton = onUpdate, onRefreshButton = onUpdate,
) )
} }
@ -79,9 +81,7 @@ fun BundleItem(
headlineContent = { Text(text = bundle.name) }, headlineContent = { Text(text = bundle.name) },
supportingContent = { 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 = { trailingContent = {
Row { Row {
@ -89,7 +89,7 @@ fun BundleItem(
when (state) { when (state) {
is PatchBundleSource.State.Failed -> Icons.Outlined.ErrorOutline to R.string.bundle_error 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.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 androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.domain.bundles.PatchBundleSource 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.LazyColumnWithScrollbar
import app.revanced.manager.ui.component.NotificationCard import app.revanced.manager.ui.component.NotificationCard
import kotlinx.coroutines.flow.mapNotNull
import org.koin.compose.koinInject
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun BundlePatchesDialog( fun BundlePatchesDialog(
onDismissRequest: () -> Unit,
bundle: PatchBundleSource, bundle: PatchBundleSource,
onDismissRequest: () -> Unit,
) { ) {
var informationCardVisible by remember { mutableStateOf(true) } 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( Dialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
@ -75,29 +81,28 @@ fun BundlePatchesDialog(
} }
} }
state.patchBundleOrNull()?.let { bundle -> items(patches.size) { index ->
items(bundle.patches.size) { bundleIndex -> val patch = patches[index]
val patch = bundle.patches[bundleIndex]
ListItem( ListItem(
headlineContent = { headlineContent = {
Text(
text = patch.name,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface
)
},
supportingContent = {
patch.description?.let {
Text( Text(
text = patch.name, text = it,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface 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 onAppClick: (InstalledApp) -> Unit
) { ) {
val bundlesSelectable by remember { derivedStateOf { vm.selectedSources.size > 0 } } 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 androidContext = LocalContext.current
val composableScope = rememberCoroutineScope() val composableScope = rememberCoroutineScope()
val pagerState = rememberPagerState( val pagerState = rememberPagerState(
@ -269,6 +269,9 @@ fun DashboardScreen(
} }
val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList()) val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList())
val patchCounts by vm.bundlePatchCountsFlow.collectAsStateWithLifecycle(
initialValue = emptyMap()
)
Column( Column(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@ -276,23 +279,24 @@ fun DashboardScreen(
sources.forEach { sources.forEach {
BundleItem( BundleItem(
bundle = it, bundle = it,
onDelete = { patchCount = patchCounts[it.uid] ?: 0,
vm.delete(it) isBundleSelected = vm.selectedSources.contains(it),
},
onUpdate = {
vm.update(it)
},
selectable = bundlesSelectable, selectable = bundlesSelectable,
onSelect = { onSelect = {
vm.selectedSources.add(it) vm.selectedSources.add(it)
}, },
isBundleSelected = vm.selectedSources.contains(it),
toggleSelection = { bundleIsNotSelected -> toggleSelection = { bundleIsNotSelected ->
if (bundleIsNotSelected) { if (bundleIsNotSelected) {
vm.selectedSources.add(it) vm.selectedSources.add(it)
} else { } else {
vm.selectedSources.remove(it) vm.selectedSources.remove(it)
} }
},
onDelete = {
vm.delete(it)
},
onUpdate = {
vm.update(it)
} }
) )
} }

View file

@ -224,13 +224,13 @@ fun PatchesSelectorScreen(
patchList( patchList(
uid = bundle.uid, uid = bundle.uid,
patches = bundle.supported.searched(), patches = bundle.supportedPatches.searched(),
filterFlag = SHOW_SUPPORTED, filterFlag = SHOW_SUPPORTED,
supported = true supported = true
) )
patchList( patchList(
uid = bundle.uid, uid = bundle.uid,
patches = bundle.universal.searched(), patches = bundle.universalPatches.searched(),
filterFlag = SHOW_UNIVERSAL, filterFlag = SHOW_UNIVERSAL,
supported = true supported = true
) { ) {
@ -242,13 +242,13 @@ fun PatchesSelectorScreen(
if (!vm.allowIncompatiblePatches) return@LazyColumnWithScrollbar if (!vm.allowIncompatiblePatches) return@LazyColumnWithScrollbar
patchList( patchList(
uid = bundle.uid, uid = bundle.uid,
patches = bundle.unsupported.searched(), patches = bundle.unsupportedPatches.searched(),
filterFlag = SHOW_UNSUPPORTED, filterFlag = SHOW_UNSUPPORTED,
supported = true supported = true
) { ) {
ListHeader( ListHeader(
title = stringResource(R.string.unsupported_patches), title = stringResource(R.string.unsupported_patches),
onHelpClick = { vm.openUnsupportedDialog(bundle.unsupported) } onHelpClick = { vm.openUnsupportedDialog(bundle.unsupportedPatches) }
) )
} }
} }
@ -332,13 +332,13 @@ fun PatchesSelectorScreen(
) { ) {
patchList( patchList(
uid = bundle.uid, uid = bundle.uid,
patches = bundle.supported, patches = bundle.supportedPatches,
filterFlag = SHOW_SUPPORTED, filterFlag = SHOW_SUPPORTED,
supported = true supported = true
) )
patchList( patchList(
uid = bundle.uid, uid = bundle.uid,
patches = bundle.universal, patches = bundle.universalPatches,
filterFlag = SHOW_UNIVERSAL, filterFlag = SHOW_UNIVERSAL,
supported = true supported = true
) { ) {
@ -348,13 +348,13 @@ fun PatchesSelectorScreen(
} }
patchList( patchList(
uid = bundle.uid, uid = bundle.uid,
patches = bundle.unsupported, patches = bundle.unsupportedPatches,
filterFlag = SHOW_UNSUPPORTED, filterFlag = SHOW_UNSUPPORTED,
supported = vm.allowIncompatiblePatches supported = vm.allowIncompatiblePatches
) { ) {
ListHeader( ListHeader(
title = stringResource(R.string.unsupported_patches), 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.AppInfo
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.destination.SelectedAppInfoDestination 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.model.SelectedApp
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
@ -53,7 +52,7 @@ fun SelectedAppInfoScreen(
val packageName = vm.selectedApp.packageName val packageName = vm.selectedApp.packageName
val version = vm.selectedApp.version val version = vm.selectedApp.version
val bundles by remember(packageName, version) { val bundles by remember(packageName, version) {
vm.bundlesRepo.bundleInfoFlow(packageName, version) vm.bundlesRepo.scopedBundleInfoFlow(packageName, version)
}.collectAsStateWithLifecycle(initialValue = emptyList()) }.collectAsStateWithLifecycle(initialValue = emptyList())
val allowIncompatiblePatches by vm.prefs.disablePatchVersionCompatCheck.getAsState() val allowIncompatiblePatches by vm.prefs.disablePatchVersionCompatCheck.getAsState()
@ -69,7 +68,7 @@ fun SelectedAppInfoScreen(
} }
val availablePatchCount by remember { val availablePatchCount by remember {
derivedStateOf { derivedStateOf {
bundles.sumOf { it.patchCount } bundles.sumOf { it.patches.size }
} }
} }

View file

@ -30,13 +30,12 @@ class DashboardViewModel(
private val networkInfo: NetworkInfo, private val networkInfo: NetworkInfo,
val prefs: PreferencesManager val prefs: PreferencesManager
) : ViewModel() { ) : ViewModel() {
val availablePatches = val bundlePatchCountsFlow = patchBundleRepository.bundleInfoFlow.map { it.mapValues { (_, bundle) -> bundle.patches.size } }
patchBundleRepository.bundles.map { it.values.sumOf { bundle -> bundle.patches.size } } val availablePatchesCountFlow = bundlePatchCountsFlow.map { it.values.sum() }
private val contentResolver: ContentResolver = app.contentResolver private val contentResolver: ContentResolver = app.contentResolver
val sources = patchBundleRepository.sources val sources = patchBundleRepository.sources
val selectedSources = mutableStateListOf<PatchBundleSource>() val selectedSources = mutableStateListOf<PatchBundleSource>()
var updatedManagerVersion: String? by mutableStateOf(null) var updatedManagerVersion: String? by mutableStateOf(null)
private set private set
@ -76,10 +75,10 @@ class DashboardViewModel(
} }
} }
fun cancelSourceSelection() { fun cancelSourceSelection() {
selectedSources.clear() selectedSources.clear()
} }
fun createLocalSource(name: String, patchBundle: Uri, integrations: Uri?) = fun createLocalSource(name: String, patchBundle: Uri, integrations: Uri?) =
viewModelScope.launch { viewModelScope.launch {
contentResolver.openInputStream(patchBundle)!!.use { patchesStream -> 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.R
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchBundleRepository 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.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.ui.model.SelectedApp
import app.revanced.manager.util.Options import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.PatchSelection
@ -55,7 +54,7 @@ class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent {
val allowIncompatiblePatches = val allowIncompatiblePatches =
get<PreferencesManager>().disablePatchVersionCompatCheck.getBlocking() get<PreferencesManager>().disablePatchVersionCompatCheck.getBlocking()
val bundlesFlow = val bundlesFlow =
get<PatchBundleRepository>().bundleInfoFlow(packageName, input.app.version) get<PatchBundleRepository>().scopedBundleInfoFlow(packageName, input.app.version)
init { init {
viewModelScope.launch { viewModelScope.launch {
@ -64,11 +63,11 @@ class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent {
return@launch return@launch
} }
fun BundleInfo.hasDefaultPatches() = fun PatchBundleInfo.Scoped.hasDefaultPatches() =
patchSequence(allowIncompatiblePatches).any { it.include } patchSequence(allowIncompatiblePatches).any { it.include }
// Don't show the warning if there are no default patches. // 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() 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 -> bundle.patchSequence(allowIncompatiblePatches).any { patch ->
isSelected(bundle.uid, 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.PatchBundleRepository
import app.revanced.manager.domain.repository.PatchOptionsRepository import app.revanced.manager.domain.repository.PatchOptionsRepository
import app.revanced.manager.domain.repository.PatchSelectionRepository import app.revanced.manager.domain.repository.PatchSelectionRepository
import app.revanced.manager.ui.model.BundleInfo import app.revanced.manager.patcher.patch.PatchBundleInfo
import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection
import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.util.Options import app.revanced.manager.util.Options
import app.revanced.manager.util.PM import app.revanced.manager.util.PM
@ -101,13 +101,13 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
selectedAppInfo = info 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) selectionState.patches(bundles, allowUnsupported)
fun getCustomPatches( fun getCustomPatches(
bundles: List<BundleInfo>, bundles: List<PatchBundleInfo.Scoped>,
allowUnsupported: Boolean allowUnsupported: Boolean
): PatchSelection? = ): PatchSelection? =
(selectionState as? SelectionState.Customized)?.patches(bundles, allowUnsupported) (selectionState as? SelectionState.Customized)?.patches(bundles, allowUnsupported)
@ -115,7 +115,7 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
fun updateConfiguration( fun updateConfiguration(
selection: PatchSelection?, selection: PatchSelection?,
options: Options, options: Options,
bundles: List<BundleInfo> bundles: List<PatchBundleInfo.Scoped>
) { ) {
selectionState = selection?.let(SelectionState::Customized) ?: SelectionState.Default 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. * 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 -> bundles.forEach bundles@{ bundle ->
val bundleOptions = this@filtered[bundle.uid] ?: return@bundles 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@{ this@options[bundle.uid] = buildMap bundleOptions@{
bundleOptions.forEach patch@{ (patchName, values) -> bundleOptions.forEach patch@{ (patchName, values) ->
@ -165,11 +165,11 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
} }
private sealed interface SelectionState : Parcelable { private sealed interface SelectionState : Parcelable {
fun patches(bundles: List<BundleInfo>, allowUnsupported: Boolean): PatchSelection fun patches(bundles: List<PatchBundleInfo.Scoped>, allowUnsupported: Boolean): PatchSelection
@Parcelize @Parcelize
data class Customized(val patchSelection: PatchSelection) : SelectionState { 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( bundles.toPatchSelection(
allowUnsupported allowUnsupported
) { uid, patch -> ) { uid, patch ->
@ -179,7 +179,7 @@ private sealed interface SelectionState : Parcelable {
@Parcelize @Parcelize
data object Default : SelectionState { 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 } bundles.toPatchSelection(allowUnsupported) { _, patch -> patch.include }
} }
} }

View file

@ -64,37 +64,38 @@ class VersionSelectorViewModel(
patchBundleRepository.suggestedVersions.first()[packageName] patchBundleRepository.suggestedVersions.first()[packageName]
} }
val supportedVersions = patchBundleRepository.bundles.map supportedVersions@{ bundles -> val supportedVersions =
requiredVersionAsync.await()?.let { version -> patchBundleRepository.bundleInfoFlow.map supportedVersions@{ bundles ->
// It is mandatory to use the suggested version if the safeguard is enabled. requiredVersionAsync.await()?.let { version ->
return@supportedVersions mapOf( // It is mandatory to use the suggested version if the safeguard is enabled.
version to bundles return@supportedVersions mapOf(
.asSequence() version to bundles
.flatMap { (_, bundle) -> bundle.patches } .asSequence()
.flatMap { it.compatiblePackages.orEmpty() } .flatMap { (_, bundle) -> bundle.patches }
.filter { it.packageName == packageName } .flatMap { it.compatiblePackages.orEmpty() }
.count { it.versions.isNullOrEmpty() || version in it.versions } .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() }
} }
}.groupingBy { it }
.eachCount() var patchesWithoutVersions = 0
.toMutableMap()
.apply { bundles.flatMap { (_, bundle) ->
replaceAll { _, count -> bundle.patches.flatMap { patch ->
count + patchesWithoutVersions patch.compatiblePackages.orEmpty()
.filter { it.packageName == packageName }
.onEach { if (it.versions == null) patchesWithoutVersions++ }
.flatMap { it.versions.orEmpty() }
} }
} }.groupingBy { it }
}.flowOn(Dispatchers.Default) .eachCount()
.toMutableMap()
.apply {
replaceAll { _, count ->
count + patchesWithoutVersions
}
}
}.flowOn(Dispatchers.Default)
init { init {
viewModelScope.launch { viewModelScope.launch {

View file

@ -43,10 +43,10 @@ class PM(
) { ) {
private val scope = CoroutineScope(Dispatchers.IO) private val scope = CoroutineScope(Dispatchers.IO)
val appList = patchBundleRepository.bundles.map { bundles -> val appList = patchBundleRepository.bundleInfoFlow.map { bundles ->
val compatibleApps = scope.async { val compatibleApps = scope.async {
val compatiblePackages = bundles.values val compatiblePackages = bundles
.flatMap { it.patches } .flatMap { (_, bundle) -> bundle.patches }
.flatMap { it.compatiblePackages.orEmpty() } .flatMap { it.compatiblePackages.orEmpty() }
.groupingBy { it.packageName } .groupingBy { it.packageName }
.eachCount() .eachCount()
@ -80,7 +80,7 @@ class PM(
(compatibleApps.await() + installedApps.await()) (compatibleApps.await() + installedApps.await())
.distinctBy { it.packageName } .distinctBy { it.packageName }
.sortedWith( .sortedWith(
compareByDescending<AppInfo>{ compareByDescending<AppInfo> {
it.packageInfo != null && (it.patches ?: 0) > 0 it.packageInfo != null && (it.patches ?: 0) > 0
}.thenByDescending { }.thenByDescending {
it.patches it.patches

View file

@ -16,6 +16,7 @@ collection = "0.3.7"
room-version = "2.6.1" room-version = "2.6.1"
revanced-patcher = "19.3.1" revanced-patcher = "19.3.1"
revanced-library = "2.2.1" revanced-library = "2.2.1"
revanced-multidexlib2 = "3.0.3.r3"
koin-version = "3.5.3" koin-version = "3.5.3"
koin-version-compose = "3.5.3" koin-version-compose = "3.5.3"
reimagined-navigation = "1.5.0" reimagined-navigation = "1.5.0"
@ -77,6 +78,7 @@ room-compiler = { group = "androidx.room", name = "room-compiler", version.ref =
# Patcher # Patcher
revanced-patcher = { group = "app.revanced", name = "revanced-patcher", version.ref = "revanced-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-library = { group = "app.revanced", name = "revanced-library", version.ref = "revanced-library" }
revanced-multidexlib2 = { group = "app.revanced", name = "multidexlib2", version.ref = "revanced-multidexlib2" }
# Koin # Koin
koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin-version" } koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin-version" }