refactor(Utils): match to official ReVanced code

This commit is contained in:
inotia00
2023-12-06 02:34:47 +09:00
parent 2eee2018fe
commit a9c062cbce
566 changed files with 2043 additions and 2341 deletions

View File

@ -0,0 +1,183 @@
package app.revanced.util
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.fingerprint.MethodFingerprint
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
import app.revanced.patcher.util.proxy.mutableTypes.MutableField
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction
import com.android.tools.smali.dexlib2.iface.reference.Reference
import com.android.tools.smali.dexlib2.util.MethodUtil
/**
* The [PatchException] of failing to resolve a [MethodFingerprint].
*
* @return The [PatchException].
*/
val MethodFingerprint.exception
get() = PatchException("Failed to resolve ${this.javaClass.simpleName}")
/**
* Find the [MutableMethod] from a given [Method] in a [MutableClass].
*
* @param method The [Method] to find.
* @return The [MutableMethod].
*/
fun MutableClass.findMutableMethodOf(method: Method) = this.methods.first {
MethodUtil.methodSignaturesMatch(it, method)
}
/**
* Apply a transform to all fields of the class.
*
* @param transform The transformation function. Accepts a [MutableField] and returns a transformed [MutableField].
*/
fun MutableClass.transformFields(transform: MutableField.() -> MutableField) {
val transformedFields = fields.map { it.transform() }
fields.clear()
fields.addAll(transformedFields)
}
/**
* Apply a transform to all methods of the class.
*
* @param transform The transformation function. Accepts a [MutableMethod] and returns a transformed [MutableMethod].
*/
fun MutableClass.transformMethods(transform: MutableMethod.() -> MutableMethod) {
val transformedMethods = methods.map { it.transform() }
methods.clear()
methods.addAll(transformedMethods)
}
/**
* Inject a call to a method that hides a view.
*
* @param insertIndex The index to insert the call at.
* @param viewRegister The register of the view to hide.
* @param classDescriptor The descriptor of the class that contains the method.
* @param targetMethod The name of the method to call.
*/
fun MutableMethod.injectHideViewCall(
insertIndex: Int,
viewRegister: Int,
classDescriptor: String,
targetMethod: String
) = addInstruction(
insertIndex,
"invoke-static { v$viewRegister }, $classDescriptor->$targetMethod(Landroid/view/View;)V"
)
/**
* Find the index of the first wide literal instruction with the given value.
*
* @return the first literal instruction with the value, or -1 if not found.
*/
fun Method.getWideLiteralInstructionIndex(literal: Long) = implementation?.let {
it.instructions.indexOfFirst { instruction ->
(instruction as? WideLiteralInstruction)?.wideLiteral == literal
}
} ?: -1
fun Method.getStringInstructionIndex(value: String) = implementation?.let {
it.instructions.indexOfFirst { instruction ->
instruction.opcode == Opcode.CONST_STRING
&& (instruction as? BuilderInstruction21c)?.reference.toString() == value
}
} ?: -1
/**
* Check if the method contains a literal with the given value.
*
* @return if the method contains a literal with the given value.
*/
fun Method.containsWideLiteralInstructionIndex(literal: Long) =
getWideLiteralInstructionIndex(literal) >= 0
/**
* Traverse the class hierarchy starting from the given root class.
*
* @param targetClass the class to start traversing the class hierarchy from.
* @param callback function that is called for every class in the hierarchy.
*/
fun BytecodeContext.traverseClassHierarchy(
targetClass: MutableClass,
callback: MutableClass.() -> Unit
) {
callback(targetClass)
this.findClass(targetClass.superclass ?: return)?.mutableClass?.let {
traverseClassHierarchy(it, callback)
}
}
/**
* Get the [Reference] of an [Instruction] as [T].
*
* @param T The type of [Reference] to cast to.
* @return The [Reference] as [T] or null
* if the [Instruction] is not a [ReferenceInstruction] or the [Reference] is not of type [T].
* @see ReferenceInstruction
*/
inline fun <reified T : Reference> Instruction.getReference() =
(this as? ReferenceInstruction)?.reference as? T
/**
* Get the index of the first [Instruction] that matches the predicate.
*
* @param predicate The predicate to match.
* @return The index of the first [Instruction] that matches the predicate.
*/
fun Method.indexOfFirstInstruction(predicate: Instruction.() -> Boolean) =
this.implementation!!.instructions.indexOfFirst(predicate)
/**
* Return the resolved methods of [MethodFingerprint]s early.
*/
fun List<MethodFingerprint>.returnEarly(bool: Boolean = false) {
val const = if (bool) "0x1" else "0x0"
this.forEach { fingerprint ->
fingerprint.result?.let { result ->
val stringInstructions = when (result.method.returnType.first()) {
'L' -> """
const/4 v0, $const
return-object v0
"""
'V' -> "return-void"
'I', 'Z' -> """
const/4 v0, $const
return v0
"""
else -> throw Exception("This case should never happen.")
}
result.mutableMethod.addInstructions(0, stringInstructions)
} ?: throw fingerprint.exception
}
}
fun BytecodeContext.updatePatchStatus(
className: String,
methodName: String
) {
this.classes.forEach { classDef ->
if (classDef.type.endsWith(className)) {
val patchStatusMethod =
this.proxy(classDef).mutableClass.methods.first { it.name == methodName }
patchStatusMethod.replaceInstruction(
0,
"const/4 v0, 0x1"
)
}
}
}

View File

