feat: Move strings to resources for localization (#2440)

Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
Co-authored-by: semantic-release-bot <semantic-release-bot@martynus.net>
Co-authored-by: Aunali321 <aunvakil.aa@gmail.com>

BREAKING CHANGE: Various APIs have been changed.
This commit is contained in:
LisoUseInAIKyrios
2024-01-27 05:34:01 +04:00
committed by GitHub
parent cb7ecb6443
commit 060ab8fbfe
201 changed files with 3411 additions and 3784 deletions

View File

@ -0,0 +1,8 @@
package app.revanced.patches.shared.fingerprints
import app.revanced.patcher.fingerprint.MethodFingerprint
internal object CastContextFetchFingerprint : MethodFingerprint(
strings = listOf("Error fetching CastContext.")
)

View File

@ -1,9 +0,0 @@
package app.revanced.patches.shared.fingerprints
import app.revanced.patches.shared.integrations.AbstractIntegrationsPatch.IntegrationsFingerprint
object HomeActivityFingerprint : IntegrationsFingerprint(
customFingerprint = { methodDef, classDef ->
methodDef.name == "onCreate" && classDef.type.endsWith("Shell_HomeActivity;")
},
)

View File

@ -2,14 +2,15 @@ package app.revanced.patches.shared.misc.gms
import app.revanced.patcher.PatchClass
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.getInstructions
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.fingerprint.MethodFingerprint
import app.revanced.patcher.patch.BytecodePatch
import app.revanced.patches.all.misc.packagename.ChangePackageNamePatch
import app.revanced.patches.shared.misc.gms.AbstractGmsCoreSupportPatch.Constants.ACTIONS
import app.revanced.patches.shared.misc.gms.AbstractGmsCoreSupportPatch.Constants.AUTHORITIES
import app.revanced.patches.shared.misc.gms.AbstractGmsCoreSupportPatch.Constants.PERMISSIONS
import app.revanced.patches.shared.misc.gms.BaseGmsCoreSupportPatch.Constants.ACTIONS
import app.revanced.patches.shared.misc.gms.BaseGmsCoreSupportPatch.Constants.AUTHORITIES
import app.revanced.patches.shared.misc.gms.BaseGmsCoreSupportPatch.Constants.PERMISSIONS
import app.revanced.patches.shared.misc.gms.fingerprints.GmsCoreSupportFingerprint
import app.revanced.patches.shared.misc.gms.fingerprints.GmsCoreSupportFingerprint.GET_GMS_CORE_VENDOR_METHOD_NAME
import app.revanced.util.exception
@ -31,17 +32,21 @@ import com.android.tools.smali.dexlib2.util.MethodUtil
* @param toPackageName The package name to fall back to if no custom package name is specified in patch options.
* @param primeMethodFingerprint The fingerprint of the "prime" method that needs to be patched.
* @param earlyReturnFingerprints The fingerprints of methods that need to be returned early.
* @param abstractGmsCoreSupportResourcePatch The corresponding resource patch that is used to patch the resources.
* @param mainActivityOnCreateFingerprint The fingerprint of the main activity's onCreate method.
* @param integrationsPatchDependency The patch responsible for the integrations.
* @param gmsCoreSupportResourcePatch The corresponding resource patch that is used to patch the resources.
* @param dependencies Additional dependencies of this patch.
* @param compatiblePackages The compatible packages of this patch.
* @param fingerprints The fingerprints of this patch.
*/
abstract class AbstractGmsCoreSupportPatch(
abstract class BaseGmsCoreSupportPatch(
private val fromPackageName: String,
private val toPackageName: String,
private val primeMethodFingerprint: MethodFingerprint,
private val earlyReturnFingerprints: Set<MethodFingerprint>,
abstractGmsCoreSupportResourcePatch: AbstractGmsCoreSupportResourcePatch,
private val mainActivityOnCreateFingerprint: MethodFingerprint,
private val integrationsPatchDependency: PatchClass,
gmsCoreSupportResourcePatch: BaseGmsCoreSupportResourcePatch,
dependencies: Set<PatchClass> = setOf(),
compatiblePackages: Set<CompatiblePackage>? = null,
fingerprints: Set<MethodFingerprint> = emptySet(),
@ -49,14 +54,18 @@ abstract class AbstractGmsCoreSupportPatch(
name = "GmsCore support",
description = "Allows patched Google apps to run without root and under a different package name " +
"by using GmsCore instead of Google Play Services.",
dependencies = setOf(ChangePackageNamePatch::class, abstractGmsCoreSupportResourcePatch::class) + dependencies,
dependencies = setOf(
ChangePackageNamePatch::class,
gmsCoreSupportResourcePatch::class,
integrationsPatchDependency
) + dependencies,
compatiblePackages = compatiblePackages,
fingerprints = setOf(GmsCoreSupportFingerprint) + fingerprints,
fingerprints = setOf(GmsCoreSupportFingerprint, mainActivityOnCreateFingerprint) + fingerprints,
requiresIntegrations = true
) {
init {
// Manually register all options of the resource patch so that they are visible in the patch API.
abstractGmsCoreSupportResourcePatch.options.values.forEach(options::register)
gmsCoreSupportResourcePatch.options.values.forEach(options::register)
}
internal abstract val gmsCoreVendor: String?
@ -84,6 +93,12 @@ abstract class AbstractGmsCoreSupportPatch(
// Return these methods early to prevent the app from crashing.
earlyReturnFingerprints.toList().returnEarly()
// Check the availability of GmsCore.
mainActivityOnCreateFingerprint.result?.mutableMethod?.addInstruction(
1, // Hack to not disturb other patches (such as the integrations patch).
"invoke-static {}, Lapp/revanced/integrations/youtube/patches/GmsCoreSupport;->checkAvailability()V"
) ?: throw mainActivityOnCreateFingerprint.exception
// Change the vendor of GmsCore in ReVanced Integrations.
GmsCoreSupportFingerprint.result?.mutableClass?.methods
?.single { it.name == GET_GMS_CORE_VENDOR_METHOD_NAME }

View File

@ -5,7 +5,7 @@ import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.ResourcePatch
import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringPatchOption
import app.revanced.patches.all.misc.packagename.ChangePackageNamePatch
import app.revanced.util.mergeStrings
import app.revanced.patches.all.misc.resources.AddResourcesPatch
import org.w3c.dom.Element
import org.w3c.dom.Node
@ -18,12 +18,12 @@ import org.w3c.dom.Node
* @param spoofedPackageSignature The signature of the package to spoof to.
* @param dependencies Additional dependencies of this patch.
*/
abstract class AbstractGmsCoreSupportResourcePatch(
abstract class BaseGmsCoreSupportResourcePatch(
private val fromPackageName: String,
private val toPackageName: String,
private val spoofedPackageSignature: String,
dependencies: Set<PatchClass> = setOf()
) : ResourcePatch(dependencies = setOf(ChangePackageNamePatch::class) + dependencies) {
) : ResourcePatch(dependencies = setOf(ChangePackageNamePatch::class, AddResourcesPatch::class) + dependencies) {
internal val gmsCoreVendorOption = stringPatchOption(
key = "gmsCoreVendor",
default = "com.mgoogle",
@ -39,7 +39,8 @@ abstract class AbstractGmsCoreSupportResourcePatch(
protected val gmsCoreVendor by gmsCoreVendorOption
override fun execute(context: ResourceContext) {
context.mergeStrings("gms/host/values/strings.xml")
AddResourcesPatch(BaseGmsCoreSupportResourcePatch::class)
context.patchManifest()
context.addSpoofingMetadata()
}

View File

@ -2,7 +2,7 @@ package app.revanced.patches.shared.misc.gms.fingerprints
import app.revanced.patcher.fingerprint.MethodFingerprint
object GmsCoreSupportFingerprint : MethodFingerprint(
internal object GmsCoreSupportFingerprint : MethodFingerprint(
customFingerprint = { _, classDef ->
classDef.type.endsWith("GmsCoreSupport;")
}

View File

@ -1,16 +1,16 @@
package app.revanced.patches.shared.integrations
package app.revanced.patches.shared.misc.integrations
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.fingerprint.MethodFingerprint
import app.revanced.patcher.patch.BytecodePatch
import app.revanced.patcher.patch.PatchException
import app.revanced.patches.shared.integrations.AbstractIntegrationsPatch.IntegrationsFingerprint.RegisterResolver
import app.revanced.patches.shared.misc.integrations.BaseIntegrationsPatch.IntegrationsFingerprint.IRegisterResolver
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.iface.Method
abstract class AbstractIntegrationsPatch(
abstract class BaseIntegrationsPatch(
private val hooks: Set<IntegrationsFingerprint>
) : BytecodePatch(hooks) {
@ -29,13 +29,15 @@ abstract class AbstractIntegrationsPatch(
"Integrations have not been merged yet. This patch can not succeed without merging the integrations."
)
for (hook in hooks) hook.invoke(INTEGRATIONS_CLASS_DESCRIPTOR)
hooks.forEach { hook ->
hook.invoke(INTEGRATIONS_CLASS_DESCRIPTOR)
}
}
/**
* [MethodFingerprint] for integrations.
*
* @param contextRegisterResolver A [RegisterResolver] to get the register.
* @param contextRegisterResolver A [IRegisterResolver] to get the register.
* @see MethodFingerprint
*/
abstract class IntegrationsFingerprint(
@ -45,7 +47,7 @@ abstract class AbstractIntegrationsPatch(
opcodes: Iterable<Opcode?>? = null,
strings: Iterable<String>? = null,
customFingerprint: ((methodDef: Method, classDef: ClassDef) -> Boolean)? = null,
private val contextRegisterResolver: (Method) -> Int = object : RegisterResolver {}
private val contextRegisterResolver: (Method) -> Int = object : IRegisterResolver {}
) : MethodFingerprint(
returnType,
accessFlags,
@ -66,7 +68,7 @@ abstract class AbstractIntegrationsPatch(
} ?: throw PatchException("Could not find hook target fingerprint.")
}
interface RegisterResolver : (Method) -> Int {
interface IRegisterResolver : (Method) -> Int {
override operator fun invoke(method: Method) = method.implementation!!.registerCount - 1
}
}

View File

@ -1,4 +1,4 @@
package app.revanced.patches.shared.mapping.misc
package app.revanced.patches.shared.misc.mapping
import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.ResourcePatch

View File

@ -0,0 +1,63 @@
package app.revanced.patches.shared.misc.settings
import app.revanced.patcher.PatchClass
import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.ResourcePatch
import app.revanced.patches.all.misc.resources.AddResourcesPatch
import app.revanced.patches.shared.misc.settings.preference.BasePreference
import app.revanced.patches.shared.misc.settings.preference.IntentPreference
import app.revanced.util.ResourceGroup
import app.revanced.util.copyResources
import app.revanced.util.getNode
import org.w3c.dom.Node
import java.io.Closeable
/**
* A resource patch that adds settings to a settings fragment.
*
* @param rootPreference A pair of an intent preference and the name of the fragment file to add it to.
* If null, no preference will be added.
* @param dependencies Additional dependencies of this patch.
*/
abstract class BaseSettingsResourcePatch(
private val rootPreference: Pair<IntentPreference, String>? = null,
dependencies: Set<PatchClass> = emptySet()
) : ResourcePatch(
dependencies = setOf(AddResourcesPatch::class) + dependencies
), MutableSet<BasePreference> by mutableSetOf(), Closeable {
private lateinit var context: ResourceContext
override fun execute(context: ResourceContext) {
context.copyResources(
"settings",
ResourceGroup("xml", "revanced_prefs.xml")
)
this.context = context
AddResourcesPatch(BaseSettingsResourcePatch::class)
}
override fun close() {
fun Node.addPreference(preference: BasePreference) {
preference.serialize(ownerDocument) { resource ->
// TODO: Currently, resources can only be added to "values", which may not be the correct place.
// It may be necessary to ask for the desired resourceValue in the future.
AddResourcesPatch("values", resource)
}.let(this::appendChild)
}
// Add the root preference to an existing fragment if needed.
rootPreference?.let { (intentPreference, fragment) ->
context.xmlEditor["res/xml/$fragment.xml"].use {
it.getNode("PreferenceScreen").addPreference(intentPreference)
}
}
// Add all preferences to the ReVanced fragment.
context.xmlEditor["res/xml/revanced_prefs.xml"].use { editor ->
val revancedPreferenceScreenNode = editor.getNode("PreferenceScreen")
forEach { revancedPreferenceScreenNode.addPreference(it) }
}
}
}

View File

@ -0,0 +1,62 @@
package app.revanced.patches.shared.misc.settings.preference
import app.revanced.util.resource.BaseResource
import org.w3c.dom.Document
import org.w3c.dom.Element
/**
* Base preference class for all preferences.
*
* @param key The key of the preference. If null, other parameters must be specified.
* @param titleKey The key of the preference title.
* @param summaryKey The key of the preference summary.
* @param tag The tag or full class name of the preference.
*/
@Suppress("MemberVisibilityCanBePrivate")
abstract class BasePreference(
val key: String? = null,
val titleKey: String = "${key}_title",
val summaryKey: String? = "${key}_summary",
val tag: String
) {
/**
* Serialize preference element to XML.
* Overriding methods should invoke super and operate on its return value.
*
* @param resourceCallback A callback for additional resources.
* @param ownerDocument Target document to create elements from.
*
* @return The serialized element.
*/
open fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit): Element =
ownerDocument.createElement(tag).apply {
setAttribute("android:key", key)
setAttribute("android:title", "@string/${titleKey}")
summaryKey?.let { addSummary(it) }
}
override fun hashCode(): Int {
var result = key?.hashCode() ?: 0
result = 31 * result + titleKey.hashCode()
result = 31 * result + tag.hashCode()
return result
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as BasePreference
if (key != other.key) return false
if (titleKey != other.titleKey) return false
if (tag != other.tag) return false
return true
}
companion object {
fun Element.addSummary(summaryKey: String, summaryType: SummaryType = SummaryType.DEFAULT) =
setAttribute("android:${summaryType.type}", "@string/$summaryKey")
}
}

View File

@ -0,0 +1,93 @@
package app.revanced.patches.shared.misc.settings.preference
import java.io.Closeable
abstract class BasePreferenceScreen(
private val root: MutableSet<Screen> = mutableSetOf()
) : Closeable {
override fun close() {
if (root.isEmpty()) return
root.forEach { preference ->
commit(preference.transform())
}
}
/**
* Finalize and insert root preference into resource patch
*/
abstract fun commit(screen: PreferenceScreen)
open inner class Screen(
key: String? = null,
titleKey: String = "${key}_title",
private val summaryKey: String? = "${key}_summary",
preferences: MutableSet<BasePreference> = mutableSetOf(),
val categories: MutableSet<Category> = mutableSetOf()
) : BasePreferenceCollection(key, titleKey, preferences) {
/**
* Initialize using title and summary keys with suffix "_title" and "_summary".
*/
constructor(
key: String? = null,
preferences: MutableSet<BasePreference> = mutableSetOf(),
categories: MutableSet<Category> = mutableSetOf()
) : this(key, key + "_title", key + "_summary", preferences, categories)
override fun transform(): PreferenceScreen {
return PreferenceScreen(
key,
titleKey,
summaryKey,
// Screens and preferences are sorted at runtime by integrations code,
// so they appear in alphabetical order for the localized language in use.
preferences = preferences + categories.map { it.transform() }
)
}
private fun ensureScreenInserted() {
// Add to screens if not yet done
if (!root.contains(this))
root.add(this)
}
fun addPreferences(vararg preferences: BasePreference) {
ensureScreenInserted()
this.preferences.addAll(preferences)
}
open inner class Category(
key: String? = null,
titleKey: String = "${key}_title",
preferences: MutableSet<BasePreference> = mutableSetOf()
) : BasePreferenceCollection(key, titleKey, preferences) {
override fun transform(): PreferenceCategory {
return PreferenceCategory(
key,
titleKey,
preferences = preferences
)
}
fun addPreferences(vararg preferences: BasePreference) {
ensureScreenInserted()
// Add to the categories if not done yet.
if (!categories.contains(this))
categories.add(this)
this.preferences.addAll(preferences)
}
}
}
abstract class BasePreferenceCollection(
val key: String? = null,
val titleKey: String = "${key}_title",
val preferences: MutableSet<BasePreference> = mutableSetOf()
) {
abstract fun transform(): BasePreference
}
}

View File

@ -1,4 +1,4 @@
package app.revanced.patches.shared.settings.preference.impl
package app.revanced.patches.shared.misc.settings.preference
enum class InputType(val type: String) {
TEXT("text"),

View File

@ -0,0 +1,56 @@
package app.revanced.patches.shared.misc.settings.preference
import app.revanced.patches.shared.misc.settings.preference.IntentPreference.Intent
import app.revanced.util.resource.BaseResource
import org.w3c.dom.Document
/**
* A preference that opens an intent.
*
* @param key The preference key. If null, other parameters must be specified.
* @param titleKey The preference title key.
* @param summaryKey The preference summary key.
* @param tag The preference tag.
* @param intent The intent to open.
*
* @see Intent
*/
class IntentPreference(
key: String? = null,
titleKey: String = "${key}_title",
summaryKey: String? = "${key}_summary",
tag: String = "Preference",
val intent: Intent,
) : BasePreference(null, titleKey, summaryKey, tag) {
override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit) =
super.serialize(ownerDocument, resourceCallback).apply {
appendChild(ownerDocument.createElement("intent").also { intentNode ->
intentNode.setAttribute("android:data", intent.data)
intentNode.setAttribute("android:targetClass", intent.targetClass)
intentNode.setAttribute("android:targetPackage", intent.targetPackageSupplier())
})
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as IntentPreference
return intent == other.intent
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + intent.hashCode()
return result
}
data class Intent(
internal val data: String,
internal val targetClass: String,
internal val targetPackageSupplier: () -> String,
)
}

View File

@ -0,0 +1,72 @@
package app.revanced.patches.shared.misc.settings.preference
import app.revanced.util.resource.ArrayResource
import app.revanced.util.resource.BaseResource
import org.w3c.dom.Document
/**
* List preference.
*
* @param key The preference key. If null, other parameters must be specified.
* @param titleKey The preference title key.
* @param summaryKey The preference summary key.
* @param tag The preference tag.
* @param entriesKey The entries array key.
* @param entryValuesKey The entry values array key.
*/
@Suppress("MemberVisibilityCanBePrivate")
class ListPreference(
key: String? = null,
titleKey: String = "${key}_title",
summaryKey: String? = "${key}_summary",
tag: String = "ListPreference",
val entriesKey: String? = "${key}_entries",
val entryValuesKey: String? = "${key}_entry_values"
) : BasePreference(key, titleKey, summaryKey, tag) {
var entries: ArrayResource? = null
private set
var entryValues: ArrayResource? = null
private set
/**
* List preference.
*
* @param key The preference key. If null, other parameters must be specified.
* @param titleKey The preference title key.
* @param summaryKey The preference summary key.
* @param tag The preference tag.
* @param entries The entries array.
* @param entryValues The entry values array.
*/
constructor(
key: String? = null,
titleKey: String = "${key}_title",
summaryKey: String? = "${key}_summary",
tag: String = "ListPreference",
entries: ArrayResource,
entryValues: ArrayResource
) : this(key, titleKey, summaryKey, tag, entries.name, entryValues.name) {
this.entries = entries
this.entryValues = entryValues
}
override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit) =
super.serialize(ownerDocument, resourceCallback).apply {
val entriesArrayName = entries?.also { resourceCallback.invoke(it) }?.name ?: entriesKey
val entryValuesArrayName = entryValues?.also { resourceCallback.invoke(it) }?.name ?: entryValuesKey
entriesArrayName?.let {
setAttribute(
"android:entries",
"@array/$it"
)
}
entryValuesArrayName?.let {
setAttribute(
"android:entryValues",
"@array/$it"
)
}
}
}

View File

@ -0,0 +1,26 @@
package app.revanced.patches.shared.misc.settings.preference
import app.revanced.util.resource.BaseResource
import org.w3c.dom.Document
/**
* A non-interactive preference.
*
* @param key The preference key.
* @param summaryKey The preference summary key.
* @param tag The preference tag.
* @param selectable Whether the preference is selectable.
*/
@Suppress("MemberVisibilityCanBePrivate")
class NonInteractivePreference(
key: String,
summaryKey: String? = "${key}_summary",
tag: String = "Preference",
val selectable: Boolean = false
) : BasePreference(null, "${key}_title", summaryKey, tag) {
override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit) =
super.serialize(ownerDocument, resourceCallback).apply {
setAttribute("android:selectable", selectable.toString())
}
}

View File

@ -0,0 +1,28 @@
package app.revanced.patches.shared.misc.settings.preference
import app.revanced.util.resource.BaseResource
import org.w3c.dom.Document
/**
* A preference category.
*
* @param key The key of the preference. If null, other parameters must be specified.
* @param titleKey The key of the preference title.
* @param tag The tag or full class name of the preference.
* @param preferences The preferences in this category.
*/
@Suppress("MemberVisibilityCanBePrivate")
open class PreferenceCategory(
key: String? = null,
titleKey: String = "${key}_title",
tag: String = "PreferenceCategory",
val preferences: Set<BasePreference>
) : BasePreference(key, titleKey, null, tag) {
override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit) =
super.serialize(ownerDocument, resourceCallback).apply {
preferences.forEach {
appendChild(it.serialize(ownerDocument, resourceCallback))
}
}
}

View File

@ -0,0 +1,30 @@
package app.revanced.patches.shared.misc.settings.preference
import app.revanced.util.resource.BaseResource
import org.w3c.dom.Document
/**
* A preference screen.
*
* @param key The key of the preference. If null, other parameters must be specified.
* @param titleKey The key of the preference title.
* @param summaryKey The key of the preference summary.
* @param tag The tag or full class name of the preference.
* @param preferences The preferences in this screen.
*/
@Suppress("MemberVisibilityCanBePrivate")
open class PreferenceScreen(
key: String? = null,
titleKey: String = "${key}_title",
summaryKey: String? = "${key}_summary",
tag: String = "PreferenceScreen",
val preferences: Set<BasePreference>
) : BasePreference(key, titleKey, summaryKey, tag) {
override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit) =
super.serialize(ownerDocument, resourceCallback).apply {
preferences.forEach {
appendChild(it.serialize(ownerDocument, resourceCallback))
}
}
}

View File

@ -1,4 +1,4 @@
package app.revanced.patches.shared.settings.preference
package app.revanced.patches.shared.misc.settings.preference
enum class SummaryType(val type: String) {
DEFAULT("summary"), ON("summaryOn"), OFF("summaryOff")

View File

@ -0,0 +1,28 @@
package app.revanced.patches.shared.misc.settings.preference
import app.revanced.util.resource.BaseResource
import org.w3c.dom.Document
/**
* A switch preference.
*
* @param key The preference key. If null, other parameters must be specified.
* @param titleKey The preference title key.
* @param tag The preference tag.
* @param summaryOnKey The preference summary-on key.
* @param summaryOffKey The preference summary-off key.
*/
@Suppress("MemberVisibilityCanBePrivate")
class SwitchPreference(
key: String? = null,
titleKey: String = "${key}_title",
tag: String = "SwitchPreference",
val summaryOnKey: String = "${key}_summary_on",
val summaryOffKey: String = "${key}_summary_off"
) : BasePreference(key, titleKey, null, tag) {
override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit) =
super.serialize(ownerDocument, resourceCallback).apply {
addSummary(summaryOnKey, SummaryType.ON)
addSummary(summaryOffKey, SummaryType.OFF)
}
}

View File

@ -0,0 +1,28 @@
package app.revanced.patches.shared.misc.settings.preference
import app.revanced.util.resource.BaseResource
import org.w3c.dom.Document
/**
* A text preference.
*
* @param key The preference key. If null, other parameters must be specified.
* @param titleKey The preference title key.
* @param summaryKey The preference summary key.
* @param tag The preference tag.
* @param inputType The preference input type.
*/
@Suppress("MemberVisibilityCanBePrivate")
class TextPreference(
key: String? = null,
titleKey: String = "${key}_title",
summaryKey: String? = "${key}_summary",
tag: String = "app.revanced.integrations.shared.settings.preference.ResettableEditTextPreference",
val inputType: InputType = InputType.TEXT
) : BasePreference(key, titleKey, summaryKey, tag) {
override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit) =
super.serialize(ownerDocument, resourceCallback).apply {
setAttribute("android:inputType", inputType.type)
}
}

View File

@ -1,124 +0,0 @@
package app.revanced.patches.shared.settings
import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.ResourcePatch
import app.revanced.patcher.util.DomFileEditor
import app.revanced.patches.shared.settings.preference.BasePreference
import app.revanced.patches.shared.settings.preference.BaseResource
import app.revanced.patches.shared.settings.preference.addPreference
import app.revanced.patches.shared.settings.preference.addResource
import app.revanced.patches.shared.settings.preference.impl.ArrayResource
import app.revanced.patches.shared.settings.preference.impl.StringResource
import app.revanced.util.ResourceGroup
import app.revanced.util.copyResources
import app.revanced.util.mergeStrings
import org.w3c.dom.Node
import java.io.Closeable
/**
* Abstract settings resource patch
*
* @param preferenceFileName Name of the settings preference xml file
* @param sourceDirectory Source directory to copy the preference template from
*/
abstract class AbstractSettingsResourcePatch(
private val preferenceFileName: String,
private val sourceDirectory: String,
) : ResourcePatch(), Closeable {
override fun execute(context: ResourceContext) {
/* copy preference template from source dir */
context.copyResources(
sourceDirectory,
ResourceGroup(
"xml", "$preferenceFileName.xml"
)
)
/* prepare xml editors */
stringsEditor = context.xmlEditor["res/values/strings.xml"]
arraysEditor = context.xmlEditor["res/values/arrays.xml"]
revancedPreferencesEditor = context.xmlEditor["res/xml/$preferenceFileName.xml"]
context.mergeStrings("settings/host/values/strings.xml")
}
internal companion object {
private var revancedPreferenceNode: 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 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!!.addResource(arrayResource) { it.include() }
/**
* Add a preference to the settings.
*
* @param preference The preference to add.
*/
fun addPreference(preference: BasePreference) =
revancedPreferenceNode!!.addPreference(preference) { it.include() }
/**
* Add a new resource to the resources.
*
* @throws IllegalArgumentException if the resource already exists.
*/
internal fun BaseResource.include() {
when (this) {
is StringResource -> {
if (strings.any { it.name == name }) return
strings.add(this)
}
is ArrayResource -> addArray(this)
else -> throw NotImplementedError("Unsupported resource type")
}
}
internal fun DomFileEditor?.getNode(tagName: String) = this!!.file.getElementsByTagName(tagName).item(0)
}
override fun close() {
// merge all strings, skip duplicates
strings.forEach {
stringsNode!!.addResource(it)
}
revancedPreferencesEditor?.close()
stringsEditor?.close()
arraysEditor?.close()
}
}

View File

@ -1,34 +0,0 @@
package app.revanced.patches.shared.settings.preference
import app.revanced.patches.shared.settings.preference.impl.StringResource
import org.w3c.dom.Document
import org.w3c.dom.Element
/**
* Base preference class for all preferences.
*
* @param key The key of the preference.
* @param title The title of the preference.
* @param tag The full class name for the preference.
* @param summary The summary of the preference.
*/
abstract class BasePreference(
val key: String?,
val title: StringResource,
val summary: StringResource? = null,
val tag: String
) {
/**
* Serialize preference element to XML.
* Overriding methods should invoke super and operate on its return value.
* @param ownerDocument Target document to create elements from.
* @param resourceCallback Called when a resource has been processed.
* @return The serialized element.
*/
open fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit): Element =
ownerDocument.createElement(tag).apply {
if (key != null) setAttribute("android:key", key)
setAttribute("android:title", "@string/${title.also { resourceCallback.invoke(it) }.name}")
addSummary(summary?.also { resourceCallback.invoke(it) })
}
}

View File

@ -1,27 +0,0 @@
package app.revanced.patches.shared.settings.preference
import org.w3c.dom.Document
import org.w3c.dom.Element
/**
* Base resource class for all resources.
*
* @param name The name of the resource.
* @param tag The tag of the resource.
*/
abstract class BaseResource(
val name: String,
val tag: String
) {
/**
* Serialize resource element to XML.
* Overriding methods should invoke super and operate on its return value.
* @param ownerDocument Target document to create elements from.
* @param resourceCallback Called when a resource has been processed.
*/
open fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit = { }): Element {
return ownerDocument.createElement(tag).apply {
setAttribute("name", name)
}
}
}

View File

@ -1,48 +0,0 @@
package app.revanced.patches.shared.settings.preference
import app.revanced.patches.shared.settings.preference.impl.StringResource
import org.w3c.dom.Element
import org.w3c.dom.Node
/**
* Add a resource node child
*
* @param resource The resource to add.
* @param resourceCallback Called when a resource has been processed.
*/
internal fun Node.addResource(resource: BaseResource, resourceCallback: (BaseResource) -> Unit = { }) {
appendChild(resource.serialize(ownerDocument, resourceCallback))
}
/**
* Add a preference node child to the settings.
*
* @param preference The preference to add.
* @param resourceCallback Called when a resource has been processed.
*/
internal fun Node.addPreference(preference: BasePreference, resourceCallback: ((BaseResource) -> Unit) = { }) {
appendChild(preference.serialize(ownerDocument, resourceCallback))
}
internal fun Element.addSummary(summaryResource: StringResource?, summaryType: SummaryType = SummaryType.DEFAULT) =
summaryResource?.let { summary ->
setAttribute("android:${summaryType.type}", "@string/${summary.name}")
}
internal fun <T> Element.addDefault(default: T) {
if (default is Boolean && !(default as Boolean)) return // No need to include the default, as no value already means 'false'
default?.let {
setAttribute(
"android:defaultValue", when (it) {
is Boolean -> it.toString()
is String -> it
else -> throw IllegalArgumentException("Unsupported default value type: ${it::class.java.name}")
}
)
}
}
internal fun CharSequence.removePunctuation(): String {
val punctuation = "\\p{P}+".toRegex()
return this.replace(punctuation, "")
}

View File

@ -1,29 +0,0 @@
package app.revanced.patches.shared.settings.preference.impl
import app.revanced.patches.shared.settings.preference.BaseResource
import org.w3c.dom.Document
// TODO: allow specifying an array resource file instead of using a list of StringResources
/**
* An array resource.
*
* @param name The name of the array resource.
* @param items The items of the array resource.
*/
class ArrayResource(
name: String,
val items: List<StringResource>
) : BaseResource(name, "string-array") {
override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit) =
super.serialize(ownerDocument, resourceCallback).apply {
setAttribute("name", name)
items.forEach { item ->
resourceCallback.invoke(item)
this.appendChild(ownerDocument.createElement("item").also { itemNode ->
itemNode.textContent = "@string/${item.name}"
})
}
}
}

View File

@ -1,30 +0,0 @@
package app.revanced.patches.shared.settings.preference.impl
import app.revanced.patches.shared.settings.preference.BasePreference
import app.revanced.patches.shared.settings.preference.BaseResource
import app.revanced.patches.shared.settings.preference.addSummary
import org.w3c.dom.Document
/**
* List preference.
*
* @param key The key of the list preference.
* @param title The title of the list preference.
* @param entries The human-readable entries of the list preference.
* @param entryValues The entry values of the list preference.
* @param summary The summary of the list preference.
*/
class ListPreference(
key: String,
title: StringResource,
private val entries: ArrayResource,
private val entryValues: ArrayResource,
summary: StringResource? = null
) : BasePreference(key, title, summary, "ListPreference") {
override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit) =
super.serialize(ownerDocument, resourceCallback).apply {
setAttribute("android:entries", "@array/${entries.also { resourceCallback.invoke(it) }.name}")
setAttribute("android:entryValues", "@array/${entryValues.also { resourceCallback.invoke(it) }.name}")
addSummary(summary)
}
}

View File

@ -1,35 +0,0 @@
package app.revanced.patches.shared.settings.preference.impl
import app.revanced.patches.shared.settings.preference.BasePreference
import app.revanced.patches.shared.settings.preference.BaseResource
import app.revanced.patches.shared.settings.preference.addSummary
import org.w3c.dom.Document
import org.w3c.dom.Element
/**
* A non interactive preference.
*
* Not backed by any preference key/value,
* and cannot be changed by or interacted with by the user.
*
* @param title The title of the preference.
* @param summary The summary of the text preference.
* @param selectable If this preference responds to tapping.
* Setting to 'true' restores the horizontal dividers on the top and bottom,
* but tapping will still do nothing since this Preference has no key.
*/
class NonInteractivePreference(
title: StringResource,
summary: StringResource?,
tag: String = "Preference",
// If androidx.preference is later used, this can be changed to the show top/bottom dividers feature.
val selectable: Boolean = false
) : BasePreference(null, title, summary, tag) {
override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit): Element {
return super.serialize(ownerDocument, resourceCallback).apply {
addSummary(summary?.also { resourceCallback.invoke(it)
setAttribute("android:selectable", selectable.toString())
})
}
}
}

View File

@ -1,41 +0,0 @@
package app.revanced.patches.shared.settings.preference.impl
import app.revanced.patches.shared.settings.preference.BasePreference
import app.revanced.patches.shared.settings.preference.BaseResource
import org.w3c.dom.Document
/**
* A preference object.
*
* @param key The key of the preference.
* @param title The title of the preference.
* @param summary The summary of the text preference.
* @param intent The intent of the preference.
*/
class Preference(
key: String,
title: StringResource,
summary: StringResource,
val intent: Intent
) : BasePreference(key, title, summary, "Preference") {
constructor(
title: StringResource,
summary: StringResource,
intent: Intent
) : this("", title, summary, intent)
override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit) =
super.serialize(ownerDocument, resourceCallback).apply {
this.appendChild(ownerDocument.createElement("intent").also { intentNode ->
intentNode.setAttribute("android:targetPackage", intent.targetPackage)
intentNode.setAttribute("android:data", intent.data)
intentNode.setAttribute("android:targetClass", intent.targetClass)
})
}
class Intent(
internal val targetPackage: String,
internal val data: String,
internal val targetClass: String
)
}

View File

@ -1,26 +0,0 @@
package app.revanced.patches.shared.settings.preference.impl
import app.revanced.patches.shared.settings.preference.BasePreference
import app.revanced.patches.shared.settings.preference.BaseResource
import org.w3c.dom.Document
/**
* A preference category.
*
* @param key The key of the preference.
* @param title The title of the preference.
* @param preferences Child preferences of this category.
*/
open class PreferenceCategory(
key: String,
title: StringResource,
var preferences: List<BasePreference>,
tag: String = "PreferenceCategory"
) : BasePreference(key, title, null, tag) {
override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit) =
super.serialize(ownerDocument, resourceCallback).apply {
for (childPreference in preferences) {
this.appendChild(childPreference.serialize(ownerDocument, resourceCallback))
}
}
}

