initial commit
This commit is contained in:
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
build
|
||||||
|
.idea
|
||||||
|
gradlew
|
||||||
|
gradlew.bat
|
||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
gradle
|
||||||
37
build.gradle
Normal file
37
build.gradle
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
group 'net.headlezz.resdiff'
|
||||||
|
version '1.0'
|
||||||
|
|
||||||
|
apply plugin: 'kotlin'
|
||||||
|
apply plugin: 'application'
|
||||||
|
|
||||||
|
sourceCompatibility = 1.6
|
||||||
|
mainClassName = "net.headlezz.resdiff.ResdiffPackage"
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:0.13.1514'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile fileTree(include: ['*.jar'], dir: 'libs')
|
||||||
|
compile 'org.jetbrains.kotlin:kotlin-stdlib:0.13.1514'
|
||||||
|
compile 'org.jetbrains.kotlin:kotlin-reflect:0.13.1514'
|
||||||
|
compile 'commons-cli:commons-cli:1.3.1'
|
||||||
|
testCompile group: 'junit', name: 'junit', version: '4.11'
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
test.kotlin.srcDirs += 'src/test/kotlin'
|
||||||
|
}
|
||||||
|
|
||||||
|
task wrapper(type: Wrapper) {
|
||||||
|
gradleVersion = '2.7'
|
||||||
|
}
|
||||||
BIN
libs/btc-ascii-table-1.0.jar
Normal file
BIN
libs/btc-ascii-table-1.0.jar
Normal file
Binary file not shown.
2
settings.gradle
Normal file
2
settings.gradle
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
rootProject.name = 'resdiff'
|
||||||
|
|
||||||
30
src/main/kotlin/net/headlezz/resdiff/CLOptions.kt
Normal file
30
src/main/kotlin/net/headlezz/resdiff/CLOptions.kt
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package net.headlezz.resdiff
|
||||||
|
|
||||||
|
import org.apache.commons.cli.CommandLine
|
||||||
|
import org.apache.commons.cli.DefaultParser
|
||||||
|
import org.apache.commons.cli.HelpFormatter
|
||||||
|
import org.apache.commons.cli.Options
|
||||||
|
|
||||||
|
public class CLOptions(args: Array<String>) {
|
||||||
|
|
||||||
|
private val options = Options()
|
||||||
|
private val clOptions: CommandLine
|
||||||
|
|
||||||
|
init {
|
||||||
|
options.addOption("h", "help", false, "Show this screen")
|
||||||
|
options.addOption("t", "type", true, "only compare this resource type (string, bool, integer, color, dimen)")
|
||||||
|
options.addOption("y", "side-by-side", false, "output in columns")
|
||||||
|
|
||||||
|
val optionParser = DefaultParser()
|
||||||
|
clOptions = optionParser.parse(options, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun printHelp() {
|
||||||
|
HelpFormatter().printHelp("resdiff [OPTIONS]... FILES", options)
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun hasFlag(flag: String) : Boolean = clOptions.hasOption(flag)
|
||||||
|
|
||||||
|
public fun getOptionValue(flag: String) : String = clOptions.getOptionValue(flag)
|
||||||
|
|
||||||
|
}
|
||||||
168
src/main/kotlin/net/headlezz/resdiff/ResDiff.kt
Normal file
168
src/main/kotlin/net/headlezz/resdiff/ResDiff.kt
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
package net.headlezz.resdiff
|
||||||
|
|
||||||
|
import com.bethecoder.ascii_table.ASCIITable
|
||||||
|
import org.xml.sax.SAXParseException
|
||||||
|
import java.io.File
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.dom.elements
|
||||||
|
import kotlin.dom.get
|
||||||
|
import kotlin.dom.parseXml
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
public fun main(args: Array<String>) {
|
||||||
|
// parse arguments
|
||||||
|
val clo = CLOptions(args)
|
||||||
|
if (clo.hasFlag("h")) {
|
||||||
|
clo.printHelp()
|
||||||
|
System.exit(0)
|
||||||
|
}
|
||||||
|
val printTable = clo.hasFlag("y")
|
||||||
|
val compareBooleanRes = !clo.hasFlag("t") || clo.getOptionValue("t").contains("bool")
|
||||||
|
val compareStringRes = !clo.hasFlag("t") || clo.getOptionValue("t").contains("string")
|
||||||
|
val compareColorRes = !clo.hasFlag("t") || clo.getOptionValue("t").contains("color")
|
||||||
|
val compareIntegerRes = !clo.hasFlag("t") || clo.getOptionValue("t").contains("integer")
|
||||||
|
val compareDimensionRes = !clo.hasFlag("t") || clo.getOptionValue("t").contains("dimen")
|
||||||
|
|
||||||
|
|
||||||
|
// assert we have two valid file paths
|
||||||
|
if (args.size() < 2 ||
|
||||||
|
!File(args.get(args.size() - 2)).exists() ||
|
||||||
|
!File(args.get(args.size() - 1)).exists()) {
|
||||||
|
println("Missing file arguments.")
|
||||||
|
clo.printHelp()
|
||||||
|
System.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all resources
|
||||||
|
val resourceList1 = ArrayList<Resource>()
|
||||||
|
getFiles(args.get(args.size() - 2)).map { file -> getResourcesFromFile(file) }.forEach { resources -> resourceList1.addAll(resources) }
|
||||||
|
|
||||||
|
val resourceList2 = ArrayList<Resource>()
|
||||||
|
getFiles(args.get(args.size() - 1)).map { file -> getResourcesFromFile(file) }.forEach { resources -> resourceList2.addAll(resources) }
|
||||||
|
|
||||||
|
// and compare them!
|
||||||
|
val differences = ArrayList<Pair<Resource?, Resource?>>()
|
||||||
|
if (compareStringRes)
|
||||||
|
differences.addAll(getDifferencesForClass(StringResource::class, resourceList1, resourceList2))
|
||||||
|
if (compareBooleanRes)
|
||||||
|
differences.addAll(getDifferencesForClass(BooleanResource::class, resourceList1, resourceList2))
|
||||||
|
if (compareIntegerRes)
|
||||||
|
differences.addAll(getDifferencesForClass(IntegerResource::class, resourceList1, resourceList2))
|
||||||
|
if (compareDimensionRes)
|
||||||
|
differences.addAll(getDifferencesForClass(DimensionResource::class, resourceList1, resourceList2))
|
||||||
|
if (compareColorRes)
|
||||||
|
differences.addAll(getDifferencesForClass(ColorResource::class, resourceList1, resourceList2))
|
||||||
|
|
||||||
|
val addCount = differences.count { pair -> pair.first == null && pair.second != null }
|
||||||
|
val delCount = differences.count { pair -> pair.first != null && pair.second == null }
|
||||||
|
println("Found ${differences.size()} differences ($addCount added, $delCount removed).")
|
||||||
|
|
||||||
|
if (printTable)
|
||||||
|
printDiffInTable(differences)
|
||||||
|
else
|
||||||
|
printDiffNormal(differences)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun printDiffNormal(differences: List<Pair<Resource?, Resource?>>) {
|
||||||
|
differences.forEach { pair ->
|
||||||
|
val resA = pair.first
|
||||||
|
val resB = pair.second
|
||||||
|
val type = (resA?.type ?: resB!!.type).toString().toLowerCase()
|
||||||
|
val name = resA?.name ?: resB!!.name
|
||||||
|
println("<$type name=\"$name\">")
|
||||||
|
if (resA != null)
|
||||||
|
println("<\t${resA.value}")
|
||||||
|
if (resA != null && resB != null)
|
||||||
|
println("---")
|
||||||
|
if (resB != null)
|
||||||
|
println(">\t${resB.value}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun printDiffInTable(differences: List<Pair<Resource?, Resource?>>) {
|
||||||
|
val header = arrayOf("Type", "Name", "Left", "Right")
|
||||||
|
val data = differences.map { pair ->
|
||||||
|
val type = (pair.first?.type ?: pair.second!!.type).toString()
|
||||||
|
val name = pair.first?.name ?: pair.second!!.name
|
||||||
|
arrayOf(type, name, pair.first?.value ?: "", pair.second?.value ?: "")
|
||||||
|
}.toTypedArray()
|
||||||
|
ASCIITable.getInstance().printTable(header, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDifferencesForClass(clazz: KClass<out Resource>, resourceList1: ArrayList<Resource>, resourceList2: ArrayList<Resource>): List<Pair<Resource?, Resource?>> {
|
||||||
|
val className = clazz.simpleName
|
||||||
|
return getDifferences(
|
||||||
|
resourceList1.filter { r -> r.javaClass.simpleName == className },
|
||||||
|
resourceList2.filter { r -> r.javaClass.simpleName == className })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDifferences(from: List<Resource>, to: List<Resource>): ArrayList<Pair<Resource?, Resource?>> {
|
||||||
|
val diffs = ArrayList<Pair<Resource?, Resource?>>()
|
||||||
|
|
||||||
|
val pairs = genResourcePairs(from, to)
|
||||||
|
|
||||||
|
pairs.forEach { pair ->
|
||||||
|
if (!Resource.match(pair.first, pair.second))
|
||||||
|
diffs.add(pair)
|
||||||
|
}
|
||||||
|
|
||||||
|
return diffs
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* creates Pairs of resource with matching identity
|
||||||
|
*/
|
||||||
|
fun genResourcePairs(from: List<Resource>, to: List<Resource>): List<Pair<Resource?, Resource?>> {
|
||||||
|
val pairs = ArrayList<Pair<Resource?, Resource?>>()
|
||||||
|
|
||||||
|
val tempFrom = ArrayList<Resource>(from.size())
|
||||||
|
tempFrom.addAll(from)
|
||||||
|
val tempTo = ArrayList<Resource>(to.size())
|
||||||
|
tempTo.addAll(to)
|
||||||
|
|
||||||
|
loop@for (f in tempFrom) {
|
||||||
|
for (t in tempTo) {
|
||||||
|
if (f.matchIdent(t)) {
|
||||||
|
pairs.add(Pair(f, t))
|
||||||
|
continue@loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pairs.add(Pair(f, null))
|
||||||
|
}
|
||||||
|
pairs.forEach { pair ->
|
||||||
|
tempFrom.remove(pair.first)
|
||||||
|
tempTo.remove(pair.second)
|
||||||
|
}
|
||||||
|
tempTo.forEach { t -> pairs.add(Pair(null, t)) }
|
||||||
|
|
||||||
|
return pairs
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If location points to a directory, returns all files in this directory (not recursive), else,
|
||||||
|
* it just returns the file
|
||||||
|
*/
|
||||||
|
fun getFiles(location: String): Array<File> {
|
||||||
|
val file = File(location)
|
||||||
|
if (file.isDirectory) {
|
||||||
|
return file.listFiles { file, filter -> true }.filterNot { file -> file.isDirectory }.toTypedArray()
|
||||||
|
} else
|
||||||
|
return arrayOf(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns a list of all resources found in the given file
|
||||||
|
*/
|
||||||
|
fun getResourcesFromFile(file: File): ArrayList<Resource> {
|
||||||
|
try {
|
||||||
|
val doc = parseXml(file.absoluteFile)
|
||||||
|
val resources = doc.get("resources").first()
|
||||||
|
return resources.elements.map { elem -> Resource.fromElement(elem) }.filterNotNull().toArrayList()
|
||||||
|
} catch(e: SAXParseException) {
|
||||||
|
println("${file.name} is not a valid xml file")
|
||||||
|
return arrayListOf()
|
||||||
|
} catch(e: NoSuchElementException) {
|
||||||
|
println("${file.name} is not a valid xml file")
|
||||||
|
return arrayListOf()
|
||||||
|
}
|
||||||
|
}
|
||||||
61
src/main/kotlin/net/headlezz/resdiff/Resource.kt
Normal file
61
src/main/kotlin/net/headlezz/resdiff/Resource.kt
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package net.headlezz.resdiff
|
||||||
|
|
||||||
|
import org.w3c.dom.Element
|
||||||
|
|
||||||
|
abstract class Resource(
|
||||||
|
public val name: String,
|
||||||
|
public val value: String,
|
||||||
|
public val type: Resource.Type) {
|
||||||
|
|
||||||
|
enum class Type {
|
||||||
|
String, Dimension, Boolean, Integer, Color
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
public fun fromElement(elem: Element) : Resource? {
|
||||||
|
val type= elem.tagName
|
||||||
|
val name = elem.getAttribute("name")
|
||||||
|
val value = elem.firstChild.nodeValue
|
||||||
|
|
||||||
|
return when(type) {
|
||||||
|
"string" -> StringResource(name, value)
|
||||||
|
"dimen" -> DimensionResource(name, value)
|
||||||
|
"bool" -> BooleanResource(name, value)
|
||||||
|
"integer" -> IntegerResource(name, value)
|
||||||
|
"color" -> ColorResource(name, value)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun match(resource1: Resource?, resource2: Resource?) : Boolean {
|
||||||
|
if(resource1 == null)
|
||||||
|
return false
|
||||||
|
|
||||||
|
if(resource2 == null)
|
||||||
|
return false
|
||||||
|
|
||||||
|
return resource1.match(resource2)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given resource matches the this one
|
||||||
|
*/
|
||||||
|
public fun match(resource2: Resource) : Boolean {
|
||||||
|
return matchIdent(resource2) && value == resource2.value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if identity of two resources match (resource name matching)
|
||||||
|
*/
|
||||||
|
public fun matchIdent(resource2: Resource) : Boolean {
|
||||||
|
return this.type == resource2.type && this.name == resource2.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StringResource(name: String, value: String) : Resource(name, value, Resource.Type.String)
|
||||||
|
class BooleanResource(name: String, value: String) : Resource(name, value, Resource.Type.Boolean)
|
||||||
|
class ColorResource(name: String, value: String) : Resource(name, value, Resource.Type.Color)
|
||||||
|
class IntegerResource(name: String, value: String) : Resource(name, value, Resource.Type.Integer)
|
||||||
|
class DimensionResource(name: String, value: String) : Resource(name, value, Resource.Type.Dimension)
|
||||||
110
src/test/kotlin/net/headlezz/resdiff/ResourceTest.kt
Normal file
110
src/test/kotlin/net/headlezz/resdiff/ResourceTest.kt
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package net.headlezz.resdiff
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import kotlin.dom.elements
|
||||||
|
import kotlin.dom.parseXml
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class ResourceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun match() {
|
||||||
|
val resA = StringResource("nameA", "valueA")
|
||||||
|
val resB = StringResource("nameA", "valueA")
|
||||||
|
assertTrue(resA.match(resB))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun match_notMatching() {
|
||||||
|
val resA = StringResource("nameA", "valueA")
|
||||||
|
val resB = StringResource("nameA", "valueB")
|
||||||
|
assertFalse(resA.match(resB))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun ident() {
|
||||||
|
val resA = StringResource("nameA", "valueA")
|
||||||
|
val resB = StringResource("nameA", "valueA")
|
||||||
|
assertTrue(resA.matchIdent(resB))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun ident_notMatching() {
|
||||||
|
val resA = StringResource("nameA", "valueA")
|
||||||
|
val resB = StringResource("nameB", "valueA")
|
||||||
|
assertFalse(resA.matchIdent(resB))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun ident_differentResourceTypes() {
|
||||||
|
val resA = object: Resource("nameA", "valueA", Resource.Type.Boolean){}
|
||||||
|
val resB = object: Resource("nameA", "valueA", Resource.Type.String){}
|
||||||
|
assertFalse(resA.matchIdent(resB))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fromElement_String() {
|
||||||
|
val res = "<string name=\"a name\">a value</string>"
|
||||||
|
val elem = parseXml(buildInputStream(res)).elements.first()
|
||||||
|
|
||||||
|
val stringResource = Resource.fromElement(elem)
|
||||||
|
assert(stringResource is StringResource)
|
||||||
|
assertEquals("a name", stringResource!!.name)
|
||||||
|
assertEquals("a value", stringResource.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fromElement_Boolean() {
|
||||||
|
val res = "<bool name=\"a name\">true</bool>"
|
||||||
|
val elem = parseXml(buildInputStream(res)).elements.first()
|
||||||
|
|
||||||
|
val booleanResource = Resource.fromElement(elem)
|
||||||
|
assert(booleanResource is BooleanResource)
|
||||||
|
assertEquals("a name", booleanResource!!.name)
|
||||||
|
assertEquals("true", booleanResource.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fromElement_Integer() {
|
||||||
|
val res = "<integer name=\"a name\">-123</integer>"
|
||||||
|
val elem = parseXml(buildInputStream(res)).elements.first()
|
||||||
|
|
||||||
|
val intResource = Resource.fromElement(elem)
|
||||||
|
assert(intResource is IntegerResource)
|
||||||
|
assertEquals("a name", intResource!!.name)
|
||||||
|
assertEquals("-123", intResource.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fromElement_Color() {
|
||||||
|
val res = "<color name=\"a name\">#000001</color>"
|
||||||
|
val elem = parseXml(buildInputStream(res)).elements.first()
|
||||||
|
|
||||||
|
val colorResource = Resource.fromElement(elem)
|
||||||
|
assert(colorResource is ColorResource)
|
||||||
|
assertEquals("a name", colorResource!!.name)
|
||||||
|
assertEquals("#000001", colorResource.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fromElement_Dimension() {
|
||||||
|
val res = "<dimen name=\"a name\">12dp</dimen>"
|
||||||
|
val elem = parseXml(buildInputStream(res)).elements.first()
|
||||||
|
|
||||||
|
val dimensionResource = Resource.fromElement(elem)
|
||||||
|
assert(dimensionResource is DimensionResource)
|
||||||
|
assertEquals("a name", dimensionResource!!.name)
|
||||||
|
assertEquals("12dp", dimensionResource.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun buildInputStream(value: String) : InputStream {
|
||||||
|
return ByteArrayInputStream(value.getBytes(StandardCharsets.UTF_8))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
24
src/test/res/testres.xml
Normal file
24
src/test/res/testres.xml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:tools="http://schemas.android.com/tools" tools:locale="en">
|
||||||
|
<string name="matching_res">This resource is matching</string>
|
||||||
|
<string name="not_matching_res">This resource is not matching</string>
|
||||||
|
|
||||||
|
<bool name="matching_bool">true</bool>
|
||||||
|
<bool name="not_matching_bool">false</bool>
|
||||||
|
|
||||||
|
<integer name="matching_int">123</integer>
|
||||||
|
<integer name="not_matching_int">122</integer>
|
||||||
|
|
||||||
|
<color name="matching_color">#000000</color>
|
||||||
|
<color name="not_matching_color">#001230</color>
|
||||||
|
|
||||||
|
<dimen name="matching_dimen">12dp</dimen>
|
||||||
|
<dimen name="not_matching_dimen">1sp</dimen>
|
||||||
|
|
||||||
|
<string name="missing_string_res">This misses in the other file</string>
|
||||||
|
|
||||||
|
<string name="a_reference">@string/is_just_a_string</string>
|
||||||
|
<string name="a_reference_not_matching">@string/is_just_a_string</string>
|
||||||
|
|
||||||
|
<bool name="a_missing_boolean">true</bool>
|
||||||
|
</resources>
|
||||||
23
src/test/res/testres2.xml
Normal file
23
src/test/res/testres2.xml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:tools="http://schemas.android.com/tools" tools:locale="en">
|
||||||
|
<string name="matching_res">This resource is matching</string>
|
||||||
|
<string name="not_matching_res">This resource is not matching!!!!</string>
|
||||||
|
|
||||||
|
<bool name="matching_bool">true</bool>
|
||||||
|
<bool name="not_matching_bool">true</bool>
|
||||||
|
|
||||||
|
<integer name="matching_int">123</integer>
|
||||||
|
<integer name="not_matching_int">533</integer>
|
||||||
|
|
||||||
|
<color name="matching_color">#000000</color>
|
||||||
|
<color name="not_matching_color">#666666</color>
|
||||||
|
|
||||||
|
<dimen name="matching_dimen">12dp</dimen>
|
||||||
|
<dimen name="not_matching_dimen">99px</dimen>
|
||||||
|
|
||||||
|
<string name="missing_string_res_no2">This misses in the other file</string>
|
||||||
|
|
||||||
|
<string name="a_reference">@string/is_just_a_string</string>
|
||||||
|
<string name="a_reference_not_matching">@string/is_just_another_string</string>
|
||||||
|
|
||||||
|
</resources>
|
||||||
Reference in New Issue
Block a user