Skip to content

Commit 060ab8f

Browse files
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.
1 parent cb7ecb6 commit 060ab8f

File tree

201 files changed

+3407
-3780
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

201 files changed

+3407
-3780
lines changed

api/revanced-patches.api

+281-213
Large diffs are not rendered by default.

build.gradle.kts

+1-1
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ tasks {
7878
dependsOn(build)
7979

8080
classpath = sourceSets["main"].runtimeClasspath
81-
mainClass.set("app.revanced.meta.PatchesFileGenerator")
81+
mainClass.set("app.revanced.meta.IPatchesFileGenerator")
8282
}
8383

8484
// Required to run tasks because Gradle semantic-release plugin runs the publish task.

src/main/kotlin/app/revanced/meta/PatchesFileGenerator.kt renamed to src/main/kotlin/app/revanced/meta/IPatchesFileGenerator.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import app.revanced.patcher.PatchBundleLoader
44
import app.revanced.patcher.PatchSet
55
import java.io.File
66

7-
internal interface PatchesFileGenerator {
7+
internal interface IPatchesFileGenerator {
88
fun generate(patches: PatchSet)
99

1010
private companion object {
@@ -14,7 +14,7 @@ internal interface PatchesFileGenerator {
1414
).also { loader ->
1515
if (loader.isEmpty()) throw IllegalStateException("No patches found")
1616
}.let { bundle ->
17-
arrayOf(JsonGenerator()).forEach { generator -> generator.generate(bundle) }
17+
arrayOf(JsonPatchesFileGenerator()).forEach { generator -> generator.generate(bundle) }
1818
}
1919
}
20-
}
20+
}

src/main/kotlin/app/revanced/meta/JsonGenerator.kt renamed to src/main/kotlin/app/revanced/meta/JsonPatchesFileGenerator.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import app.revanced.patcher.patch.Patch
55
import com.google.gson.GsonBuilder
66
import java.io.File
77