@ -0,0 +1,121 @@
package app.revanced.util
import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.util.DomFileEditor
import org.w3c.dom.Element
import org.w3c.dom.Node
import java.nio.file.Files
import java.nio.file.StandardCopyOption
val classLoader: ClassLoader = object {}.javaClass.classLoader
fun Node.adoptChild(tagName: String, block: Element.() -> Unit) {
val child = ownerDocument.createElement(tagName)
child.block()
appendChild(child)
}
fun Node.cloneNodes(parent: Node) {
val node = cloneNode(true)
parent.appendChild(node)
parent.removeChild(this)
}
/**
* Recursively traverse the DOM tree starting from the given root node.
*
* @param action function that is called for every node in the tree.
*/
fun Node.doRecursively(action: (Node) -> Unit) {
action(this)
for (i in 0 until this.childNodes.length) this.childNodes.item(i).doRecursively(action)
}
fun Node.insertNode(tagName: String, targetNode: Node, block: Element.() -> Unit) {
val child = ownerDocument.createElement(tagName)
child.block()
parentNode.insertBefore(child, targetNode)
}
fun String.startsWithAny(vararg prefixes: String): Boolean {
for (prefix in prefixes)
if (this.startsWith(prefix))
return true
return false
}
/**
* Copy resources from the current class loader to the resource directory.
* @param sourceResourceDirectory The source resource directory name.
* @param resources The resources to copy.
*/
fun ResourceContext.copyResources(
sourceResourceDirectory: String,
vararg resources: ResourceGroup
) {
val targetResourceDirectory = this["res"]
for (resourceGroup in resources) {
resourceGroup.resources.forEach { resource ->
val resourceFile = "${resourceGroup.resourceDirectoryName}/$resource"
Files.copy(
classLoader.getResourceAsStream("$sourceResourceDirectory/$resourceFile")!!,
targetResourceDirectory.resolve(resourceFile).toPath(),
StandardCopyOption.REPLACE_EXISTING
)
}
}
}
/**
* Resource names mapped to their corresponding resource data.
* @param resourceDirectoryName The name of the directory of the resource.
* @param resources A list of resource names.
*/
class ResourceGroup(val resourceDirectoryName: String, vararg val resources: String)
/**
* Copy resources from the current class loader to the resource directory.
* @param resourceDirectory The directory of the resource.
* @param targetResource The target resource.
* @param elementTag The element to copy.
*/
fun ResourceContext.copyXmlNode(
resourceDirectory: String,
targetResource: String,
elementTag: String
) {
val stringsResourceInputStream =
classLoader.getResourceAsStream("$resourceDirectory/$targetResource")!!
// Copy nodes from the resources node to the real resource node
elementTag.copyXmlNode(
this.xmlEditor[stringsResourceInputStream],
this.xmlEditor["res/$targetResource"]
).close()
}
/**
* Copies the specified node of the source [DomFileEditor] to the target [DomFileEditor].
* @param source the source [DomFileEditor].
* @param target the target [DomFileEditor]-
* @return AutoCloseable that closes the target [DomFileEditor]s.
*/
fun String.copyXmlNode(source: DomFileEditor, target: DomFileEditor): AutoCloseable {
val hostNodes = source.file.getElementsByTagName(this).item(0).childNodes
val destinationResourceFile = target.file
val destinationNode = destinationResourceFile.getElementsByTagName(this).item(0)
for (index in 0 until hostNodes.length) {
val node = hostNodes.item(index).cloneNode(true)
destinationResourceFile.adoptNode(node)
destinationNode.appendChild(node)
}
return AutoCloseable {
source.close()
target.close()
}
}

View File

@ -1,32 +0,0 @@
package app.revanced.util.bytecode
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.util.integrations.Constants.MUSIC_UTILS_PATH
import app.revanced.util.integrations.Constants.UTILS_PATH
internal object BytecodeHelper {
internal fun BytecodeContext.updatePatchStatus(
methodName: String,
isYouTube: Boolean
) {
val integrationPath =
if (isYouTube)
UTILS_PATH
else
MUSIC_UTILS_PATH
this.classes.forEach { classDef ->
if (classDef.type.endsWith("$integrationPath/PatchStatus;")) {
val patchStatusMethod =
this.proxy(classDef).mutableClass.methods.first { it.name == methodName }
patchStatusMethod.replaceInstruction(
0,
"const/4 v0, 0x1"
)
}
}
}
}

View File

@ -1,56 +0,0 @@
package app.revanced.util.bytecode
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.instruction.NarrowLiteralInstruction
import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction
fun Method.isNarrowLiteralExists(value: Int): Boolean {
return getNarrowLiteralIndex(value) != -1
}
fun Method.isWideLiteralExists(value: Long): Boolean {
return getWideLiteralIndex(value) != -1
}
fun Method.isWide32LiteralExists(value: Long): Boolean {
return getWide32LiteralIndex(value) != -1
}
fun Method.getNarrowLiteralIndex(value: Int): Int {
return implementation?.let {
it.instructions.indexOfFirst { instruction ->
instruction.opcode == Opcode.CONST
&& (instruction as NarrowLiteralInstruction).narrowLiteral == value
}
} ?: -1
}
fun Method.getStringIndex(value: String): Int {
return implementation?.let {
it.instructions.indexOfFirst { instruction ->
instruction.opcode == Opcode.CONST_STRING
&& (instruction as BuilderInstruction21c).reference.toString() == value
}
} ?: -1
}
fun Method.getWideLiteralIndex(value: Long): Int {
return implementation?.let {
it.instructions.indexOfFirst { instruction ->
instruction.opcode == Opcode.CONST
&& (instruction as WideLiteralInstruction).wideLiteral == value
}
} ?: -1
}
fun Method.getWide32LiteralIndex(value: Long): Int {
return implementation?.let {
it.instructions.indexOfFirst { instruction ->
instruction.opcode == Opcode.CONST_WIDE_32
&& (instruction as WideLiteralInstruction).wideLiteral == value
}
} ?: -1
}

View File

@ -1,13 +0,0 @@
package app.revanced.util.enum
internal enum class CategoryType(val value: String, var added: Boolean) {
ACCOUNT("account", false),
ACTION_BAR("action_bar", false),
ADS("ads", false),
FLYOUT("flyout", false),
GENERAL("general", false),
MISC("misc", false),
NAVIGATION("navigation", false),
PLAYER("player", false),
VIDEO("video", false)
}

View File

@ -1,13 +0,0 @@
package app.revanced.util.enum
internal enum class ResourceType(val value: String) {
ATTR("attr"),
BOOL("bool"),
COLOR("color"),
DIMEN("dimen"),
DRAWABLE("drawable"),
ID("id"),
LAYOUT("layout"),
STRING("string"),
STYLE("style")
}

View File

@ -0,0 +1,34 @@
package app.revanced.util.fingerprint
import app.revanced.patcher.fingerprint.MethodFingerprint
import app.revanced.util.containsWideLiteralInstructionIndex
import com.android.tools.smali.dexlib2.Opcode
/**
* A fingerprint to resolve methods that contain a specific literal value.
*
* @param returnType The method's return type compared using String.startsWith.
* @param accessFlags The method's exact access flags using values of AccessFlags.
* @param parameters The parameters of the method. Partial matches allowed and follow the same rules as returnType.
* @param opcodes An opcode pattern of the method's instructions. Wildcard or unknown opcodes can be specified by null.
* @param strings A list of the method's strings compared each using String.contains.
* @param literalSupplier A supplier for the literal value to check for.
*/
abstract class LiteralValueFingerprint(
returnType: String? = null,
accessFlags: Int? = null,
parameters: Iterable<String>? = null,
opcodes: Iterable<Opcode>? = null,
strings: Iterable<String>? = null,
// Has to be a supplier because the fingerprint is created before patches can set literals.
literalSupplier: () -> Long
) : MethodFingerprint(
returnType = returnType,
accessFlags = accessFlags,
parameters = parameters,
opcodes = opcodes,
strings = strings,
customFingerprint = { methodDef, _ ->
methodDef.containsWideLiteralInstructionIndex(literalSupplier())
}
)

View File

