Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow bundles to use classes from other bundles #1951

Draft
wants to merge 1 commit into
base: compose-dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ dependencies {
// ReVanced
implementation(libs.revanced.patcher)
implementation(libs.revanced.library)
implementation(libs.revanced.multidexlib2)

// Native processes
implementation(libs.kotlin.process)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ class LocalPatchBundle(name: String, id: Int, directory: File) : PatchBundleSour
}
}

reload()
refresh()
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()

/**
Expand All @@ -36,27 +34,24 @@ 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 {
fun patchBundleOrNull(): PatchBundle? = null

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
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ 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
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
Expand Down Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Loading
Loading