View File

@ -1,29 +0,0 @@
package app.revanced.patches.shared.settings.preference.impl
import app.revanced.patches.shared.settings.preference.BasePreference
import app.revanced.patches.shared.settings.preference.BaseResource
import app.revanced.patches.shared.settings.preference.addSummary
import org.w3c.dom.Document
/**
* A 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.
*/
open class PreferenceScreen(
key: String,
title: StringResource,
var preferences: List<BasePreference>,
summary: StringResource? = null
) : BasePreference(key, title, summary, "PreferenceScreen") {
override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit) =
super.serialize(ownerDocument, resourceCallback).apply {
addSummary(summary?.also { resourceCallback.invoke(it) })
for (childPreference in preferences)
this.appendChild(childPreference.serialize(ownerDocument, resourceCallback))
}
}

View File

@ -1,27 +0,0 @@
package app.revanced.patches.shared.settings.preference.impl
import app.revanced.patches.shared.settings.preference.BaseResource
import org.w3c.dom.Document
/**
* A string value.
* Represets a string 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.
*/
class StringResource(
name: String,
val value: String,
val formatted: Boolean = true
) : BaseResource(name, "string") {
override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit) =
super.serialize(ownerDocument, resourceCallback).apply {
// if the string is un-formatted, explicitly add the formatted attribute
if (!formatted) setAttribute("formatted", "false")
textContent = value
}
}

