Skip to content

Commit f30f233

Browse files
committed
feat: allow bundles to use classes from other bundles
1 parent 44b5f7b commit f30f233

26 files changed

+392
-267
lines changed

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ dependencies {
149149
// ReVanced
150150
implementation(libs.revanced.patcher)
151151
implementation(libs.revanced.library)
152+
implementation(libs.revanced.multidexlib2)
152153

153154
// Native processes
154155
implementation(libs.kotlin.process)

app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@ class LocalPatchBundle(name: String, id: Int, directory: File) : PatchBundleSour
2020
}
2121
}
2222

23-
reload()
23+
refresh()
2424
}
2525
}

app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
package app.revanced.manager.domain.bundles
22

3-
import android.util.Log
43
import androidx.compose.runtime.Stable
54
import app.revanced.manager.patcher.patch.PatchBundle
6-
import app.revanced.manager.util.tag
75
import kotlinx.coroutines.flow.MutableStateFlow
86
import kotlinx.coroutines.flow.asStateFlow
97
import kotlinx.coroutines.flow.flowOf
@@ -18,7 +16,7 @@ sealed class PatchBundleSource(val name: String, val uid: Int, directory: File)
1816
protected val patchesFile = directory.resolve("patches.jar")
1917
protected val integrationsFile = directory.resolve("integrations.apk")
2018

21-
private val _state = MutableStateFlow(load())
19+
private val _state = MutableStateFlow(getPatchBundle())
2220
val state = _state.asStateFlow()
2321