@ -1,38 +0,0 @@
package app.revanced.util.integrations
internal object Constants {
const val INTEGRATIONS_PATH = "Lapp/revanced/integrations"
const val PATCHES_PATH = "$INTEGRATIONS_PATH/patches"
const val ADS_PATH = "$PATCHES_PATH/ads"
const val ALTERNATIVE_THUMBNAILS = "$PATCHES_PATH/layout/AlternativeThumbnailsPatch;"
const val COMPONENTS_PATH = "$PATCHES_PATH/components"
const val SWIPE_PATH = "$PATCHES_PATH/swipe"
const val FLYOUT_PANEL = "$PATCHES_PATH/layout/FlyoutPanelPatch;"
const val FULLSCREEN = "$PATCHES_PATH/layout/FullscreenPatch;"
const val GENERAL = "$PATCHES_PATH/layout/GeneralPatch;"
const val NAVIGATION = "$PATCHES_PATH/layout/NavigationPatch;"
const val PLAYER = "$PATCHES_PATH/layout/PlayerPatch;"
const val SEEKBAR = "$PATCHES_PATH/layout/SeekBarPatch;"
const val SHORTS = "$PATCHES_PATH/layout/ShortsPatch;"
const val MISC_PATH = "$PATCHES_PATH/misc"
const val BUTTON_PATH = "$PATCHES_PATH/button"
const val VIDEO_PATH = "$PATCHES_PATH/video"
const val UTILS_PATH = "$PATCHES_PATH/utils"
const val MUSIC_INTEGRATIONS_PATH = "Lapp/revanced/music"
private const val MUSIC_PATCHES_PATH = "$MUSIC_INTEGRATIONS_PATH/patches"
const val MUSIC_ACCOUNT = "$MUSIC_PATCHES_PATH/account/AccountPatch;"
const val MUSIC_ACTIONBAR = "$MUSIC_PATCHES_PATH/actionbar/ActionBarPatch;"
const val MUSIC_ADS_PATH = "$MUSIC_PATCHES_PATH/ads"
const val MUSIC_COMPONENTS_PATH = "$MUSIC_PATCHES_PATH/components"
const val MUSIC_FLYOUT = "$MUSIC_PATCHES_PATH/flyout/FlyoutPatch;"
const val MUSIC_GENERAL = "$MUSIC_PATCHES_PATH/general/GeneralPatch;"
const val MUSIC_MISC_PATH = "$MUSIC_PATCHES_PATH/misc"
const val MUSIC_NAVIGATION = "$MUSIC_PATCHES_PATH/navigation/NavigationPatch;"
const val MUSIC_PLAYER = "$MUSIC_PATCHES_PATH/player/PlayerPatch;"
const val MUSIC_VIDEO_PATH = "$MUSIC_PATCHES_PATH/video"
const val MUSIC_UTILS_PATH = "$MUSIC_PATCHES_PATH/utils"
}

View File

@ -1,136 +0,0 @@
package app.revanced.util.microg
/**
* constants for microG builds with signature spoofing
*/
internal object Constants {
/**
* microG vendor name
* aka. package prefix / package base
*/
const val MICROG_VENDOR = "com.mgoogle"
/**
* microG package name
*/
const val MICROG_PACKAGE_NAME = "$MICROG_VENDOR.android.gms"
/**
* meta-data for microG package name spoofing on patched builds
*/
const val META_SPOOFED_PACKAGE_NAME = "$MICROG_PACKAGE_NAME.SPOOFED_PACKAGE_NAME"
/**
* meta-data for microG package signature spoofing on patched builds
*/
const val META_SPOOFED_PACKAGE_SIGNATURE =
"$MICROG_PACKAGE_NAME.SPOOFED_PACKAGE_SIGNATURE"
/**
* meta-data for microG package detection
*/
const val META_GMS_PACKAGE_NAME = "app.revanced.MICROG_PACKAGE_NAME"
/**
* a list of all permissions in microG
*/
val PERMISSIONS = listOf(
// C2DM / GCM
"com.google.android.c2dm.permission.RECEIVE",
"com.google.android.c2dm.permission.SEND",
"com.google.android.gtalkservice.permission.GTALK_SERVICE",
// GAuth
"com.google.android.googleapps.permission.GOOGLE_AUTH",
"com.google.android.googleapps.permission.GOOGLE_AUTH.cp",
"com.google.android.googleapps.permission.GOOGLE_AUTH.local",
"com.google.android.googleapps.permission.GOOGLE_AUTH.mail",
"com.google.android.googleapps.permission.GOOGLE_AUTH.writely",
)
/**
* a list of all (intent) actions in microG
*/
val ACTIONS = listOf(
// location
"com.google.android.gms.location.places.ui.PICK_PLACE",
"com.google.android.gms.location.places.GeoDataApi",
"com.google.android.gms.location.places.PlacesApi",
"com.google.android.gms.location.places.PlaceDetectionApi",
"com.google.android.gms.wearable.MESSAGE_RECEIVED",
// C2DM / GCM
"com.google.android.c2dm.intent.REGISTER",
"com.google.android.c2dm.intent.REGISTRATION",
"com.google.android.c2dm.intent.UNREGISTER",
"com.google.android.c2dm.intent.RECEIVE",
"com.google.iid.TOKEN_REQUEST",
"com.google.android.gcm.intent.SEND",
// car
"com.google.android.gms.car.service.START",
// people
"com.google.android.gms.people.service.START",
// wearable
"com.google.android.gms.wearable.BIND",
// auth
"com.google.android.gsf.login",
"com.google.android.gsf.action.GET_GLS",
"com.google.android.gms.common.account.CHOOSE_ACCOUNT",
"com.google.android.gms.auth.login.LOGIN",
"com.google.android.gms.auth.api.credentials.PICKER",
"com.google.android.gms.auth.api.credentials.service.START",
"com.google.android.gms.auth.service.START",
"com.google.firebase.auth.api.gms.service.START",
"com.google.android.gms.auth.be.appcert.AppCertService",
// fido
"com.google.android.gms.fido.fido2.privileged.START",
// games
"com.google.android.gms.games.service.START",
"com.google.android.gms.games.PLAY_GAMES_UPGRADE",
// chimera
"com.google.android.gms.chimera",
// fonts
"com.google.android.gms.fonts",
// phenotype
"com.google.android.gms.phenotype.service.START",
// location
"com.google.android.gms.location.reporting.service.START",
// misc
"com.google.android.gms.gmscompliance.service.START",
"com.google.android.gms.oss.licenses.service.START",
"com.google.android.gms.safetynet.service.START",
"com.google.android.gms.tapandpay.service.BIND"
)
/**
* a list of all content provider authorities in microG
*/
val AUTHORITIES = listOf(
// gsf
"com.google.android.gsf.gservices",
"com.google.settings",
// auth
"com.google.android.gms.auth.accounts",
// chimera
"com.google.android.gms.chimera",
// fonts
"com.google.android.gms.fonts",
// phenotype
"com.google.android.gms.phenotype"
)
}