View File

@ -1,34 +0,0 @@
package app.revanced.patches.shared.settings.preference.impl
import app.revanced.patches.shared.settings.AbstractSettingsResourcePatch.Companion.include
import app.revanced.patches.shared.settings.preference.BasePreference
import app.revanced.patches.shared.settings.preference.BaseResource
import app.revanced.patches.shared.settings.preference.SummaryType
import app.revanced.patches.shared.settings.preference.addSummary
import org.w3c.dom.Document
import org.w3c.dom.Element
/**
* A switch preference.
*
* @param key The key of the switch.
* @param title The title 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.
* @param userDialogMessage The message to show in a dialog when the user toggles the preference.
*/
class SwitchPreference(
key: String, title: StringResource,
val summaryOn: StringResource,
val summaryOff: StringResource,
val userDialogMessage: StringResource? = null,
) : BasePreference(key, title, null, "SwitchPreference") {
override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit): Element {
userDialogMessage?.include()
return super.serialize(ownerDocument, resourceCallback).apply {
addSummary(summaryOn.also { resourceCallback.invoke(it) }, SummaryType.ON)
addSummary(summaryOff.also { resourceCallback.invoke(it) }, SummaryType.OFF)
}
}
}

View File

@ -1,27 +0,0 @@
package app.revanced.patches.shared.settings.preference.impl
import app.revanced.patches.shared.settings.preference.BasePreference
import app.revanced.patches.shared.settings.preference.BaseResource
import org.w3c.dom.Document
/**
* A 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 summary The summary of the text preference.
*/
class TextPreference(
key: String?,
title: StringResource,
summary: StringResource?,
val inputType: InputType = InputType.TEXT,
tag: String = "app.revanced.integrations.shared.settings.preference.ResettableEditTextPreference"
) : BasePreference(key, title, summary, tag) {
override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit) =
super.serialize(ownerDocument, resourceCallback).apply {
setAttribute("android:inputType", inputType.type)
}
}