2422
/**
@@ -36,27 +34,24 @@ sealed class PatchBundleSource(val name: String, val uid: Int, directory: File)
3634
}
3735
}
3836

39-
private fun load(): State {
40-
if (!hasInstalled()) return State.Missing
37+
private fun getPatchBundle() =
38+
if (!hasInstalled()) State.Missing
39+
else State.Available(PatchBundle(patchesFile, integrationsFile.takeIf(File::exists)))
4140

42-
return try {
43-
State.Loaded(PatchBundle(patchesFile, integrationsFile.takeIf(File::exists)))
44-
} catch (t: Throwable) {
45-
Log.e(tag, "Failed to load patch bundle $name", t)
46-
State.Failed(t)
47-
}
41+
fun refresh() {
42+
_state.value = getPatchBundle()
4843
}
4944

50-
fun reload() {
51-
_state.value = load()
45+
fun markAsFailed(e: Throwable) {
46+
_state.value = State.Failed(e)
5247
}
5348

5449
sealed interface State {
5550
fun patchBundleOrNull(): PatchBundle? = null
5651

5752
data object Missing : State
5853
data class Failed(val throwable: Throwable) : State
59-
data class Loaded(val bundle: PatchBundle) : State {
54+
data class Available(val bundle: PatchBundle) : State {
6055
override fun patchBundleOrNull() = bundle
6156
}
6257
}

app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
4949
}
5050

5151
saveVersion(patches.version, integrations.version)
52-
reload()
52+
refresh()
5353
}
5454

5555
suspend fun downloadLatest() {
@@ -76,7 +76,7 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
7676

7777
suspend fun deleteLocalFiles() = withContext(Dispatchers.Default) {
7878
arrayOf(patchesFile, integrationsFile).forEach(File::delete)
79-
reload()
79+
refresh()
8080
}
8181

8282
fun propsFlow() = configRepository.getProps(uid)

app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@ import app.revanced.manager.domain.bundles.RemotePatchBundle
1515
import app.revanced.manager.domain.bundles.PatchBundleSource
1616
import app.revanced.manager.domain.manager.PreferencesManager
1717
import app.revanced.manager.patcher.patch.PatchInfo
18+
import app.revanced.manager.patcher.patch.PatchBundleInfo
19+
import app.revanced.manager.patcher.patch.PatchBundleLoader
1820
import app.revanced.manager.util.flatMapLatestAndCombine
1921
import app.revanced.manager.util.tag
2022
import app.revanced.manager.util.uiSafe
2123
import kotlinx.coroutines.Dispatchers
2224
import kotlinx.coroutines.coroutineScope
2325
import kotlinx.coroutines.flow.MutableStateFlow
2426
import kotlinx.coroutines.flow.first
27+
import kotlinx.coroutines.flow.flowOn
2528
import kotlinx.coroutines.flow.map
2629
import kotlinx.coroutines.flow.update
2730
import kotlinx.coroutines.launch
@@ -51,7 +54,46 @@ class PatchBundleRepository(
5154
it.state.map { state -> it.uid to state }
5255
}
5356

54-
val suggestedVersions = bundles.map {
57+
val bundleInfoFlow = sources.flatMapLatestAndCombine(
58+
transformer = { source ->
59+
source.state.map {
60+
source to it
61+
}
62+
},
63+
combiner = { states ->
64+
val patchBundleLoader by lazy {
65+
PatchBundleLoader(states.mapNotNull { (_, state) -> state.patchBundleOrNull() })
66+
}
67+
68+
states.mapNotNull { (source, state) ->
69+
val bundle = state.patchBundleOrNull() ?: return@mapNotNull null
70+
71+
try {
72+
source.uid to PatchBundleInfo.Global(
73+
source.name,
74+
source.uid,
75+
patchBundleLoader.loadMetadata(bundle)
76+
)
77+
} catch (t: Throwable) {
78+
Log.e(tag, "Failed to load patches from ${source.name}", t)
79+
source.markAsFailed(t)
80+
81+
null
82+
}
83+
}.toMap()
84+
}
85+
).flowOn(Dispatchers.Default)
86+
87+
fun scopedBundleInfoFlow(packageName: String, version: String) = bundleInfoFlow.map {
88+
it.map { (_, bundle) ->
89+
bundle.forPackage(
90+
packageName,
91+
version
92+
)
93+
}
94+
}
95+
96+
val suggestedVersions = bundleInfoFlow.map {
5597
val allPatches =
5698
it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet()
5799

Lines changed: 4 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,8 @@
11
package app.revanced.manager.patcher.patch
22

3-
import android.util.Log
4-
import app.revanced.manager.util.tag
5-
import app.revanced.patcher.PatchBundleLoader
6-
import app.revanced.patcher.patch.Patch
3+
import android.os.Parcelable
4+
import kotlinx.parcelize.Parcelize
75
import java.io.File
86

9-
class PatchBundle(val patchesJar: File, val integrations: File?) {
10-
private val loader = object : Iterable<Patch<*>> {
11-
private fun load(): Iterable<Patch<*>> {
12-
patchesJar.setReadOnly()
13-
return PatchBundleLoader.Dex(patchesJar, optimizedDexDirectory = null)
14-
}
15-
16-
override fun iterator(): Iterator<Patch<*>> = load().iterator()
17-
}
18-
19-
init {
20-
Log.d(tag, "Loaded patch bundle: $patchesJar")
21-
}
22-
23-
/**
24-
* A list containing the metadata of every patch inside this bundle.
25-
*/
26-
val patches = loader.map(::PatchInfo)
27-
28-
/**
29-
* Load all patches compatible with the specified package.
30-
*/
31-
fun patchClasses(packageName: String) = loader.filter { patch ->
32-
val compatiblePackages = patch.compatiblePackages
33-
?: // The patch has no compatibility constraints, which means it is universal.
34-
return@filter true
35-
36-
if (!compatiblePackages.any { it.name == packageName }) {
37-
// Patch is not compatible with this package.
38-
return@filter false
39-
}
40-
41-
true
42-
}
43-
}
7+
@Parcelize
8+
data class PatchBundle(val patchesJar: File, val integrations: File?) : Parcelable
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package app.revanced.manager.patcher.patch
2+
3+
import app.revanced.manager.util.PatchSelection
4+
5+
/**
6+
* A base class for storing [PatchBundle] metadata.
7+
*
8+
* @param name The name of the bundle.
9+
* @param uid The unique ID of the bundle.
10+
* @param patches The patch list.
11+
*/
12+
sealed class PatchBundleInfo(val name: String, val uid: Int, val patches: List<PatchInfo>) {
13+
/**
14+
* Information about a bundle and all the patches it contains.
15+
*
16+
* @see [PatchBundleInfo]
17+
*/
18+
class Global(name: String, uid: Int, patches: List<PatchInfo>) :
19+
PatchBundleInfo(name, uid, patches) {
20+
21+
/**
22+
* Create a [PatchBundleInfo.Scoped] that only contains information about patches that are relevant for a specific [packageName].
23+
*/
24+
fun forPackage(packageName: String, version: String): Scoped {
25+
val relevantPatches = patches.filter { it.compatibleWith(packageName) }
26+
val supported = mutableListOf<PatchInfo>()
27+
val unsupported = mutableListOf<PatchInfo>()
28+
val universal = mutableListOf<PatchInfo>()
29+
30+
relevantPatches.forEach {
31+
val targetList = when {
32+
it.compatiblePackages == null -> universal
33+
it.supportsVersion(
34+
packageName,
35+
version
36+
) -> supported
37+
38+
else -> unsupported
39+
}
40+
41+
targetList.add(it)
42+
}
43+
44+
return Scoped(name, uid, relevantPatches, supported, unsupported, universal)
45+
}
46+
}
47+
48+
/**
49+
* Contains information about a bundle that is relevant for a specific package name.
50+
*
51+
* @param supportedPatches Patches that are compatible with the specified package name and version.
52+
* @param unsupportedPatches Patches that are compatible with the specified package name but not version.
53+
* @param universalPatches Patches that are compatible with all packages.
54+
* @see [PatchBundleInfo.Global.forPackage]
55+
* @see [PatchBundleInfo]
56+
*/
57+
class Scoped(
58+
name: String,
59+
uid: Int,
60+
patches: List<PatchInfo>,
61+
val supportedPatches: List<PatchInfo>,
62+
val unsupportedPatches: List<PatchInfo>,
63+
val universalPatches: List<PatchInfo>
64+
) : PatchBundleInfo(name, uid, patches) {
65+
fun patchSequence(allowUnsupported: Boolean) = if (allowUnsupported) {
66+
patches.asSequence()
67+
} else {
68+
sequence {
69+
yieldAll(supportedPatches)
70+
yieldAll(universalPatches)
71+
}
72+
}
73+
}
74+
75+
companion object Extensions {
76+
inline fun Iterable<Scoped>.toPatchSelection(
77+
allowUnsupported: Boolean,
78+
condition: (Int, PatchInfo) -> Boolean
79+
): PatchSelection = this.associate { bundle ->
80+
val patches =
81+
bundle.patchSequence(allowUnsupported)
82+
.mapNotNullTo(mutableSetOf()) { patch ->
83+
patch.name.takeIf {
84+
condition(
85+
bundle.uid,
86+
patch
87+
)
88+
}
89+
}
90+
91+
bundle.uid to patches
92+
}
93+
}
94+
}

0 commit comments

Comments
 (0)