feat: settings patch framework (#266)

This commit is contained in:
oSumAtrIX
2022-08-22 01:59:43 +02:00
committed by GitHub
parent 8c3866f5e3
commit 9ef93f6618
62 changed files with 1219 additions and 728 deletions
@@ -4,7 +4,7 @@ import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version
import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import app.revanced.patches.youtube.layout.returnyoutubedislike.annotations.ReturnYouTubeDislikeCompatibility
import app.revanced.patches.youtube.misc.settings.annotations.SettingsCompatibility
// TODO: This is more of a class fingerprint than a method fingerprint.
// Convert to a class fingerprint whenever possible.
@@ -12,7 +12,7 @@ import app.revanced.patches.youtube.layout.returnyoutubedislike.annotations.Retu
@MatchingMethod(
"Lcom/google/android/libraries/social/licenses/LicenseActivity;", "onCreate"
)
@ReturnYouTubeDislikeCompatibility
@SettingsCompatibility
@Version("0.0.1")
object LicenseActivityFingerprint : MethodFingerprint(
null,
@@ -2,10 +2,9 @@ package app.revanced.patches.youtube.misc.settings.bytecode.fingerprints
import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version
import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod
import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import app.revanced.patches.youtube.layout.returnyoutubedislike.annotations.ReturnYouTubeDislikeCompatibility
import app.revanced.patches.youtube.misc.settings.annotations.SettingsCompatibility
// TODO: This is more of a class fingerprint than a method fingerprint.
// Convert to a class fingerprint whenever possible.
@@ -13,7 +12,7 @@ import app.revanced.patches.youtube.layout.returnyoutubedislike.annotations.Retu
@MatchingMethod(
"Lapp/revanced/integrations/settingsmenu/ReVancedSettingActivity;", "initializeSettings"
)
@ReturnYouTubeDislikeCompatibility
@SettingsCompatibility
@Version("0.0.1")
object ReVancedSettingsActivityFingerprint : MethodFingerprint(
null,
@@ -4,7 +4,7 @@ import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version
import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import app.revanced.patches.youtube.layout.returnyoutubedislike.annotations.ReturnYouTubeDislikeCompatibility
import app.revanced.patches.youtube.misc.settings.annotations.SettingsCompatibility
import app.revanced.patches.youtube.misc.settings.bytecode.patch.SettingsPatch
import org.jf.dexlib2.Opcode
import org.jf.dexlib2.iface.instruction.WideLiteralInstruction
@@ -13,7 +13,7 @@ import org.jf.dexlib2.iface.instruction.WideLiteralInstruction
@MatchingMethod(
"Lfyq;", "a"
)
@ReturnYouTubeDislikeCompatibility
@SettingsCompatibility
@Version("0.0.1")
object ThemeSetterFingerprint : MethodFingerprint(
"L",
@@ -18,14 +18,20 @@ import app.revanced.patches.youtube.misc.settings.annotations.SettingsCompatibil
import app.revanced.patches.youtube.misc.settings.bytecode.fingerprints.LicenseActivityFingerprint
import app.revanced.patches.youtube.misc.settings.bytecode.fingerprints.ReVancedSettingsActivityFingerprint
import app.revanced.patches.youtube.misc.settings.bytecode.fingerprints.ThemeSetterFingerprint
import app.revanced.patches.youtube.misc.settings.framework.components.BasePreference
import app.revanced.patches.youtube.misc.settings.framework.components.impl.ArrayResource
import app.revanced.patches.youtube.misc.settings.framework.components.impl.Preference
import app.revanced.patches.youtube.misc.settings.framework.components.impl.PreferenceScreen
import app.revanced.patches.youtube.misc.settings.framework.components.impl.StringResource
import app.revanced.patches.youtube.misc.settings.resource.patch.SettingsResourcePatch
import org.jf.dexlib2.util.MethodUtil
import java.io.Closeable
@Patch
@DependsOn(
[
IntegrationsPatch::class,
SettingsResourcePatch::class,
ResourceIdMappingProviderResourcePatch::class
]
)
@Name("settings")
@@ -34,7 +40,7 @@ import app.revanced.patches.youtube.misc.settings.resource.patch.SettingsResourc
@Version("0.0.1")
class SettingsPatch : BytecodePatch(
listOf(LicenseActivityFingerprint, ReVancedSettingsActivityFingerprint, ThemeSetterFingerprint)
) {
), Closeable {
override fun execute(data: BytecodeData): PatchResult {
val licenseActivityResult = LicenseActivityFingerprint.result!!
val settingsResult = ReVancedSettingsActivityFingerprint.result!!
@@ -65,7 +71,7 @@ class SettingsPatch : BytecodePatch(
)
}
// add the setTheme call to the onCreate method to not affect the offsets.
// add the setTheme call to the onCreate method to not affect the offsets
onCreate.addInstructions(
1,
"""
@@ -74,18 +80,76 @@ class SettingsPatch : BytecodePatch(
"""
)
// add the initializeSettings call to the onCreate method.
// add the initializeSettings call to the onCreate method
onCreate.addInstruction(
0,
"invoke-static { p0 }, ${settingsClass.type}->$setThemeMethodName(${licenseActivityClass.type})V"
)
// get rid of, now, useless overridden methods
licenseActivityResult.mutableClass.methods.removeIf { it.name != "onCreate" && !MethodUtil.isConstructor(it) }
return PatchResultSuccess()
}
internal companion object {
val appearanceStringId = ResourceIdMappingProviderResourcePatch.resourceMappings.find {
// TODO: hide this somehow
var appearanceStringId: Long = ResourceIdMappingProviderResourcePatch.resourceMappings.find {
it.type == "string" && it.name == "app_theme_appearance_dark"
}!!.id
fun addString(identifier: String, value: String, formatted: Boolean = true) =
SettingsResourcePatch.addString(identifier, value, formatted)
fun addPreferenceScreen(preferenceScreen: app.revanced.patches.youtube.misc.settings.framework.components.impl.PreferenceScreen) =
SettingsResourcePatch.addPreferenceScreen(preferenceScreen)
fun addPreference(preference: Preference) =
SettingsResourcePatch.addPreference(preference)
fun addArray(arrayResource: ArrayResource) =
SettingsResourcePatch.addArray(arrayResource)
fun renameIntentsTargetPackage(newPackage: String) {
SettingsResourcePatch.overrideIntentsTargetPackage = newPackage
}
}
/**
* Preference screens patches should add their settings to.
*/
internal enum class PreferenceScreen(
private val key: String,
private val title: String,
private val summary: String? = null,
private val preferences: MutableList<BasePreference> = mutableListOf()
) : Closeable {
ADS("ads", "Ads", "Ad related settings"),
INTERACTIONS("interactions", "Interaction", "Settings related to interactions"),
LAYOUT("layout", "Layout", "Settings related to the layout"),
MISC("misc", "Miscellaneous", "Miscellaneous patches");
override fun close() {
if (preferences.size == 0) return
addPreferenceScreen(
PreferenceScreen(
key,
StringResource("${key}_title", title),
preferences,
summary?.let { summary ->
StringResource("${key}_summary", summary)
}
)
)
}
/**
* Add preferences to the preference screen.
*/
fun addPreferences(vararg preferences: BasePreference) = this.preferences.addAll(preferences)
}
override fun close() = PreferenceScreen.values().forEach(PreferenceScreen::close)
}
@@ -0,0 +1,14 @@
package app.revanced.patches.youtube.misc.settings.framework.components
import app.revanced.patches.youtube.misc.settings.framework.components.impl.StringResource
/**
* Base preference class for all preferences.
*
* @param key The key of the preference.
* @param title The title of the preference.
*/
internal abstract class BasePreference(
override val key: String,
override val title: StringResource,
) : IPreference
@@ -0,0 +1,23 @@
package app.revanced.patches.youtube.misc.settings.framework.components
import app.revanced.patches.youtube.misc.settings.framework.components.impl.StringResource
/**
* Preference
*/
internal interface IPreference {
/**
* Key of the preference.
*/
val key: String
/**
* Title of the preference.
*/
val title: StringResource
/**
* Tag name of the preference.
*/
val tag: String
}
@@ -0,0 +1,9 @@
package app.revanced.patches.youtube.misc.settings.framework.components.impl
/**
* Represents an array resource.
*
* @param name The name of the array resource.
* @param items The items of the array resource.
*/
internal data class ArrayResource(val name: String, val items: List<StringResource>)
@@ -0,0 +1,6 @@
package app.revanced.patches.youtube.misc.settings.framework.components.impl
enum class InputType(val type: String) {
STRING("text"),
NUMBER("number"),
}
@@ -0,0 +1,18 @@
package app.revanced.patches.youtube.misc.settings.framework.components.impl
/**
* A Preference object.
*
* @param title The title of the preference.
* @param intent The intent of the preference.
* @param summary The summary of the text preference.
*/
internal class Preference(
val title: StringResource,
val intent: Intent,
val summary: StringResource? = null
) {
val tag: String = "Preference"
data class Intent(val targetPackage: String, val data: String, val targetClass: String)
}
@@ -0,0 +1,20 @@
package app.revanced.patches.youtube.misc.settings.framework.components.impl
import app.revanced.patches.youtube.misc.settings.framework.components.BasePreference
/**
* Preference screen.
*
* @param key The key of the preference.
* @param title The title of the preference.
* @param preferences Child preferences of this screen.
* @param summary The summary of the text preference.
*/
internal open class PreferenceScreen(
key: String,
title: StringResource,
val preferences: List<BasePreference>,
var summary: StringResource? = null
) : BasePreference(key, title) {
override val tag: String = "PreferenceScreen"
}
@@ -0,0 +1,10 @@
package app.revanced.patches.youtube.misc.settings.framework.components.impl
/**
* Represents a string value in the strings.xml file
*
* @param name The name of the string
* @param value The value of the string
* @param formatted If the string is formatted. If false, the attribute will be set
*/
internal data class StringResource(val name: String, val value: String, val formatted: Boolean = true)
@@ -0,0 +1,21 @@
package app.revanced.patches.youtube.misc.settings.framework.components.impl
import app.revanced.patches.youtube.misc.settings.framework.components.BasePreference
/**
* Switch preference.
*
* @param key The key of the switch.
* @param title The title of the switch.
* @param default The default value of the switch.
* @param summaryOn The summary to show when the preference is enabled.
* @param summaryOff The summary to show when the preference is disabled.
*/
internal class SwitchPreference(
key: String, title: StringResource,
val default: Boolean = false,
var summaryOn: StringResource? = null,
var summaryOff: StringResource? = null
) : BasePreference(key, title) {
override val tag: String = "SwitchPreference"
}
@@ -0,0 +1,22 @@
package app.revanced.patches.youtube.misc.settings.framework.components.impl
import app.revanced.patches.youtube.misc.settings.framework.components.BasePreference
/**
* Text preference.
*
* @param key The key of the text preference.
* @param title The title of the text preference.
* @param inputType The input type of the text preference.
* @param default The default value of the text preference.
* @param summary The summary of the text preference.
*/
internal class TextPreference(
key: String,
title: StringResource,
var inputType: InputType = InputType.STRING,
var default: String? = null,
var summary: StringResource? = null
) : BasePreference(key, title) {
override val tag: String = "EditTextPreference"
}
@@ -2,71 +2,290 @@ package app.revanced.patches.youtube.misc.settings.resource.patch
import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version
import app.revanced.patcher.data.impl.DomFileEditor
import app.revanced.patcher.data.impl.ResourceData
import app.revanced.patcher.patch.PatchResult
import app.revanced.patcher.patch.PatchResultSuccess
import app.revanced.patcher.patch.annotations.DependsOn
import app.revanced.patcher.patch.impl.ResourcePatch
import app.revanced.patches.youtube.misc.manifest.patch.FixLocaleConfigErrorPatch
import app.revanced.patches.youtube.misc.mapping.patch.ResourceIdMappingProviderResourcePatch
import app.revanced.patches.youtube.misc.settings.annotations.SettingsCompatibility
import app.revanced.patches.youtube.misc.settings.bytecode.patch.SettingsPatch
import app.revanced.patches.youtube.misc.settings.framework.components.BasePreference
import app.revanced.patches.youtube.misc.settings.framework.components.impl.*
import app.revanced.util.resources.ResourceUtils
import app.revanced.util.resources.ResourceUtils.copyResources
import app.revanced.util.resources.ResourceUtils.copyXmlNode
import org.w3c.dom.Element
import org.w3c.dom.Node
import java.io.Closeable
@Name("settings-resource-patch")
@SettingsCompatibility
@DependsOn([FixLocaleConfigErrorPatch::class])
@DependsOn([FixLocaleConfigErrorPatch::class, ResourceIdMappingProviderResourcePatch::class])
@Version("0.0.1")
class SettingsResourcePatch : ResourcePatch() {
class SettingsResourcePatch : ResourcePatch(), Closeable {
override fun execute(data: ResourceData): PatchResult {
/*
* Copy strings
* create missing directory for the resources
*/
data.copyXmlNode("settings/host", "values/strings.xml", "resources")
data["res/drawable-ldrtl-xxxhdpi"].mkdirs()
/*
* Copy arrays
*/
data.copyXmlNode("settings/host", "values/arrays.xml", "resources")
/*
* Copy preference fragments
*/
data.copyXmlNode("settings/host", "xml/settings_fragment.xml", "PreferenceScreen")
/*
* Copy layout resources
* copy layout resources
*/
arrayOf(
ResourceUtils.ResourceGroup(
"layout",
"xsettings_toolbar.xml",
"xsettings_with_toolbar.xml",
"xsettings_with_toolbar_layout.xml"
),
ResourceUtils.ResourceGroup(
"xml",
"revanced_prefs.xml"
"revanced_settings_toolbar.xml",
"revanced_settings_with_toolbar.xml",
"revanced_settings_with_toolbar_layout.xml"
), ResourceUtils.ResourceGroup(
"xml", "revanced_prefs.xml" // template for new preferences
), ResourceUtils.ResourceGroup(
// required resource for back button, because when the base APK is used, this resource will not exist
"drawable-xxxhdpi", "quantum_ic_arrow_back_white_24.png"
), ResourceUtils.ResourceGroup(
// required resource for back button, because when the base APK is used, this resource will not exist
"drawable-ldrtl-xxxhdpi", "quantum_ic_arrow_back_white_24.png"
)
).forEach { resourceGroup ->
data.copyResources("settings", resourceGroup)
}
data.xmlEditor["AndroidManifest.xml"].use {
val manifestNode = it
.file
.getElementsByTagName("manifest")
.item(0) as Element
val element = it.file.createElement("uses-permission")
element.setAttribute("android:name", "android.permission.SCHEDULE_EXACT_ALARM")
manifestNode.appendChild(element)
data.xmlEditor["AndroidManifest.xml"].use { editor ->
editor.file.getElementsByTagName("manifest").item(0).also {
it.appendChild(it.ownerDocument.createElement("uses-permission").also { element ->
element.setAttribute("android:name", "android.permission.SCHEDULE_EXACT_ALARM")
})
}
}
revancedPreferencesEditor = data.xmlEditor["res/xml/revanced_prefs.xml"]
preferencesEditor = data.xmlEditor["res/xml/settings_fragment.xml"]
stringsEditor = data.xmlEditor["res/values/strings.xml"]
arraysEditor = data.xmlEditor["res/values/arrays.xml"]
// Add the ReVanced settings to the YouTube settings
val youtubePackage = "com.google.android.youtube"
SettingsPatch.addPreference(
Preference(
StringResource("revanced_settings", "ReVanced"),
Preference.Intent(
youtubePackage, "revanced_settings", "com.google.android.libraries.social.licenses.LicenseActivity"
),
StringResource("revanced_settings_summary", "ReVanced specific settings"),
)
)
return PatchResultSuccess()
}
internal companion object {
// if this is not null, all intents will be renamed to this
var overrideIntentsTargetPackage: String? = null
private var revancedPreferenceNode: Node? = null
private var preferencesNode: Node? = null
private var stringsNode: Node? = null
private var arraysNode: Node? = null
private var strings = mutableListOf<StringResource>()
private var revancedPreferencesEditor: DomFileEditor? = null
set(value) {
field = value
revancedPreferenceNode = value.getNode("PreferenceScreen")
}
private var preferencesEditor: DomFileEditor? = null
set(value) {
field = value
preferencesNode = value.getNode("PreferenceScreen")
}
private var stringsEditor: DomFileEditor? = null
set(value) {
field = value
stringsNode = value.getNode("resources")
}
private var arraysEditor: DomFileEditor? = null
set(value) {
field = value
arraysNode = value.getNode("resources")
}
/**
* Add a new string to the resources.
*
* @param identifier The key of the string.
* @param value The value of the string.
* @throws IllegalArgumentException if the string already exists.
*/
fun addString(identifier: String, value: String, formatted: Boolean) =
StringResource(identifier, value, formatted).include()
/**
* Add an array to the resources.
*
* @param arrayResource The array resource to add.
*/
fun addArray(arrayResource: ArrayResource) {
arraysNode!!.appendChild(arraysNode!!.ownerDocument.createElement("string-array").also { arrayNode ->
arrayResource.items.forEach { item ->
item.include()
arrayNode.setAttribute("name", item.name)
arrayNode.appendChild(arrayNode.ownerDocument.createElement("item").also { itemNode ->
itemNode.textContent = item.value
})
}
})
}
/**
* Add a preference screen to the settings.
*
* @param preferenceScreen The name of the preference screen.
*/
fun addPreferenceScreen(preferenceScreen: PreferenceScreen) =
revancedPreferenceNode!!.addPreference(preferenceScreen)
/**
* Add a preference fragment to the preferences.
*
* @param preference The preference to add.
*/
fun addPreference(preference: Preference) {
preferencesNode!!.appendChild(preferencesNode.createElement(preference.tag).also { preferenceNode ->
preferenceNode.setAttribute(
"android:title", "@string/${preference.title.also { it.include() }.name}"
)
preference.summary?.let { summary ->
preferenceNode.setAttribute("android:summary", "@string/${summary.also { it.include() }.name}")
}
preferenceNode.appendChild(preferenceNode.createElement("intent").also { intentNode ->
intentNode.setAttribute("android:targetPackage", preference.intent.targetPackage)
intentNode.setAttribute("android:data", preference.intent.data)
intentNode.setAttribute("android:targetClass", preference.intent.targetClass)
})
})
}
/**
* Add a preference to the settings.
*
* @param preference The preference to add.
*/
private fun Node.addPreference(preference: BasePreference) {
// add a summary to the element
fun Element.addSummary(summaryResource: StringResource?, summaryType: SummaryType = SummaryType.DEFAULT) =
summaryResource?.let { summary ->
setAttribute("android:${summaryType.type}", "@string/${summary.also { it.include() }.name}")
}
fun <T> Element.addDefault(default: T) {
default?.let {
setAttribute(
"android:defaultValue", when (it) {
is Boolean -> if (it) "true" else "false"
is String -> it
else -> throw IllegalArgumentException("Unsupported default value type: ${it::class.java.name}")
}
)
}
}
val preferenceElement = ownerDocument.createElement(preference.tag)
preferenceElement.setAttribute("android:key", preference.key)
preferenceElement.setAttribute("android:title", "@string/${preference.title.also { it.include() }.name}")
when (preference) {
is PreferenceScreen -> {
for (childPreference in preference.preferences) preferenceElement.addPreference(childPreference)
preferenceElement.addSummary(preference.summary)
}
is SwitchPreference -> {
preferenceElement.addDefault(preference.default)
preferenceElement.addSummary(preference.summaryOn, SummaryType.ON)
preferenceElement.addSummary(preference.summaryOff, SummaryType.OFF)
}
is TextPreference -> {
preferenceElement.setAttribute("android:inputType", preference.inputType.type)
preferenceElement.addDefault(preference.default)
preferenceElement.addSummary(preference.summary)
}
}
appendChild(preferenceElement)
}
/**
* Add a new string to the resources.
*
* @throws IllegalArgumentException if the string already exists.
*/
private fun StringResource.include() {
if (strings.any { it.name == name }) return
strings.add(this)
}
private fun DomFileEditor?.getNode(tagName: String) = this!!.file.getElementsByTagName(tagName).item(0)
private fun Node?.createElement(tagName: String) = this!!.ownerDocument.createElement(tagName)
private enum class SummaryType(val type: String) {
DEFAULT("summary"), ON("summaryOn"), OFF("summaryOff")
}
}
override fun close() {
// merge all strings, skip duplicates
strings.forEach { stringResource ->
stringsNode!!.appendChild(stringsNode!!.ownerDocument.createElement("string").also { stringElement ->
stringElement.setAttribute("name", stringResource.name)
// if the string is un-formatted, explicitly add the formatted attribute
if (!stringResource.formatted) stringElement.setAttribute("formatted", "false")
stringElement.textContent = stringResource.value
})
}
// rename the intent package names if it was set
overrideIntentsTargetPackage?.let { packageName ->
val preferences = preferencesEditor!!.getNode("PreferenceScreen").childNodes
for (i in 1 until preferences.length) {
val preferenceNode = preferences.item(i)
// preferences have a child node with the intent tag, skip over every other node
if (preferenceNode.childNodes.length == 0) continue
val intentNode = preferenceNode.firstChild
// if the node doesn't have a target package attribute, skip it
val targetPackageAttribute = intentNode.attributes.getNamedItem("android:targetPackage") ?: continue
// do not replace intent target package if the package name is not from YouTube
val youtubePackage = "com.google.android.youtube"
if (targetPackageAttribute.nodeValue != youtubePackage) continue
// replace the target package name
intentNode.attributes.setNamedItem(preferenceNode.ownerDocument.createAttribute("android:targetPackage")
.also { attribute ->
attribute.value = packageName
})
}
}
revancedPreferencesEditor?.close()
preferencesEditor?.close()
stringsEditor?.close()
arraysEditor?.close()
}
}