View File

@ -1,98 +0,0 @@
package app.revanced.patches.shared.settings.util
import app.revanced.patches.shared.settings.preference.BasePreference
import app.revanced.patches.shared.settings.preference.impl.PreferenceCategory
import app.revanced.patches.shared.settings.preference.impl.PreferenceScreen
import app.revanced.patches.shared.settings.preference.impl.StringResource
import app.revanced.patches.shared.settings.preference.removePunctuation
import java.io.Closeable
abstract class AbstractPreferenceScreen(
private val root: MutableList<Screen> = mutableListOf()
) : Closeable {
override fun close() {
if (root.isEmpty())
return
for (preference in root.sortedBy { it.title }) {
commit(preference.transform())
}
}
/**
* Finalize and insert root preference into resource patch
*/
abstract fun commit(screen: PreferenceScreen)
open inner class Screen(
key: String,
title: String,
val summary: String? = null,
preferences: MutableList<BasePreference> = mutableListOf(),
val categories: MutableList<Category> = mutableListOf()
) : BasePreferenceCollection(key, title, preferences) {
override fun transform() = PreferenceScreen(
key,
StringResource(
"${key}_title", title
),
preferences.sortedWith(
compareBy(
{ it is PreferenceScreen },
{ it.title.value.removePunctuation().lowercase() }
)
) + categories.sortedBy {
it.title.removePunctuation().lowercase()
}.map {
it.transform()
},
summary?.let { summary ->
StringResource("${key}_summary", summary)
}
)
private fun ensureScreenInserted() {
// Add to screens if not yet done
if (!this@AbstractPreferenceScreen.root.contains(this))
this@AbstractPreferenceScreen.root.add(this)
}
fun addPreferences(vararg preferences: BasePreference) {
ensureScreenInserted()
this.preferences.addAll(preferences)
}
open inner class Category(
key: String,
title: String,
preferences: MutableList<BasePreference> = mutableListOf()
) : BasePreferenceCollection(key, title, preferences) {
override fun transform(): PreferenceCategory {
return PreferenceCategory(
key,
StringResource("${key}_title", title),
preferences.sortedBy { it.title.value.removePunctuation().lowercase() }
)
}
fun addPreferences(vararg preferences: BasePreference) {
ensureScreenInserted()
// Add to categories if not yet done
if (!this@Screen.categories.contains(this))
this@Screen.categories.add(this)
this.preferences.addAll(preferences)
}
}
}
abstract class BasePreferenceCollection(
val key: String,
val title: String,
val preferences: MutableList<BasePreference> = mutableListOf()
) {
abstract fun transform(): BasePreference
}
}