View File

@ -1,241 +0,0 @@
package app.revanced.util.microg
import app.revanced.extensions.exception
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.fingerprint.MethodFingerprint
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.util.microg.Constants.ACTIONS
import app.revanced.util.microg.Constants.AUTHORITIES
import app.revanced.util.microg.Constants.MICROG_VENDOR
import app.revanced.util.microg.Constants.PERMISSIONS
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c
import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction21c
import com.android.tools.smali.dexlib2.iface.reference.StringReference
import com.android.tools.smali.dexlib2.immutable.reference.ImmutableStringReference
/**
* Helper class for applying bytecode patches needed for the microg-support patches.
*/
internal object MicroGBytecodeHelper {
/**
* Transform strings with package name out of [fromPackageName] and [toPackageName].
*
* @param fromPackageName Original package name.
* @param toPackageName The package name to accept.
**/
fun packageNameTransform(fromPackageName: String, toPackageName: String): (String) -> String? {
return { referencedString ->
when (referencedString) {
"$fromPackageName.SuggestionsProvider",
"$fromPackageName.fileprovider" -> referencedString.replace(
fromPackageName,
toPackageName
)
else -> null
}
}
}
/**
* Prime method data class for the [MicroGBytecodeHelper] class.
*
* @param primeMethodFingerprint The prime methods [MethodFingerprint].
* @param fromPackageName Original package name.
* @param toPackageName The package name to accept.
**/
data class PrimeMethodTransformationData(
val primeMethodFingerprint: MethodFingerprint,
val fromPackageName: String,
val toPackageName: String
) {
/**
* Patch the prime method to accept the new package name.
*/
fun transformPrimeMethodPackageName() {
primeMethodFingerprint.result?.let {
var targetRegister = 2
it.mutableMethod.apply {
val targetIndex = implementation!!.instructions.indexOfFirst { instructions ->
if (instructions.opcode != Opcode.CONST_STRING) return@indexOfFirst false
val instructionString =
((instructions as Instruction21c).reference as StringReference).string
if (instructionString != fromPackageName) return@indexOfFirst false
targetRegister = instructions.registerA
return@indexOfFirst true
}
replaceInstruction(
targetIndex,
"const-string v$targetRegister, \"$toPackageName\""
)
}
} ?: throw primeMethodFingerprint.exception
}
}
/**
* Patch the bytecode to work with MicroG.
* Note: this only handles string constants to gms (intent actions, authorities, ...).
* If the app employs additional checks to validate the installed gms package, you'll have to handle those in the app- specific patch
*
* @param context The context.
* @param additionalStringTransforms Additional transformations applied to all const-string references.
* @param primeMethodTransformationData Data to patch the prime method.
* @param earlyReturns List of [MethodFingerprint] to return the resolved methods early.
*/
fun patchBytecode(
context: BytecodeContext,
additionalStringTransforms: Array<(str: String) -> String?>,
primeMethodTransformationData: PrimeMethodTransformationData,
earlyReturns: List<MethodFingerprint>
) {
earlyReturns.returnEarly()
primeMethodTransformationData.transformPrimeMethodPackageName()
val allTransforms = arrayOf(
MicroGBytecodeHelper::commonTransform,
MicroGBytecodeHelper::contentUrisTransform,
*additionalStringTransforms
)
// transform all strings using all provided transforms, first match wins
context.transformStringReferences transform@{
for (transformFn in allTransforms) {
val s = transformFn(it)
if (s != null) return@transform s
}
return@transform null
}
}
/**
* const-string transform function for common gms string references.
*
* @param referencedString The string to transform.
*/
private fun commonTransform(referencedString: String): String? =
when (referencedString) {
"com.google",
"com.google.android.gms",
in PERMISSIONS,
in ACTIONS,
in AUTHORITIES -> referencedString.replace("com.google", MICROG_VENDOR)
// subscribedfeeds has no vendor prefix for whatever reason...
"subscribedfeeds" -> "${MICROG_VENDOR}.subscribedfeeds"
else -> null
}
/**
* const-string transform function for strings containing gms content uris / authorities.
*/
private fun contentUrisTransform(str: String): String? {
// only when content:// uri
if (str.startsWith("content://")) {
// check if matches any authority
for (authority in AUTHORITIES) {
val uriPrefix = "content://$authority"
if (str.startsWith(uriPrefix)) {
return str.replace(
uriPrefix,
"content://${authority.replace("com.google", MICROG_VENDOR)}"
)
}
}
// gms also has a 'subscribedfeeds' authority, check for that one too
val subFeedsUriPrefix = "content://subscribedfeeds"
if (str.startsWith(subFeedsUriPrefix)) {
return str.replace(subFeedsUriPrefix, "content://${MICROG_VENDOR}.subscribedfeeds")
}
}
return null
}
/**
* Transform all constant string references using a transformation function.
*
* @param transformFn string transformation function. if null, string is not changed.
*/
private fun BytecodeContext.transformStringReferences(transformFn: (str: String) -> String?) {
classes.forEach { classDef ->
var mutableClass: MutableClass? = null
// enumerate all methods
classDef.methods.forEach classLoop@{ methodDef ->
var mutableMethod: MutableMethod? = null
val implementation = methodDef.implementation ?: return@classLoop
// enumerate all instructions and find const-string
implementation.instructions.forEachIndexed implLoop@{ index, instruction ->
// skip all that are not const-string
if (instruction.opcode != Opcode.CONST_STRING) return@implLoop
val str = ((instruction as Instruction21c).reference as StringReference).string
// call transform function
val transformedStr = transformFn(str)
if (transformedStr != null) {
// make class and method mutable, if not already
mutableClass = mutableClass ?: proxy(classDef).mutableClass
mutableMethod = mutableMethod ?: mutableClass!!.methods.first {
it.name == methodDef.name && it.parameterTypes.containsAll(methodDef.parameterTypes)
}
// replace instruction with updated string
mutableMethod!!.implementation!!.replaceInstruction(
index,
BuilderInstruction21c(
Opcode.CONST_STRING,
instruction.registerA,
ImmutableStringReference(
transformedStr
)
)
)
}
}
}
}
}
/**
* Return the resolved methods of a list of [MethodFingerprint] early.
*/
private fun List<MethodFingerprint>.returnEarly() {
this.forEach { fingerprint ->
fingerprint.result?.let {
it.mutableMethod.apply {
val stringInstructions = when (it.method.returnType.first()) {
'L' -> """
const/4 v0, 0x0
return-object v0
"""
'V' -> "return-void"
'I' -> """
const/4 v0, 0x0
return v0
"""
else -> throw Exception("This case should never happen.")
}
addInstructions(
0, stringInstructions
)
}
} ?: throw fingerprint.exception
}
}
}

View File

