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(YouTube - GmsCore support): Check for availability by adding a patch to add resources without needing to rely on a settings patch #2568

Closed
wants to merge 7 commits into from
494 changes: 281 additions & 213 deletions api/revanced-patches.api

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import app.revanced.patcher.PatchBundleLoader
import app.revanced.patcher.PatchSet
import java.io.File

internal interface PatchesFileGenerator {
internal interface IPatchesFileGenerator {
fun generate(patches: PatchSet)

private companion object {
Expand All @@ -14,7 +14,7 @@ internal interface PatchesFileGenerator {
).also { loader ->
if (loader.isEmpty()) throw IllegalStateException("No patches found")
}.let { bundle ->
arrayOf(JsonGenerator()).forEach { generator -> generator.generate(bundle) }
arrayOf(JsonPatchesFileGenerator()).forEach { generator -> generator.generate(bundle) }
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import app.revanced.patcher.patch.Patch
import com.google.gson.GsonBuilder
import java.io.File

internal class JsonGenerator : PatchesFileGenerator {
internal class JsonPatchesFileGenerator : IPatchesFileGenerator {
override fun generate(patches: PatchSet) = patches.map {
JsonPatch(
it.name!!,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package app.revanced.patches.all.connectivity.wifi.spoof

import app.revanced.patcher.patch.annotation.Patch
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patches.all.misc.transformation.AbstractTransformInstructionsPatch
import app.revanced.patches.all.misc.transformation.BaseTransformInstructionsPatch
import app.revanced.patches.all.misc.transformation.IMethodCall
import app.revanced.patches.all.misc.transformation.Instruction35cInfo
import app.revanced.patches.all.misc.transformation.filterMapInstruction35c
Expand All @@ -17,7 +17,7 @@ import com.android.tools.smali.dexlib2.iface.instruction.Instruction
requiresIntegrations = true
)
@Suppress("unused")
object SpoofWifiPatch : AbstractTransformInstructionsPatch<Instruction35cInfo>() {
object SpoofWifiPatch : BaseTransformInstructionsPatch<Instruction35cInfo>() {
private const val INTEGRATIONS_CLASS_DESCRIPTOR_PREFIX = "Lapp/revanced/integrations/all/connectivity/wifi/spoof/SpoofWifiPatch"
private const val INTEGRATIONS_CLASS_DESCRIPTOR = "$INTEGRATIONS_CLASS_DESCRIPTOR_PREFIX;"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,22 +41,22 @@ object ChangePackageNamePatch : ResourcePatch(), Closeable {
* @throws PatchOptionException.ValueValidationException If the package name is invalid.
*/
fun setOrGetFallbackPackageName(fallbackPackageName: String): String {
val packageName = this.packageNameOption.value!!
val packageName = packageNameOption.value!!

return if (packageName == this.packageNameOption.default)
fallbackPackageName.also { this.packageNameOption.value = it }
return if (packageName == packageNameOption.default)
fallbackPackageName.also { packageNameOption.value = it }
else
packageName
}

override fun close() = context.xmlEditor["AndroidManifest.xml"].use { editor ->
val manifest = editor.file.getElementsByTagName("manifest").item(0) as Element
val originalPackageName = manifest.getAttribute("package")

var replacementPackageName = this.packageNameOption.value
if (replacementPackageName == this.packageNameOption.default)
replacementPackageName = "$originalPackageName.revanced"
val replacementPackageName = packageNameOption.value

manifest.setAttribute("package", replacementPackageName)
val manifest = editor.file.getElementsByTagName("manifest").item(0) as Element
manifest.setAttribute(
"package",
if (replacementPackageName != packageNameOption.default) replacementPackageName
else "${manifest.getAttribute("package")}.revanced"
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
package app.revanced.patches.all.misc.resources

import app.revanced.patcher.PatchClass
import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.ResourcePatch
import app.revanced.patcher.patch.annotation.Patch
import app.revanced.patcher.util.DomFileEditor
import app.revanced.patches.all.misc.resources.AddResourcesPatch.resources
import app.revanced.util.*
import app.revanced.util.resource.ArrayResource
import app.revanced.util.resource.BaseResource
import app.revanced.util.resource.StringResource
import org.w3c.dom.Node
import java.io.Closeable
import java.util.*

/**
* An identifier of an app. For example, `youtube`.
*/
private typealias AppId = String
/**
* An identifier of a patch. For example, `ad.general.HideAdsPatch`.
*/
private typealias PatchId = String

/**
* A set of resources of a patch.
*/
private typealias PatchResources = MutableSet<BaseResource>
/**
* A map of resources belonging to a patch.
*/
private typealias AppResources = MutableMap<PatchId, PatchResources>
/**
* A map of resources belonging to an app.
*/
private typealias Resources = MutableMap<AppId, AppResources>

/**
* The value of a resource.
* For example, `values` or `values-de`.
*/
private typealias Value = String

@Patch(description = "Add resources such as strings or arrays to the app.")
object AddResourcesPatch : ResourcePatch(), MutableMap<Value, MutableSet<BaseResource>> by mutableMapOf(), Closeable {
private lateinit var context: ResourceContext

/**
* A map of all resources associated by their value staged by [execute].
*/
private lateinit var resources: Map<Value, Resources>

/*
The strategy of this patch is to stage resources present in `/resources/addresources`.
These resources are organized by their respective value and patch.

On AddResourcesPatch#execute, all resources are staged in a temporary map.
After that, other patches that depend on AddResourcesPatch can call
AddResourcesPatch#invoke(PatchClass) to stage resources belonging to that patch
from the temporary map to AddResourcesPatch.

After all patches that depend on AddResourcesPatch have been executed,
AddResourcesPatch#close is finally called to add all staged resources to the app.
*/
override fun execute(context: ResourceContext) {
this.context = context

resources = buildMap {
/**
* Puts resources under `/resources/addresources/<value>/<resourceKind>.xml` into the map.
*
* @param value The value of the resource. For example, `values` or `values-de`.
* @param resourceKind The kind of the resource. For example, `strings` or `arrays`.
* @param transform A function that transforms the [Node]s from the XML files to a [BaseResource].
*/
fun addResources(
value: Value,
resourceKind: String,
transform: (Node) -> BaseResource,
) {
inputStreamFromBundledResource(
"addresources",
"$value/$resourceKind.xml"
)?.let { stream ->
// Add the resources associated with the given value to the map,
// instead of overwriting it.
// This covers the example case such as adding strings and arrays of the same value.
getOrPut(value, ::mutableMapOf).apply {
context.xmlEditor[stream].use {
it.file.getElementsByTagName("app").asSequence().forEach { app ->
val appId = app.attributes.getNamedItem("id").textContent

getOrPut(appId, ::mutableMapOf).apply {
app.forEachChildElement { patch ->
val patchId = patch.attributes.getNamedItem("id").textContent

getOrPut(patchId, ::mutableSetOf).apply {
patch.forEachChildElement { resourceNode ->
val resource = transform(resourceNode)

add(resource)
}
}
}
}
}
}
}
}
}

// Stage all resources to a temporary map.
// Staged resources consumed by AddResourcesPatch#invoke(PatchClass)
// are later used in AddResourcesPatch#close.
try {
val addStringResources = { value: Value ->
addResources(value, "strings", StringResource::fromNode)
}
Locale.getISOLanguages().asSequence().map { "values-$it" }.forEach { addStringResources(it) }
addStringResources("values")

addResources("values", "arrays", ArrayResource::fromNode)
} catch (e: Exception) {
throw PatchException("Failed to read resources", e)
}
}
}

/**
* Adds a [BaseResource] to the map using [MutableMap.getOrPut].
*
* @param value The value of the resource. For example, `values` or `values-de`.
* @param resource The resource to add.
*
* @return True if the resource was added, false if it already existed.
*/
operator fun invoke(value: Value, resource: BaseResource) =
getOrPut(value, ::mutableSetOf).add(resource)

/**
* Adds a list of [BaseResource]s to the map using [MutableMap.getOrPut].
*
* @param value The value of the resource. For example, `values` or `values-de`.
* @param resources The resources to add.
*
* @return True if the resources were added, false if they already existed.
*/
operator fun invoke(value: Value, resources: Iterable<BaseResource>) =
getOrPut(value, ::mutableSetOf).addAll(resources)

/**
* Adds a [StringResource].
*
* @param name The name of the string resource.
* @param value The value of the string resource.
* @param formatted Whether the string resource is formatted. Defaults to `true`.
* @param resourceValue The value of the resource. For example, `values` or `values-de`.
*
* @return True if the resource was added, false if it already existed.
*/
operator fun invoke(
name: String,
value: String,
formatted: Boolean = true,
resourceValue: Value = "values",
) = invoke(resourceValue, StringResource(name, value, formatted))

/**
* Adds an [ArrayResource].
*
* @param name The name of the array resource.
* @param items The items of the array resource.
*
* @return True if the resource was added, false if it already existed.
*/
operator fun invoke(
name: String,
items: List<String>
) = invoke("values", ArrayResource(name, items))


/**
* Puts all resources of any [Value] staged in [resources] for the given [PatchClass] to [AddResourcesPatch].
*
* @param patch The class of the patch to add resources for.
* @param parseIds A function that parses the [AppId] and [PatchId] from the given [PatchClass].
* This is used to access the resources in [resources] to stage them in [AddResourcesPatch].
* The default implementation assumes that the [PatchClass] qualified name has the following format:
* `<any>.<any>.<any>.<app id>.<patch id>`.
*
* @return True if any resources were added, false if none were added.
*
* @see AddResourcesPatch.close
*/
operator fun invoke(
patch: PatchClass,
parseIds: PatchClass.() -> Pair<AppId, PatchId> = {
val qualifiedName = qualifiedName ?: throw PatchException("Patch qualified name is null")

// This requires qualifiedName to have the following format:
// `<any>.<any>.<any>.<app id>.<patch id>`
with(qualifiedName.split(".")) {
if (size < 5) throw PatchException("Patch qualified name has invalid format")

val appId = this[3]
val patchId = subList(4, size).joinToString(".")

appId to patchId
}
}
): Boolean {
val (appId, patchId) = patch.parseIds()

var result = false

// Stage resources for the given patch to AddResourcesPatch associated with their value.
resources.forEach { (value, resources) ->
resources[appId]?.get(patchId)?.let { patchResources ->
if (invoke(value, patchResources)) result = true
}
}

return result
}

/**
* Adds all resources staged in [AddResourcesPatch] to the app.
* This is called after all patches that depend on [AddResourcesPatch] have been executed.
*/
override fun close() {
operator fun MutableMap<String, Pair<DomFileEditor, Node>>.invoke(
value: Value,
resource: BaseResource
) {
// TODO: Fix open-closed principle violation by modifying BaseResource#serialize so that it accepts
// a Value and the map of editors. It will then get or put the editor suitable for its resource type
// to serialize itself to it.
val resourceFileName = when (resource) {
is StringResource -> "strings"
is ArrayResource -> "arrays"
else -> throw NotImplementedError("Unsupported resource type")
}

getOrPut(resourceFileName) {
val targetFile = context["res/$value/$resourceFileName.xml"].also {
it.parentFile?.mkdirs()
it.createNewFile()
}

context.xmlEditor[targetFile.path].let { editor ->
// Save the target node here as well
// in order to avoid having to call editor.getNode("resources")
// every time addUsingEditors is called but also save the editor so that it can be closed later.
editor to editor.getNode("resources")
}
}.let { (_, targetNode) ->
targetNode.addResource(resource) { invoke(value, it) }
}
}

forEach { (value, resources) ->
// A map of editors associated by their kind (e.g. strings, arrays).
// Each editor is accompanied by the target node to which resources are added.
// A map is used because Map#getOrPut allows opening a new editor for the duration of a resource value.
// This is done to prevent having to open the files for every resource that is added.
// Instead, it is cached once and reused for resources of the same value.
// This map is later accessed to close all editors for the current resource value.
val resourceFileEditors = mutableMapOf<String, Pair<DomFileEditor, Node>>()

resources.forEach { resource -> resourceFileEditors(value, resource) }

resourceFileEditors.values.forEach { (editor, _) -> editor.close() }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.instruction.Instruction

@Suppress("MemberVisibilityCanBePrivate")
abstract class AbstractTransformInstructionsPatch<T> : BytecodePatch() {
abstract class BaseTransformInstructionsPatch<T> : BytecodePatch() {
abstract fun filterMap(
classDef: ClassDef,
method: Method,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package app.revanced.patches.all.screencapture.removerestriction

import app.revanced.patcher.patch.annotation.Patch
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patches.all.misc.transformation.AbstractTransformInstructionsPatch
import app.revanced.patches.all.misc.transformation.BaseTransformInstructionsPatch
import app.revanced.patches.all.misc.transformation.IMethodCall
import app.revanced.patches.all.misc.transformation.Instruction35cInfo
import app.revanced.patches.all.misc.transformation.filterMapInstruction35c
Expand All @@ -18,7 +18,7 @@ import com.android.tools.smali.dexlib2.iface.instruction.Instruction
requiresIntegrations = true
)
@Suppress("unused")
object RemoveCaptureRestrictionPatch : AbstractTransformInstructionsPatch<Instruction35cInfo>() {
object RemoveCaptureRestrictionPatch : BaseTransformInstructionsPatch<Instruction35cInfo>() {
private const val INTEGRATIONS_CLASS_DESCRIPTOR_PREFIX =
"Lapp/revanced/integrations/all/screencapture/removerestriction/RemoveScreencaptureRestrictionPatch"
private const val INTEGRATIONS_CLASS_DESCRIPTOR = "$INTEGRATIONS_CLASS_DESCRIPTOR_PREFIX;"
Expand Down
Loading
Loading