mirror of
https://github.com/inotia00/revanced-patches.git
synced 2025-06-12 21:27:43 +02:00
refactor(Utils): match to official ReVanced code
This commit is contained in:
183
src/main/kotlin/app/revanced/util/BytecodeUtils.kt
Normal file
183
src/main/kotlin/app/revanced/util/BytecodeUtils.kt
Normal 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"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
121
src/main/kotlin/app/revanced/util/ResourceUtils.kt
Normal file
121
src/main/kotlin/app/revanced/util/ResourceUtils.kt
Normal 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()
|
||||
}
|
||||
}
|
@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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")
|
||||
}
|
@ -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())
|
||||
}
|
||||
)
|
@ -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"
|
||||
}
|
@ -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"
|
||||
)
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
@ -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()),
|
||||
)
|
||||
}
|
||||
}
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user