@ -1,56 +0,0 @@
package app.revanced.util.microg
import app.revanced.patcher.data.ResourceContext
import app.revanced.util.microg.Constants.META_GMS_PACKAGE_NAME
import app.revanced.util.microg.Constants.META_SPOOFED_PACKAGE_NAME
import app.revanced.util.microg.Constants.META_SPOOFED_PACKAGE_SIGNATURE
import app.revanced.util.microg.Constants.MICROG_VENDOR
import org.w3c.dom.Element
import org.w3c.dom.Node
/**
* helper class for adding manifest metadata needed for microG builds with signature spoofing
*/
internal object MicroGManifestHelper {
/**
* Add manifest entries needed for package and signature spoofing when using MicroG.
* Note: this only adds metadata entries for signature spoofing, other changes may still be required to make a microG patch work.
*
* @param spoofedPackage The package to spoof.
* @param spoofedSignature The signature to spoof.
*/
fun ResourceContext.addSpoofingMetadata(
spoofedPackage: String,
spoofedSignature: String
) {
this.xmlEditor["AndroidManifest.xml"].use {
val applicationNode = it
.file
.getElementsByTagName("application")
.item(0)
// package spoofing
applicationNode.adoptChild("meta-data") {
setAttribute("android:name", META_SPOOFED_PACKAGE_NAME)
setAttribute("android:value", spoofedPackage)
}
applicationNode.adoptChild("meta-data") {
setAttribute("android:name", META_SPOOFED_PACKAGE_SIGNATURE)
setAttribute("android:value", spoofedSignature)
}
// microG presence detection in integrations
applicationNode.adoptChild("meta-data") {
setAttribute("android:name", META_GMS_PACKAGE_NAME)
setAttribute("android:value", "${MICROG_VENDOR}.android.gms")
}
}
}
private fun Node.adoptChild(tagName: String, block: Element.() -> Unit) {
val child = ownerDocument.createElement(tagName)
child.block()
appendChild(child)
}
}

View File

@ -1,65 +0,0 @@
package app.revanced.util.microg
import app.revanced.patcher.data.ResourceContext
/**
* Helper class for applying resource patches needed for the microg-support patches.
*/
internal object MicroGResourceHelper {
/**
* Patch the manifest to work with MicroG.
*
* @param fromPackageName Original package name.
* @param toPackageName The package name to accept.
*/
fun ResourceContext.patchManifest(
fromPackageName: String,
toPackageName: String
) {
val manifest = this["AndroidManifest.xml"]
manifest.writeText(
manifest.readText()
.replace(
"package=\"$fromPackageName",
"package=\"$toPackageName"
).replace(
"android:authorities=\"$fromPackageName",
"android:authorities=\"$toPackageName"
).replace(
"$fromPackageName.permission.C2D_MESSAGE",
"$toPackageName.permission.C2D_MESSAGE"
).replace(
"$fromPackageName.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION",
"$toPackageName.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
).replace(
"com.google.android.c2dm",
"${Constants.MICROG_VENDOR}.android.c2dm"
).replace(
"</queries>",
"<package android:name=\"${Constants.MICROG_VENDOR}.android.gms\"/></queries>"
)
)
}
/**
* Patch the settings fragment to work with MicroG.
*
* @param fromPackageName Original package name.
* @param toPackageName The package name to accept.
*/
fun ResourceContext.patchSetting(
fromPackageName: String,
toPackageName: String
) {
val prefs = this["res/xml/settings_fragment.xml"]
prefs.writeText(
prefs.readText()
.replace(
"android:targetPackage=\"$fromPackageName",
"android:targetPackage=\"$toPackageName"
)
)
}
}

View File

@ -1,30 +0,0 @@
package app.revanced.util.pivotbar
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import com.android.tools.smali.dexlib2.Opcode.MOVE_RESULT_OBJECT
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
internal object InjectionUtils {
const val REGISTER_TEMPLATE_REPLACEMENT: String = "REGISTER_INDEX"
/**
* Injects an instruction into insertIndex of the hook.
* @param hook The hook to insert.
* @param insertIndex The index to insert the instruction at.
* [MOVE_RESULT_OBJECT] has to be the previous instruction before [insertIndex].
*/
fun MutableMethod.injectHook(hook: String, insertIndex: Int) {
val injectTarget = this
// Register to pass to the hook
val registerIndex = insertIndex - 1 // MOVE_RESULT_OBJECT is always the previous instruction
val register = injectTarget.getInstruction<OneRegisterInstruction>(registerIndex).registerA
injectTarget.addInstruction(
insertIndex,
hook.replace("REGISTER_INDEX", register.toString()),
)
}
}

View File

