mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2024-11-10 01:01:56 +01:00
feat: allow bundles to use classes from other bundles
This commit is contained in:
parent
413083c58d
commit
8b9314078c
26 changed files with 393 additions and 268 deletions
|
@ -149,6 +149,7 @@ dependencies {
|
|||
// ReVanced
|
||||
implementation(libs.revanced.patcher)
|
||||
implementation(libs.revanced.library)
|
||||
implementation(libs.revanced.multidexlib2)
|
||||
|
||||
// Native processes
|
||||
implementation(libs.kotlin.process)
|
||||
|
|
|
@ -20,6 +20,6 @@ class LocalPatchBundle(name: String, id: Int, directory: File) : PatchBundleSour
|
|||
}
|
||||
}
|
||||
|
||||
reload()
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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) }
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
|
|
|
@ -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
|
|
@ -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) ->
|
||||
|
|
|
@ -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
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
},
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,9 +81,9 @@ fun BundlePatchesDialog(
|
|||
}
|
||||
}
|
||||
|
||||
state.patchBundleOrNull()?.let { bundle ->
|
||||
items(bundle.patches.size) { bundleIndex ->
|
||||
val patch = bundle.patches[bundleIndex]
|
||||
items(patches.size) { index ->
|
||||
val patch = patches[index]
|
||||
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
|
@ -101,5 +107,4 @@ fun BundlePatchesDialog(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package app.revanced.manager.ui.model
|
||||
|
||||
enum class BundleType {
|
||||
Local,
|
||||
Remote
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,7 +64,8 @@ class VersionSelectorViewModel(
|
|||
patchBundleRepository.suggestedVersions.first()[packageName]
|
||||
}
|
||||
|
||||
val supportedVersions = patchBundleRepository.bundles.map supportedVersions@{ bundles ->
|
||||
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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" }
|
||||
|
|
Loading…
Reference in a new issue