8-
internal class JsonGenerator : PatchesFileGenerator {
8+
internal class JsonPatchesFileGenerator : IPatchesFileGenerator {
99
override fun generate(patches: PatchSet) = patches.map {
1010
JsonPatch(
1111
it.name!!,

src/main/kotlin/app/revanced/patches/all/connectivity/wifi/spoof/SpoofWifiPatch.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package app.revanced.patches.all.connectivity.wifi.spoof
22

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

src/main/kotlin/app/revanced/patches/all/misc/packagename/ChangePackageNamePatch.kt

+10-10
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,22 @@ object ChangePackageNamePatch : ResourcePatch(), Closeable {
4141
* @throws PatchOptionException.ValueValidationException If the package name is invalid.
4242
*/
4343
fun setOrGetFallbackPackageName(fallbackPackageName: String): String {
44-
val packageName = this.packageNameOption.value!!
44+
val packageName = packageNameOption.value!!
4545

46-
return if (packageName == this.packageNameOption.default)
47-
fallbackPackageName.also { this.packageNameOption.value = it }
46+
return if (packageName == packageNameOption.default)
47+
fallbackPackageName.also { packageNameOption.value = it }
4848
else
4949
packageName
5050
}
5151

5252
override fun close() = context.xmlEditor["AndroidManifest.xml"].use { editor ->
53-
val manifest = editor.file.getElementsByTagName("manifest").item(0) as Element
54-
val originalPackageName = manifest.getAttribute("package")
55-
56-
var replacementPackageName = this.packageNameOption.value
57-
if (replacementPackageName == this.packageNameOption.default)
58-
replacementPackageName = "$originalPackageName.revanced"
53+
val replacementPackageName = packageNameOption.value
5954

60-
manifest.setAttribute("package", replacementPackageName)
55+
val manifest = editor.file.getElementsByTagName("manifest").item(0) as Element
56+
manifest.setAttribute(
57+
"package",
58+
if (replacementPackageName != packageNameOption.default) replacementPackageName
59+
else "${manifest.getAttribute("package")}.revanced"
60+
)
6161
}
6262
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
package app.revanced.patches.all.misc.resources
2+
3+
import app.revanced.patcher.PatchClass
4+
import app.revanced.patcher.data.ResourceContext
5+
import app.revanced.patcher.patch.PatchException
6+
import app.revanced.patcher.patch.ResourcePatch
7+
import app.revanced.patcher.patch.annotation.Patch
8+
import app.revanced.patcher.util.DomFileEditor
9+
import app.revanced.patches.all.misc.resources.AddResourcesPatch.resources
10+
import app.revanced.util.*
11+
import app.revanced.util.resource.ArrayResource
12+
import app.revanced.util.resource.BaseResource
13+
import app.revanced.util.resource.StringResource
14+
import org.w3c.dom.Node
15+
import java.io.Closeable
16+
import java.util.*
17+
18+
/**
19+
* An identifier of an app. For example, `youtube`.
20+
*/
21+
private typealias AppId = String
22+
/**
23+
* An identifier of a patch. For example, `ad.general.HideAdsPatch`.
24+
*/
25+
private typealias PatchId = String
26+
27+
/**
28+
* A set of resources of a patch.
29+
*/
30+
private typealias PatchResources = MutableSet<BaseResource>
31+
/**
32+
* A map of resources belonging to a patch.
33+
*/
34+
private typealias AppResources = MutableMap<PatchId, PatchResources>
35+
/**
36+
* A map of resources belonging to an app.
37+
*/
38+
private typealias Resources = MutableMap<AppId, AppResources>
39+
40+
/**
41+
* The value of a resource.
42+
* For example, `values` or `values-de`.
43+
*/
44+
private typealias Value = String
45+
46+
@Patch(description = "Add resources such as strings or arrays to the app.")
47+
object AddResourcesPatch : ResourcePatch(), MutableMap<Value, MutableSet<BaseResource>> by mutableMapOf(), Closeable {
48+
private lateinit var context: ResourceContext
49+
50+
/**
51+
* A map of all resources associated by their value staged by [execute].
52+
*/
53+
private lateinit var resources: Map<Value, Resources>
54+
55+
/*
56+
The strategy of this patch is to stage resources present in `/resources/addresources`.
57+
These resources are organized by their respective value and patch.
58+
59+
On AddResourcesPatch#execute, all resources are staged in a temporary map.
60+
After that, other patches that depend on AddResourcesPatch can call
61+
AddResourcesPatch#invoke(PatchClass) to stage resources belonging to that patch
62+
from the temporary map to AddResourcesPatch.
63+
64+
After all patches that depend on AddResourcesPatch have been executed,
65+
AddResourcesPatch#close is finally called to add all staged resources to the app.
66+
*/
67+
override fun execute(context: ResourceContext) {
68+
this.context = context
69+
70+
resources = buildMap {
71+
/**
72+
* Puts resources under `/resources/addresources/<value>/<resourceKind>.xml` into the map.
73+
*
74+
* @param value The value of the resource. For example, `values` or `values-de`.
75+
* @param resourceKind The kind of the resource. For example, `strings` or `arrays`.
76+
* @param transform A function that transforms the [Node]s from the XML files to a [BaseResource].
77+
*/
78+
fun addResources(
79+
value: Value,
80+
resourceKind: String,
81+
transform: (Node) -> BaseResource,
82+
) {
83+
inputStreamFromBundledResource(
84+
"addresources",
85+
"$value/$resourceKind.xml"
86+
)?.let { stream ->
87+
// Add the resources associated with the given value to the map,
88+
// instead of overwriting it.
89+
// This covers the example case such as adding strings and arrays of the same value.
90+
getOrPut(value, ::mutableMapOf).apply {
91+
context.xmlEditor[stream].use {
92+
it.file.getElementsByTagName("app").asSequence().forEach { app ->
93+
val appId = app.attributes.getNamedItem("id").textContent
94+
95+
getOrPut(appId, ::mutableMapOf).apply {
96+
app.forEachChildElement { patch ->
97+
val patchId = patch.attributes.getNamedItem("id").textContent
98+
99+
getOrPut(patchId, ::mutableSetOf).apply {
100+
patch.forEachChildElement { resourceNode ->
101+
val resource = transform(resourceNode)
102+
103+
add(resource)
104+
}
105+
}
106+
}
107+
}
108+
}
109+
}
110+
}
111+
}
112+
}
113+
114+
// Stage all resources to a temporary map.
115+
// Staged resources consumed by AddResourcesPatch#invoke(PatchClass)
116+
// are later used in AddResourcesPatch#close.
117+
try {
118+
val addStringResources = { value: Value ->
119+
addResources(value, "strings", StringResource::fromNode)
120+
}
121+
Locale.getISOLanguages().asSequence().map { "values-$it" }.forEach { addStringResources(it) }
122+
addStringResources("values")
123+
124+
addResources("values", "arrays", ArrayResource::fromNode)
125+
} catch (e: Exception) {
126+
throw PatchException("Failed to read resources", e)
127+
}
128+
}
129+
}
130+
131+
/**
132+
* Adds a [BaseResource] to the map using [MutableMap.getOrPut].
133+
*
134+
* @param value The value of the resource. For example, `values` or `values-de`.
135+
* @param resource The resource to add.
136+
*
137+
* @return True if the resource was added, false if it already existed.
138+
*/
139+
operator fun invoke(value: Value, resource: BaseResource) =
140+
getOrPut(value, ::mutableSetOf).add(resource)
141+
142+
/**
143+
* Adds a list of [BaseResource]s to the map using [MutableMap.getOrPut].
144+
*
145+
* @param value The value of the resource. For example, `values` or `values-de`.
146+
* @param resources The resources to add.
147+
*
148+
* @return True if the resources were added, false if they already existed.
149+
*/
150+
operator fun invoke(value: Value, resources: Iterable<BaseResource>) =
151+
getOrPut(value, ::mutableSetOf).addAll(resources)
152+
153+
/**
154+
* Adds a [StringResource].
155+
*
156+
* @param name The name of the string resource.
157+
* @param value The value of the string resource.
158+
* @param formatted Whether the string resource is formatted. Defaults to `true`.
159+
* @param resourceValue The value of the resource. For example, `values` or `values-de`.
160+
*
161+
* @return True if the resource was added, false if it already existed.
162+
*/
163+
operator fun invoke(
164+
name: String,
165+
value: String,
166+
formatted: Boolean = true,
167+
resourceValue: Value = "values",
168+
) = invoke(resourceValue, StringResource(name, value, formatted))
169+
170+
/**
171+
* Adds an [ArrayResource].
172+
*
173+
* @param name The name of the array resource.
174+
* @param items The items of the array resource.
175+
*
176+
* @return True if the resource was added, false if it already existed.
177+
*/
178+
operator fun invoke(
179+
name: String,
180+
items: List<String>
181+
) = invoke("values", ArrayResource(name, items))
182+
183+
184+
/**
185+
* Puts all resources of any [Value] staged in [resources] for the given [PatchClass] to [AddResourcesPatch].
186+
*
187+
* @param patch The class of the patch to add resources for.
188+
* @param parseIds A function that parses the [AppId] and [PatchId] from the given [PatchClass].
189+
* This is used to access the resources in [resources] to stage them in [AddResourcesPatch].
190+
* The default implementation assumes that the [PatchClass] qualified name has the following format:
191+
* `<any>.<any>.<any>.<app id>.<patch id>`.
192+
*
193+
* @return True if any resources were added, false if none were added.
194+
*
195+
* @see AddResourcesPatch.close
196+
*/
197+
operator fun invoke(
198+
patch: PatchClass,
199+
parseIds: PatchClass.() -> Pair<AppId, PatchId> = {
200+
val qualifiedName = qualifiedName ?: throw PatchException("Patch qualified name is null")
201+
202+
// This requires qualifiedName to have the following format:
203+
// `<any>.<any>.<any>.<app id>.<patch id>`
204+
with(qualifiedName.split(".")) {
205+
if (size < 5) throw PatchException("Patch qualified name has invalid format")
206+
207+
val appId = this[3]
208+
val patchId = subList(4, size).joinToString(".")
209+
210+
appId to patchId
211+
}
212+
}
213+
): Boolean {
214+
val (appId, patchId) = patch.parseIds()
215+
216+
var result = false
217+
218+
// Stage resources for the given patch to AddResourcesPatch associated with their value.
219+
resources.forEach { (value, resources) ->
220+
resources[appId]?.get(patchId)?.let { patchResources ->
221+
if (invoke(value, patchResources)) result = true
222+
}
223+
}
224+
225+
return result
226+
}
227+
228+
/**
229+
* Adds all resources staged in [AddResourcesPatch] to the app.
230+
* This is called after all patches that depend on [AddResourcesPatch] have been executed.
231+
*/
232+
override fun close() {
233+
operator fun MutableMap<String, Pair<DomFileEditor, Node>>.invoke(
234+
value: Value,
235+
resource: BaseResource
236+
) {
237+
// TODO: Fix open-closed principle violation by modifying BaseResource#serialize so that it accepts
238+
// a Value and the map of editors. It will then get or put the editor suitable for its resource type
239+
// to serialize itself to it.
240+
val resourceFileName = when (resource) {
241+
is StringResource -> "strings"
242+
is ArrayResource -> "arrays"
243+
else -> throw NotImplementedError("Unsupported resource type")
244+
}
245+
246+
getOrPut(resourceFileName) {
247+
val targetFile = context["res/$value/$resourceFileName.xml"].also {
248+
it.parentFile?.mkdirs()
249+
it.createNewFile()
250+
}
251+
252+
context.xmlEditor[targetFile.path].let { editor ->
253+
// Save the target node here as well
254+
// in order to avoid having to call editor.getNode("resources")
255+
// every time addUsingEditors is called but also save the editor so that it can be closed later.
256+
editor to editor.getNode("resources")
257+
}
258+
}.let { (_, targetNode) ->
259+
targetNode.addResource(resource) { invoke(value, it) }
260+
}
261+
}
262+
263+
forEach { (value, resources) ->
264+
// A map of editors associated by their kind (e.g. strings, arrays).
265+
// Each editor is accompanied by the target node to which resources are added.
266+
// A map is used because Map#getOrPut allows opening a new editor for the duration of a resource value.
267+
// This is done to prevent having to open the files for every resource that is added.
268+
// Instead, it is cached once and reused for resources of the same value.
269+
// This map is later accessed to close all editors for the current resource value.
270+
val resourceFileEditors = mutableMapOf<String, Pair<DomFileEditor, Node>>()
271+
272+
resources.forEach { resource -> resourceFileEditors(value, resource) }
273+
274+
resourceFileEditors.values.forEach { (editor, _) -> editor.close() }
275+
}
276+
}
277+
}
+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import com.android.tools.smali.dexlib2.iface.Method
99
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
1010

1111
@Suppress("MemberVisibilityCanBePrivate")
12-
abstract class AbstractTransformInstructionsPatch<T> : BytecodePatch() {
12+
abstract class BaseTransformInstructionsPatch<T> : BytecodePatch() {
1313
abstract fun filterMap(
1414
classDef: ClassDef,
1515
method: Method,

src/main/kotlin/app/revanced/patches/all/screencapture/removerestriction/RemoveCaptureRestrictionPatch.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package app.revanced.patches.all.screencapture.removerestriction
22

33
import app.revanced.patcher.patch.annotation.Patch
44
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
5-
import app.revanced.patches.all.misc.transformation.AbstractTransformInstructionsPatch
5+
import app.revanced.patches.all.misc.transformation.BaseTransformInstructionsPatch
66
import app.revanced.patches.all.misc.transformation.IMethodCall
77
import app.revanced.patches.all.misc.transformation.Instruction35cInfo
88
import app.revanced.patches.all.misc.transformation.filterMapInstruction35c
@@ -18,7 +18,7 @@ import com.android.tools.smali.dexlib2.iface.instruction.Instruction
1818
requiresIntegrations = true
1919
)
2020
@Suppress("unused")
21-
object RemoveCaptureRestrictionPatch : AbstractTransformInstructionsPatch<Instruction35cInfo>() {
21+
object RemoveCaptureRestrictionPatch : BaseTransformInstructionsPatch<Instruction35cInfo>() {
2222
private const val INTEGRATIONS_CLASS_DESCRIPTOR_PREFIX =
2323
"Lapp/revanced/integrations/all/screencapture/removerestriction/RemoveScreencaptureRestrictionPatch"
2424
private const val INTEGRATIONS_CLASS_DESCRIPTOR = "$INTEGRATIONS_CLASS_DESCRIPTOR_PREFIX;"

0 commit comments

Comments
 (0)