@ -1,235 +0,0 @@
package app.revanced.util.resources
import app.revanced.patcher.data.ResourceContext
import org.w3c.dom.Element
import java.io.File
import java.nio.file.Files
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
internal object IconHelper {
internal var YOUTUBE_LAUNCHER_ICON_ARRAY =
arrayOf(
"adaptiveproduct_youtube_background_color_108",
"adaptiveproduct_youtube_foreground_color_108",
"ic_launcher",
"ic_launcher_round"
)
internal var YOUTUBE_MUSIC_LAUNCHER_ICON_ARRAY =
arrayOf(
"adaptiveproduct_youtube_music_background_color_108",
"adaptiveproduct_youtube_music_foreground_color_108",
"ic_launcher_release"
)
internal fun ResourceContext.copyFiles(
resourceGroups: List<ResourceUtils.ResourceGroup>,
path: String
) {
val iconPath = File(Paths.get("").toAbsolutePath().toString()).resolve(path)
val resourceDirectory = this["res"]
resourceGroups.forEach { group ->
val fromDirectory = iconPath.resolve(group.resourceDirectoryName)
val toDirectory = resourceDirectory.resolve(group.resourceDirectoryName)
group.resources.forEach { iconFileName ->
Files.write(
toDirectory.resolve(iconFileName).toPath(),
fromDirectory.resolve(iconFileName).readBytes()
)
}
}
}
internal fun ResourceContext.makeDirectoryAndCopyFiles(
resourceGroups: List<ResourceUtils.ResourceGroup>,
path: String
) {
val newDirectory = Paths.get("").toAbsolutePath().toString() + "/$path"
this[newDirectory].mkdir()
val iconPath = File(Paths.get("").toAbsolutePath().toString()).resolve(path)
val resourceDirectory = this["res"]
resourceGroups.forEach { group ->
this[newDirectory + "/${group.resourceDirectoryName}"].mkdir()
val fromDirectory = iconPath.resolve(group.resourceDirectoryName)
val toDirectory = resourceDirectory.resolve(group.resourceDirectoryName)
group.resources.forEach { iconFileName ->
Files.write(
fromDirectory.resolve(iconFileName).toPath(),
toDirectory.resolve(iconFileName).readBytes()
)
}
}
}
internal fun ResourceContext.customIcon(iconName: String) {
val launchIcon = YOUTUBE_LAUNCHER_ICON_ARRAY
val splashIcon = arrayOf(
"product_logo_youtube_color_24",
"product_logo_youtube_color_36",
"product_logo_youtube_color_144",
"product_logo_youtube_color_192"
)
copyResources(
"youtube",
iconName,
"launchericon",
"mipmap",
launchIcon
)
copyResources(
"youtube",
iconName,
"splashicon",
"drawable",
splashIcon
)
monochromeIcon(
"youtube",
"adaptive_monochrome_ic_youtube_launcher",
iconName
)
this.disableSplashAnimation()
}
internal fun ResourceContext.customIconMusic(iconName: String) {
val launchIcon = YOUTUBE_MUSIC_LAUNCHER_ICON_ARRAY
copyResources(
"music",
iconName,
"launchericon",
"mipmap",
launchIcon
)
monochromeIcon(
"music",
"ic_app_icons_themed_youtube_music",
iconName
)
}
internal fun ResourceContext.customIconMusicAdditional(iconName: String) {
val record = arrayOf(
"hdpi",
"large-hdpi",
"large-mdpi",
"large-xhdpi",
"mdpi",
"xhdpi",
"xlarge-hdpi",
"xlarge-mdpi",
"xxhdpi"
)
val actionbarLogo = arrayOf(
"hdpi",
"mdpi",
"xhdpi",
"xxhdpi",
"xxxhdpi"
)
val actionbarLogoRelease = arrayOf(
"hdpi"
)
copyMusicResources(
iconName,
record,
"record"
)
copyMusicResources(
iconName,
actionbarLogo,
"action_bar_logo"
)
copyMusicResources(
iconName,
actionbarLogoRelease,
"action_bar_logo_release"
)
}
private fun ResourceContext.copyResources(
appName: String,
iconName: String,
iconPath: String,
directory: String,
iconArray: Array<String>
) {
arrayOf(
"xxxhdpi",
"xxhdpi",
"xhdpi",
"hdpi",
"mdpi"
).forEach { size ->
iconArray.forEach iconLoop@{ name ->
Files.copy(
ResourceUtils.javaClass.classLoader.getResourceAsStream("$appName/branding/$iconName/$iconPath/$size/$name.png")!!,
this["res"].resolve("$directory-$size").resolve("$name.png").toPath(),
StandardCopyOption.REPLACE_EXISTING
)
}
}
}
private fun ResourceContext.monochromeIcon(
appName: String,
monochromeIconName: String,
iconName: String
) {
try {
val relativePath = "drawable/$monochromeIconName.xml"
Files.copy(
ResourceUtils.javaClass.classLoader.getResourceAsStream("$appName/branding/$iconName/monochromeicon/$relativePath")!!,
this["res"].resolve(relativePath).toPath(),
StandardCopyOption.REPLACE_EXISTING
)
} catch (_: Exception) {
}
}
private fun ResourceContext.copyMusicResources(
iconName: String,
iconArray: Array<String>,
resourceNames: String
) {
iconArray.forEach { path ->
val relativePath = "drawable-$path/$resourceNames.png"
Files.copy(
ResourceUtils.javaClass.classLoader.getResourceAsStream("music/branding/$iconName/resource/$relativePath")!!,
this["res"].resolve(relativePath).toPath(),
StandardCopyOption.REPLACE_EXISTING
)
}
}
private fun ResourceContext.disableSplashAnimation() {
val targetPath = "res/values-v31/styles.xml"
xmlEditor[targetPath].use { editor ->
val tags = editor.file.getElementsByTagName("item")
List(tags.length) { tags.item(it) as Element }
.filter {
it.getAttribute("name").contains("android:windowSplashScreenAnimatedIcon")
}
.forEach { it.parentNode.removeChild(it) }
}
}
}

View File

@ -1,292 +0,0 @@
package app.revanced.util.resources
import app.revanced.extensions.doRecursively
import app.revanced.patcher.data.ResourceContext
import app.revanced.util.enum.CategoryType
import org.w3c.dom.Element
import org.w3c.dom.Node
private fun Node.adoptChild(tagName: String, block: Element.() -> Unit) {
val child = ownerDocument.createElement(tagName)
child.block()
appendChild(child)
}
private fun Node.cloneNodes(parent: Node) {
val node = cloneNode(true)
parent.appendChild(node)
parent.removeChild(this)
}
private fun Node.insertNode(tagName: String, targetNode: Node, block: Element.() -> Unit) {
val child = ownerDocument.createElement(tagName)
child.block()
parentNode.insertBefore(child, targetNode)
}
internal object MusicResourceHelper {
private const val YOUTUBE_MUSIC_SETTINGS_PATH = "res/xml/settings_headers.xml"
internal const val YOUTUBE_MUSIC_SETTINGS_KEY = "revanced_extended_settings"
internal const val RETURN_YOUTUBE_DISLIKE_SETTINGS_KEY = "revanced_ryd_settings"
private const val YOUTUBE_MUSIC_PREFERENCE_SCREEN_TAG_NAME =
"PreferenceScreen"
private const val YOUTUBE_MUSIC_PREFERENCE_TAG_NAME =
"com.google.android.apps.youtube.music.ui.preference.SwitchCompatPreference"
private const val YOUTUBE_MUSIC_PREFERENCE_TARGET_CLASS =
"com.google.android.libraries.strictmode.penalties.notification.FullStackTraceActivity"
internal var targetPackage = "com.google.android.apps.youtube.music"
internal fun ResourceContext.setMicroG(newPackage: String) {
targetPackage = newPackage
replacePackageName()
}
private fun setMusicPreferenceCategory(newCategory: String) {
CategoryType.entries.forEach { preference ->
if (newCategory == preference.value)
preference.added = true
}
}
private fun included(category: String): Boolean {
CategoryType.entries.forEach { preference ->
if (category == preference.value)
return preference.added
}
return false
}
internal fun ResourceContext.addMusicPreferenceCategory(
category: String
) {
this.xmlEditor[YOUTUBE_MUSIC_SETTINGS_PATH].use { editor ->
val tags = editor.file.getElementsByTagName("PreferenceScreen")
List(tags.length) { tags.item(it) as Element }
.filter { it.getAttribute("android:key").contains(YOUTUBE_MUSIC_SETTINGS_KEY) }
.forEach {
if (!included(category)) {
it.adoptChild(YOUTUBE_MUSIC_PREFERENCE_SCREEN_TAG_NAME) {
setAttribute("android:title", "@string/revanced_category_$category")
setAttribute("android:key", "revanced_settings_$category")
}
setMusicPreferenceCategory(category)
}
}
}
}
internal fun ResourceContext.sortMusicPreferenceCategory(
category: String
) {
this.xmlEditor[YOUTUBE_MUSIC_SETTINGS_PATH].use { editor ->
editor.file.doRecursively loop@{
if (it !is Element) return@loop
it.getAttributeNode("android:key")?.let { attribute ->
if (attribute.textContent == "revanced_settings_$category") {
it.cloneNodes(it.parentNode)
}
}
}
}
replacePackageName()
}
private fun ResourceContext.replacePackageName() {
this[YOUTUBE_MUSIC_SETTINGS_PATH].writeText(
this[YOUTUBE_MUSIC_SETTINGS_PATH].readText()
.replace("\"com.google.android.apps.youtube.music\"", "\"" + targetPackage + "\"")
)
}
internal fun ResourceContext.addMicroGPreference(
category: String,
key: String,
packageName: String,
targetClassName: String
) {
this.xmlEditor[YOUTUBE_MUSIC_SETTINGS_PATH].use { editor ->
val tags = editor.file.getElementsByTagName(YOUTUBE_MUSIC_PREFERENCE_SCREEN_TAG_NAME)
List(tags.length) { tags.item(it) as Element }
.filter { it.getAttribute("android:key").contains("revanced_settings_$category") }
.forEach {
it.adoptChild("Preference") {
setAttribute("android:title", "@string/$key" + "_title")
setAttribute("android:summary", "@string/$key" + "_summary")
this.adoptChild("intent") {
setAttribute("android:targetPackage", packageName)
setAttribute("android:data", key)
setAttribute(
"android:targetClass",
targetClassName
)
}
}
}
}
}
internal fun ResourceContext.addMusicPreference(
category: String,
key: String,
defaultValue: String,
dependencyKey: String
) {
this.xmlEditor[YOUTUBE_MUSIC_SETTINGS_PATH].use { editor ->
val tags = editor.file.getElementsByTagName(YOUTUBE_MUSIC_PREFERENCE_SCREEN_TAG_NAME)
List(tags.length) { tags.item(it) as Element }
.filter { it.getAttribute("android:key").contains("revanced_settings_$category") }
.forEach {
it.adoptChild(YOUTUBE_MUSIC_PREFERENCE_TAG_NAME) {
setAttribute("android:title", "@string/$key" + "_title")
setAttribute("android:summary", "@string/$key" + "_summary")
setAttribute("android:key", key)
setAttribute("android:defaultValue", defaultValue)
if (dependencyKey != "") {
setAttribute("android:dependency", dependencyKey)
}
}
}
}
}
internal fun ResourceContext.addMusicPreferenceWithIntent(
category: String,
key: String,
dependencyKey: String
) {
this.xmlEditor[YOUTUBE_MUSIC_SETTINGS_PATH].use { editor ->
val tags = editor.file.getElementsByTagName(YOUTUBE_MUSIC_PREFERENCE_SCREEN_TAG_NAME)
List(tags.length) { tags.item(it) as Element }
.filter { it.getAttribute("android:key").contains("revanced_settings_$category") }
.forEach {
it.adoptChild("Preference") {
setAttribute("android:title", "@string/$key" + "_title")
setAttribute("android:summary", "@string/$key" + "_summary")
setAttribute("android:key", key)
if (dependencyKey != "") {
setAttribute("android:dependency", dependencyKey)
}
this.adoptChild("intent") {
setAttribute("android:targetPackage", targetPackage)
setAttribute("android:data", key)
setAttribute(
"android:targetClass",
YOUTUBE_MUSIC_PREFERENCE_TARGET_CLASS
)
}
}
}
}
}
internal fun ResourceContext.addMusicPreferenceWithoutSummary(
category: String,
key: String,
defaultValue: String
) {
this.xmlEditor[YOUTUBE_MUSIC_SETTINGS_PATH].use { editor ->
val tags = editor.file.getElementsByTagName(YOUTUBE_MUSIC_PREFERENCE_SCREEN_TAG_NAME)
List(tags.length) { tags.item(it) as Element }
.filter { it.getAttribute("android:key").contains("revanced_settings_$category") }
.forEach {
it.adoptChild(YOUTUBE_MUSIC_PREFERENCE_TAG_NAME) {
setAttribute("android:title", "@string/$key" + "_title")
setAttribute("android:key", key)
setAttribute("android:defaultValue", defaultValue)
}
}
}
}
internal fun ResourceContext.addReVancedMusicPreference(
key: String
) {
this.xmlEditor[YOUTUBE_MUSIC_SETTINGS_PATH].use { editor ->
with(editor.file) {
doRecursively loop@{
if (it !is Element) return@loop
it.getAttributeNode("android:key")?.let { attribute ->
if (attribute.textContent == "settings_header_about_youtube_music" && it.getAttributeNode(
"app:allowDividerBelow"
).textContent == "false"
) {
it.insertNode("PreferenceScreen", it) {
setAttribute(
"android:title",
"@string/" + key + "_title"
)
setAttribute("android:key", key)
setAttribute("app:allowDividerAbove", "false")
setAttribute("app:allowDividerAbove", "false")
}
it.getAttributeNode("app:allowDividerBelow").textContent = "true"
return@loop
}
}
}
doRecursively loop@{
if (it !is Element) return@loop
it.getAttributeNode("app:allowDividerBelow")?.let { attribute ->
if (attribute.textContent == "true") {
attribute.textContent = "false"
}
}
}
}
}
}
internal fun ResourceContext.hookPreference(
key: String,
fragment: String
) {
this.xmlEditor[YOUTUBE_MUSIC_SETTINGS_PATH].use { editor ->
with(editor.file) {
doRecursively loop@{
if (it !is Element) return@loop
it.getAttributeNode("android:key")?.let { attribute ->
if (attribute.textContent == "settings_header_about_youtube_music" && it.getAttributeNode(
"app:allowDividerBelow"
).textContent == "false"
) {
it.insertNode("Preference", it) {
setAttribute("android:persistent", "false")
setAttribute(
"android:title",
"@string/" + key + "_title"
)
setAttribute("android:key", key)
setAttribute("android:fragment", fragment)
setAttribute("app:allowDividerAbove", "false")
setAttribute("app:allowDividerAbove", "false")
}
it.getAttributeNode("app:allowDividerBelow").textContent = "true"
return@loop
}
}
}
doRecursively loop@{
if (it !is Element) return@loop
it.getAttributeNode("app:allowDividerBelow")?.let { attribute ->
if (attribute.textContent == "true") {
attribute.textContent = "false"
}
}
}
}
}
}
}

View File

@ -1,160 +0,0 @@
package app.revanced.util.resources
import app.revanced.extensions.doRecursively
import app.revanced.patcher.data.ResourceContext
import org.w3c.dom.Element
import org.w3c.dom.Node
import java.nio.file.Files
import java.nio.file.StandardCopyOption
private fun Node.insertNode(tagName: String, targetNode: Node, block: Element.() -> Unit) {
val child = ownerDocument.createElement(tagName)
child.block()
parentNode.insertBefore(child, targetNode)
}
internal object ResourceHelper {
private const val TARGET_PREFERENCE_PATH = "res/xml/revanced_prefs.xml"
private const val YOUTUBE_SETTINGS_PATH = "res/xml/settings_fragment.xml"
private var targetPackage = "com.google.android.youtube"
internal fun setMicroG(newPackage: String) {
targetPackage = newPackage
}
internal fun ResourceContext.addEntryValues(
path: String,
speedEntryValues: String,
attributeName: String
) {
xmlEditor[path].use {
with(it.file) {
val resourcesNode = getElementsByTagName("resources").item(0) as Element
val newElement: Element = createElement("item")
for (i in 0 until resourcesNode.childNodes.length) {
val node = resourcesNode.childNodes.item(i) as? Element ?: continue
if (node.getAttribute("name") == attributeName) {
newElement.appendChild(createTextNode(speedEntryValues))
node.appendChild(newElement)
}
}
}
}
}
internal fun ResourceContext.addPreference(settingArray: Array<String>) {
val prefs = this[TARGET_PREFERENCE_PATH]
settingArray.forEach preferenceLoop@{ preference ->
prefs.writeText(
prefs.readText()
.replace("<!-- $preference", "")
.replace("$preference -->", "")
)
}
}
internal fun ResourceContext.updatePatchStatus(patchTitle: String) {
updatePatchStatusSettings(patchTitle, "@string/revanced_patches_included")
}
internal fun ResourceContext.updatePatchStatusHeader(headerName: String) {
updatePatchStatusSettings("Header", headerName)
}
internal fun ResourceContext.updatePatchStatusIcon(iconName: String) {
updatePatchStatusSettings("Icon", "@string/revanced_icon_$iconName")
}
internal fun ResourceContext.updatePatchStatusLabel(appName: String) {
updatePatchStatusSettings("Label", appName)
}
internal fun ResourceContext.updatePatchStatusTheme(themeName: String) {
updatePatchStatusSettings("Theme", themeName)
}
internal fun ResourceContext.updatePatchStatusSettings(
patchTitle: String,
updateText: String
) {
this.xmlEditor[TARGET_PREFERENCE_PATH].use { editor ->
editor.file.doRecursively loop@{
if (it !is Element) return@loop
it.getAttributeNode("android:title")?.let { attribute ->
if (attribute.textContent == patchTitle) {
it.getAttributeNode("android:summary").textContent = updateText
}
}
}
}
}
internal fun ResourceContext.addReVancedPreference(key: String) {
val targetClass =
"com.google.android.apps.youtube.app.settings.videoquality.VideoQualitySettingsActivity"
this.xmlEditor[YOUTUBE_SETTINGS_PATH].use { editor ->
with(editor.file) {
doRecursively loop@{
if (it !is Element) return@loop
it.getAttributeNode("android:key")?.let { attribute ->
if (attribute.textContent == "@string/about_key" && it.getAttributeNode("app:iconSpaceReserved").textContent == "false") {
it.insertNode("Preference", it) {
setAttribute("android:title", "@string/revanced_" + key + "_title")
this.appendChild(
ownerDocument.createElement("intent").also { intentNode ->
intentNode.setAttribute(
"android:targetPackage",
targetPackage
)
intentNode.setAttribute("android:data", key)
intentNode.setAttribute("android:targetClass", targetClass)
})
}
it.getAttributeNode("app:iconSpaceReserved").textContent = "true"
return@loop
}
}
}
doRecursively loop@{
if (it !is Element) return@loop
it.getAttributeNode("app:iconSpaceReserved")?.let { attribute ->
if (attribute.textContent == "true") {
attribute.textContent = "false"
}
}
}
}
}
}
internal fun ResourceContext.addTranslations(
sourceDirectory: String,
languageArray: Array<String>
) {
languageArray.forEach { language ->
val directory = "values-$language-v21"
val relativePath = "$language/strings.xml"
this["res/$directory"].mkdir()
Files.copy(
ResourceUtils.javaClass.classLoader.getResourceAsStream("$sourceDirectory/translations/$relativePath")!!,
this["res"].resolve("$directory/strings.xml").toPath(),
StandardCopyOption.REPLACE_EXISTING
)
}
}
}

View File

@ -1,85 +0,0 @@
package app.revanced.util.resources
import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.util.DomFileEditor
import java.nio.file.Files
import java.nio.file.StandardCopyOption
@Suppress("MemberVisibilityCanBePrivate")
object ResourceUtils {
/**
* Copy resources from the current class loader to the resource directory.
* @param sourceResourceDirectory The source resource directory name.
* @param resources The resources to copy.
*/
fun ResourceContext.copyResources(
sourceResourceDirectory: String,
vararg resources: ResourceGroup
) {
val classLoader = ResourceUtils.javaClass.classLoader
val targetResourceDirectory = this["res"]
for (resourceGroup in resources) {
resourceGroup.resources.forEach { resource ->
val resourceFile = "${resourceGroup.resourceDirectoryName}/$resource"
Files.copy(
classLoader.getResourceAsStream("$sourceResourceDirectory/$resourceFile")!!,
targetResourceDirectory.resolve(resourceFile).toPath(),
StandardCopyOption.REPLACE_EXISTING
)
}
}
}
/**
* Resource names mapped to their corresponding resource data.
* @param resourceDirectoryName The name of the directory of the resource.
* @param resources A list of resource names.
*/
class ResourceGroup(val resourceDirectoryName: String, vararg val resources: String)
/**
* Copy resources from the current class loader to the resource directory.
* @param resourceDirectory The directory of the resource.
* @param targetResource The target resource.
* @param elementTag The element to copy.
*/
fun ResourceContext.copyXmlNode(
resourceDirectory: String,
targetResource: String,
elementTag: String
) {
val stringsResourceInputStream =
ResourceUtils.javaClass.classLoader.getResourceAsStream("$resourceDirectory/$targetResource")!!
// Copy nodes from the resources node to the real resource node
elementTag.copyXmlNode(
this.xmlEditor[stringsResourceInputStream],
this.xmlEditor["res/$targetResource"]
).close()
}
/**
* Copies the specified node of the source [DomFileEditor] to the target [DomFileEditor].
* @param source the source [DomFileEditor].
* @param target the target [DomFileEditor]-
* @return AutoCloseable that closes the target [DomFileEditor]s.
*/
fun String.copyXmlNode(source: DomFileEditor, target: DomFileEditor): AutoCloseable {
val hostNodes = source.file.getElementsByTagName(this).item(0).childNodes
val destinationResourceFile = target.file
val destinationNode = destinationResourceFile.getElementsByTagName(this).item(0)
for (index in 0 until hostNodes.length) {
val node = hostNodes.item(index).cloneNode(true)
destinationResourceFile.adoptNode(node)
destinationNode.appendChild(node)
}
return AutoCloseable {
source.close()
target.close()
}
}
}