Added the aaptcompiler and layoutlib-api implementation

This commit is contained in:
Akash Yadav
2022-08-19 15:22:34 +05:30
parent 57df89efc5
commit eb949f0e29
76 changed files with 19819 additions and 0 deletions
+3
View File
@@ -25,11 +25,14 @@
<module name="AndroidIDE.build-tools.eventbus-models.test" target="11" />
<module name="AndroidIDE.build-tools.eventbus.main" target="11" />
<module name="AndroidIDE.build-tools.eventbus.test" target="11" />
<module name="AndroidIDE.build-tools.layoutib-api.main" target="11" />
<module name="AndroidIDE.build-tools.layoutib-api.test" target="11" />
<module name="AndroidIDE.build-tools.logger" target="11" />
<module name="AndroidIDE.build-tools.logger.main" target="11" />
<module name="AndroidIDE.build-tools.logger.test" target="11" />
<module name="AndroidIDE.build-tools.project" target="11" />
<module name="AndroidIDE.build-tools.tooling-api-plugin" target="11" />
<module name="AndroidIDE.build-tools.tools-common" target="11" />
<module name="AndroidIDE.google-java-format" target="1.8" />
<module name="AndroidIDE.gradle-tooling" target="1.8" />
<module name="AndroidIDE.gradle-tooling.plugin" target="1.8" />
+3
View File
@@ -72,6 +72,8 @@ android {
'META-INF/CHANGES',
'META-INF/README.md',
'about_files/LICENSE-2.0.txt',
'META-INF/AL2.0',
'META-INF/LGPL2.1',
'plugin.xml',
'plugin.properties',
'about.mappings',
@@ -130,6 +132,7 @@ dependencies {
implementation common.kotlin
// Local projects here
implementation project (':build-tools:aaptcompiler')
implementation project (':eventbus-android')
implementation project (':eventbus-events')
implementation project (':build-tools:projects')
+1
View File
@@ -0,0 +1 @@
android.jetifier.ignorelist=common-30.2.2.jar
Binary file not shown.
@@ -35,14 +35,20 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.text.HtmlCompat;
import com.android.aaptcompiler.BlameLogger;
import com.android.aaptcompiler.ResourceCompiler;
import com.android.aaptcompiler.ResourceCompilerOptions;
import com.android.utils.StdLogger;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.itsaky.androidide.app.StudioActivity;
import com.itsaky.androidide.databinding.ActivityMainBinding;
import com.itsaky.androidide.fragments.MainFragment;
import com.itsaky.androidide.models.Constants;
import com.itsaky.androidide.projects.ProjectManager;
import com.itsaky.androidide.tasks.TaskExecutor;
import com.itsaky.androidide.utils.DialogUtils;
import com.itsaky.androidide.utils.Environment;
import com.itsaky.androidide.utils.StopWatch;
import com.itsaky.toaster.Toaster;
import java.io.File;
@@ -63,6 +69,27 @@ public class MainActivity extends StudioActivity {
.beginTransaction()
.replace(R.id.container, new MainFragment(), MainFragment.TAG)
.commit();
TaskExecutor.executeAsyncProvideError(
() -> {
final var input =
new File(Environment.PROJECTS_DIR, "TestApp/app/src/main/res/layout/activity_main.xml");
final var output = Environment.PROJECTS_DIR;
final var watch = new StopWatch("Compile sample resource");
ResourceCompiler.compileResource(
input,
output,
new ResourceCompilerOptions(),
new BlameLogger(new StdLogger(StdLogger.Level.VERBOSE), str -> str));
watch.log();
return true;
},
(result, error) -> {
if (error != null) {
LOG.error("XML compilation error", error);
}
});
}
private void openLastProject() {
+1
View File
@@ -0,0 +1 @@
/build
+51
View File
@@ -0,0 +1,51 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
}
java {
sourceCompatibility = rootProject.ext.javaSourceVersion
targetCompatibility = rootProject.ext.javaTargetVersion
}
compileKotlin {
kotlinOptions {
jvmTarget = "11"
}
}
compileTestKotlin {
kotlinOptions {
jvmTarget = "11"
}
}
dependencies {
implementation common.jkotlin
implementation project (':build-tools:layoutlib-api')
api 'com.android.tools:common:30.2.2'
implementation 'com.android.tools:annotations:30.2.2'
implementation 'com.android.tools.build:aapt2-proto:7.2.2-7984345'
implementation 'com.google.protobuf:protobuf-java:3.10.0'
testImplementation tests.junit
testImplementation tests.google_truth
}
@@ -0,0 +1,2 @@
#androidx.enableJetifier=false
android.jetifier.ignorelist=common-30.2.2.jar
@@ -0,0 +1,94 @@
package com.android.aaptcompiler
import com.android.aaptcompiler.android.ResTableConfig
private const val ANIM_TAG = "anim"
private const val ANIMATOR_TAG = "animator"
private const val ARRAY_TAG = "array"
private const val ATTR_TAG = "attr"
private const val ATTR_PRIVATE_TAG = "^attr-private"
private const val BOOL_TAG = "bool"
private const val COLOR_TAG = "color"
private const val CONFIG_VARYING_TAG = "configVarying"
private const val DIMEN_TAG = "dimen"
private const val DRAWABLE_TAG = "drawable"
private const val FONT_TAG = "font"
private const val FRACTION_TAG = "fraction"
private const val ID_TAG = "id"
private const val INTEGER_TAG = "integer"
private const val INTERPOLATOR_TAG = "interpolator"
private const val LAYOUT_TAG = "layout"
private const val MACRO_TAG = "macro"
private const val MENU_TAG = "menu"
private const val MIPMAP_TAG = "mipmap"
private const val NAVIGATION_TAG = "navigation"
private const val PLURALS_TAG = "plurals"
private const val RAW_TAG = "raw"
private const val STRING_TAG = "string"
private const val STYLE_TAG = "style"
private const val STYLEABLE_TAG = "styleable"
private const val TRANSITION_TAG = "transition"
private const val XML_TAG = "xml"
fun resourceTypeFromTag(tag: String) =
when(tag) {
ANIM_TAG -> AaptResourceType.ANIM
ANIMATOR_TAG -> AaptResourceType.ANIMATOR
ARRAY_TAG -> AaptResourceType.ARRAY
ATTR_TAG -> AaptResourceType.ATTR
ATTR_PRIVATE_TAG -> AaptResourceType.ATTR_PRIVATE
BOOL_TAG -> AaptResourceType.BOOL
COLOR_TAG -> AaptResourceType.COLOR
CONFIG_VARYING_TAG -> AaptResourceType.CONFIG_VARYING
DIMEN_TAG -> AaptResourceType.DIMEN
DRAWABLE_TAG -> AaptResourceType.DRAWABLE
FONT_TAG -> AaptResourceType.FONT
FRACTION_TAG -> AaptResourceType.FRACTION
ID_TAG -> AaptResourceType.ID
INTEGER_TAG -> AaptResourceType.INTEGER
INTERPOLATOR_TAG -> AaptResourceType.INTERPOLATOR
LAYOUT_TAG -> AaptResourceType.LAYOUT
MACRO_TAG -> AaptResourceType.MACRO
MENU_TAG -> AaptResourceType.MENU
MIPMAP_TAG -> AaptResourceType.MIPMAP
NAVIGATION_TAG -> AaptResourceType.NAVIGATION
PLURALS_TAG -> AaptResourceType.PLURALS
RAW_TAG -> AaptResourceType.RAW
STRING_TAG -> AaptResourceType.STRING
STYLE_TAG -> AaptResourceType.STYLE
STYLEABLE_TAG -> AaptResourceType.STYLEABLE
TRANSITION_TAG -> AaptResourceType.TRANSITION
XML_TAG -> AaptResourceType.XML
else -> null
}
enum class AaptResourceType(val tagName: String) {
ANIM(ANIM_TAG),
ANIMATOR(ANIMATOR_TAG),
ARRAY(ARRAY_TAG),
ATTR(ATTR_TAG),
ATTR_PRIVATE(ATTR_PRIVATE_TAG),
BOOL(BOOL_TAG),
COLOR(COLOR_TAG),
CONFIG_VARYING(CONFIG_VARYING_TAG),
DIMEN(DIMEN_TAG),
DRAWABLE(DRAWABLE_TAG),
FONT(FONT_TAG),
FRACTION(FRACTION_TAG),
ID(ID_TAG),
INTEGER(INTEGER_TAG),
INTERPOLATOR(INTERPOLATOR_TAG),
LAYOUT(LAYOUT_TAG),
MACRO(MACRO_TAG),
MENU(MENU_TAG),
MIPMAP(MIPMAP_TAG),
NAVIGATION(NAVIGATION_TAG),
PLURALS(PLURALS_TAG),
RAW(RAW_TAG),
STRING(STRING_TAG),
STYLE(STYLE_TAG),
STYLEABLE(STYLEABLE_TAG),
TRANSITION(TRANSITION_TAG),
UNKNOWN(""),
XML(XML_TAG)
}
@@ -0,0 +1,114 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.aaptcompiler
import com.android.ide.common.blame.SourceFilePosition
import com.android.ide.common.blame.SourcePosition
import com.android.utils.ILogger
import java.io.File
import javax.xml.stream.Location
internal fun blameSource(
source: Source,
line: Int? = source.line,
column: Int? = null
): BlameLogger.Source = BlameLogger.Source(source.path, line ?: -1, column ?: -1)
internal fun blameSource(source: Source, location: Location): BlameLogger.Source =
BlameLogger.Source(source.path, location.lineNumber, location.columnNumber)
class BlameLogger
constructor(
val logger: ILogger,
private val userVisibleSourceTransform: (String) -> String,
val blameMap: (Source) -> Source = { it }
) {
@JvmOverloads
constructor(
logger: ILogger,
blameMap: (Source) -> Source = { it }
) : this(logger, { it }, blameMap)
data class Source(val sourcePath: String, val line: Int = -1, val column: Int = -1) {
override fun toString(): String {
var result = sourcePath
if (line != -1) {
result += ":$line"
if (column != -1) {
result += ":$column"
}
}
return "$result: "
}
fun toSourceFilePosition() =
SourceFilePosition(File(sourcePath), SourcePosition(line, column, -1, line, column, -1))
companion object {
fun fromSourceFilePosition(filePosition: SourceFilePosition) =
Source(
filePosition.file.sourcePath!!,
filePosition.position.startLine,
filePosition.position.startColumn
)
}
}
fun error(message: String, source: Source? = null, throwable: Throwable? = null) {
if (source != null) {
logger.error(throwable, "${getOutputSource(source)}$message")
} else {
logger.error(throwable, message)
}
}
fun warning(message: String, source: Source? = null) {
if (source != null) logger.warning("${getOutputSource(source)}$message")
else logger.warning(message)
}
fun info(message: String, source: Source? = null) {
if (source != null) logger.info("${getOutputSource(source)}$message") else logger.info(message)
}
fun lifecycle(message: String, source: Source? = null) {
if (source != null) logger.lifecycle("${getOutputSource(source)}$message")
else logger.lifecycle(message)
}
fun quiet(message: String, source: Source? = null) {
if (source != null) logger.quiet("${getOutputSource(source)}$message")
else logger.quiet(message)
}
fun verbose(message: String, source: Source? = null) {
if (source != null) logger.verbose("${getOutputSource(source)}$message")
else logger.verbose(message)
}
internal fun getOutputSource(source: Source): Source {
return getOriginalSource(
source.copy(sourcePath = userVisibleSourceTransform(source.sourcePath))
)
}
fun getOriginalSource(source: Source): Source {
return blameMap(source)
}
}
@@ -0,0 +1,232 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.aaptcompiler
import com.android.aaptcompiler.android.ResTableConfig
class ConfigDescription(base: ResTableConfig = ResTableConfig()): ResTableConfig(base) {
override fun equals(other: Any?): Boolean {
if (other is ConfigDescription) {
return compareTo(other) == 0
}
return false
}
private fun compatible(first: Int, second: Int) = first == 0 || second == 0 || first == second
}
const val WILDCARD_NAME: String = "any"
fun parse(config: String): ConfigDescription {
val parts = config.split('-').map { it.toLowerCase()}
var index = 0
val configDescription = ConfigDescription()
val locale = LocaleValue()
if (parts.isEmpty() || config.isEmpty()) {
return applyVersionForCompatibility(configDescription)
}
if (parseMcc(parts[index], configDescription)) {
index++
if (index == parts.size) {
return applyVersionForCompatibility(configDescription)
}
}
if (parseMnc(parts[index], configDescription)) {
index++
if (index == parts.size) {
return applyVersionForCompatibility(configDescription)
}
}
// Locale spans a few '-' separators, so we let it control the index
index += locale.initFromParts(parts, index)
locale.writeTo(configDescription)
if (index == parts.size) {
return configDescription
}
if(parseLayoutDirection(parts[index], configDescription)) {
index++
if (index == parts.size) {
return applyVersionForCompatibility(configDescription)
}
}
if (parseSmallestScreenWidthDp(parts[index], configDescription)) {
index++
if (index == parts.size) {
return applyVersionForCompatibility(configDescription)
}
}
if (parseScreenWidthDp(parts[index], configDescription)) {
index++
if (index == parts.size) {
return applyVersionForCompatibility(configDescription)
}
}
if (parseScreenHeightDp(parts[index], configDescription)) {
index++
if (index == parts.size) {
return applyVersionForCompatibility(configDescription)
}
}
if (parseScreenLayoutSize(parts[index], configDescription)) {
index++
if (index == parts.size) {
return applyVersionForCompatibility(configDescription)
}
}
if (parseScreenLayoutLong(parts[index], configDescription)) {
index++
if (index == parts.size) {
return applyVersionForCompatibility(configDescription)
}
}
if (parseScreenRound(parts[index], configDescription)) {
index++
if (index == parts.size) {
return applyVersionForCompatibility(configDescription)
}
}
if (parseWideColorGamut(parts[index], configDescription)) {
index++
if (index == parts.size) {
return applyVersionForCompatibility(configDescription)
}
}
if (parseHdr(parts[index], configDescription)) {
index++
if (index == parts.size) {
return applyVersionForCompatibility(configDescription)
}
}
if (parseOrientation(parts[index], configDescription)) {
index++
if (index == parts.size) {
return applyVersionForCompatibility(configDescription)
}
}
if (parseUiModeType(parts[index], configDescription)) {
index++
if (index == parts.size) {
return applyVersionForCompatibility(configDescription)
}
}
if (parseUiModeNight(parts[index], configDescription)) {
index++
if (index == parts.size) {
return applyVersionForCompatibility(configDescription)
}
}
if (parseDensity(parts[index], configDescription)) {
index++
if (index == parts.size) {
return applyVersionForCompatibility(configDescription)
}
}
if (parseTouchscreen(parts[index], configDescription)) {
index++
if (index == parts.size) {
return applyVersionForCompatibility(configDescription)
}
}
if (parseKeysHidden(parts[index], configDescription)) {
index++
if (index == parts.size) {
return applyVersionForCompatibility(configDescription)
}
}
if (parseKeyboard(parts[index], configDescription)) {
index++
if (index == parts.size) {
return applyVersionForCompatibility(configDescription)
}
}
if (parseNavHidden(parts[index], configDescription)) {
index++
if (index == parts.size) {
return applyVersionForCompatibility(configDescription)
}
}
if (parseNavigation(parts[index], configDescription)) {
index++
if (index == parts.size) {
return applyVersionForCompatibility(configDescription)
}
}
if (parseScreenSize(parts[index], configDescription)) {
index++
if (index == parts.size) {
return applyVersionForCompatibility(configDescription)
}
}
if (parseVersion(parts[index], configDescription)) {
index++
if (index == parts.size) {
return applyVersionForCompatibility(configDescription)
}
}
throw error("Unrecognized part '${parts[index]}' in configuration $config")
}
private fun applyVersionForCompatibility(config: ConfigDescription): ConfigDescription {
val minSdk = when {
config.uiModeType() == ResTableConfig.UI_MODE.TYPE_VR_HEADSET ||
config.wideColorGamut() != ResTableConfig.COLOR_MODE.WIDE_GAMUT_ANY ||
config.hdr() != ResTableConfig.COLOR_MODE.HDR_ANY -> SDKConstants.SDK_O
config.layoutRound() != 0.toByte() -> SDKConstants.SDK_MARSHMALLOW
config.density == ResTableConfig.DENSITY.ANY -> SDKConstants.SDK_LOLLIPOP
config.smallestScreenWidthDp != 0 ||
config.screenWidthDp != 0 ||
config.screenHeightDp != 0-> SDKConstants.SDK_HONEYCOMB_MR2
config.uiModeType() != ResTableConfig.UI_MODE.TYPE_ANY ||
config.uiModeNight() != ResTableConfig.UI_MODE.NIGHT_ANY -> SDKConstants.SDK_FROYO
config.layoutSize() != ResTableConfig.SCREEN_LAYOUT.SIZE_ANY ||
config.layoutLong() != ResTableConfig.SCREEN_LAYOUT.SCREENLONG_ANY ||
config.density != ResTableConfig.DENSITY.DEFAULT -> SDKConstants.SDK_DONUT
else -> 0
}
if (minSdk > config.sdkVersion) {
config.sdkVersion = minSdk
}
return config
}
@@ -0,0 +1,335 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.aaptcompiler
import com.android.aaptcompiler.android.AConfiguration
import com.android.aaptcompiler.android.ResTableConfig
import com.android.aaptcompiler.android.isTruthy
/** attempt to parse the Mobile Country Code */
fun parseMcc(part: String, config: ConfigDescription): Boolean {
if (part == WILDCARD_NAME) {
config.mcc = 0
return true
}
// The only acceptable format is "mccDDD" (where D is a digit).
if (!part.startsWith("mcc") || part.length != 6) {
return false
}
val value = part.substring(3).toIntOrNull()?.toShort() ?: return false
config.mcc = value
return true
}
/** attempt to parse the Mobile Network Code */
fun parseMnc(part: String, config: ConfigDescription): Boolean {
if (part == WILDCARD_NAME) {
config.mnc = 0
return true
}
// The only acceptable format is "mncDDD" (where D is a digit).
if (!part.startsWith("mnc") || part.length != 6) {
return false
}
val value = part.substring(3).toIntOrNull()?.toShort() ?: return false
// Zero is reserved, so we need to use MINC_ZERO instead.
config.mnc = if (value.isTruthy()) value else AConfiguration.ACONFIGURATION_MNC_ZERO.toShort()
return true
}
// TODO(@daniellabar): Make more like the aapt2 parser.
fun parseLayoutDirection(part: String, config: ConfigDescription) : Boolean {
val value = when(part) {
WILDCARD_NAME -> ResTableConfig.SCREEN_LAYOUT.DIR_ANY
"ldltr" -> ResTableConfig.SCREEN_LAYOUT.DIR_LTR
"ldrtl" -> ResTableConfig.SCREEN_LAYOUT.DIR_RTL
else -> return false
}
// AND existing config with the mask and then OR with the found layout direction.
config.screenLayout =
maskAndApply(config.screenLayout, ResTableConfig.SCREEN_LAYOUT.DIR_MASK, value)
return true
}
fun parseSmallestScreenWidthDp(part: String, config: ConfigDescription): Boolean {
if (part == WILDCARD_NAME) {
config.smallestScreenWidthDp = 0
return true
}
if (!part.startsWith("sw") || !part.endsWith("dp")) {
return false
}
val value = part.substring(2, part.length-2).toIntOrNull() ?: return false
config.smallestScreenWidthDp = value
return true
}
fun parseScreenWidthDp(part: String, config: ConfigDescription): Boolean {
if (part == WILDCARD_NAME) {
config.screenWidthDp = 0
return true
}
if (!part.startsWith('w') || !part.endsWith("dp")) {
return false
}
val value = part.substring(1, part.length-2).toIntOrNull() ?: return false
config.screenWidthDp = value
return true
}
fun parseScreenHeightDp(part: String, config: ConfigDescription): Boolean {
if (part == WILDCARD_NAME) {
config.screenHeightDp = 0
return true
}
if (!part.startsWith('h') || !part.endsWith("dp")) {
return false
}
val value = part.substring(1, part.length-2).toIntOrNull() ?: return false
config.screenHeightDp = value
return true
}
fun parseScreenLayoutSize(part: String, config: ConfigDescription): Boolean {
val value = when (part) {
WILDCARD_NAME -> 0
"small" -> ResTableConfig.SCREEN_LAYOUT.SIZE_SMALL
"normal" -> ResTableConfig.SCREEN_LAYOUT.SIZE_NORMAL
"large" -> ResTableConfig.SCREEN_LAYOUT.SIZE_LARGE
"xlarge" -> ResTableConfig.SCREEN_LAYOUT.SIZE_XLARGE
else -> return false
}
// AND existing config with the mask and then OR with the found layout size.
config.screenLayout =
maskAndApply(config.screenLayout, ResTableConfig.SCREEN_LAYOUT.SIZE_MASK, value)
return true
}
fun parseScreenLayoutLong(part: String, config: ConfigDescription): Boolean {
val value = when (part) {
WILDCARD_NAME -> ResTableConfig.SCREEN_LAYOUT.SCREENLONG_ANY
"long" -> ResTableConfig.SCREEN_LAYOUT.SCREENLONG_YES
"notlong" -> ResTableConfig.SCREEN_LAYOUT.SCREENLONG_NO
else -> return false
}
// AND existing config with the mask and then OR with the found layout size.
config.screenLayout =
maskAndApply(config.screenLayout, ResTableConfig.SCREEN_LAYOUT.SCREENLONG_MASK, value)
return true
}
fun parseScreenRound(part: String, config: ConfigDescription): Boolean {
val value = when (part) {
WILDCARD_NAME -> ResTableConfig.SCREEN_LAYOUT2.SCREENROUND_ANY
"round" -> ResTableConfig.SCREEN_LAYOUT2.SCREENROUND_YES
"notround" -> ResTableConfig.SCREEN_LAYOUT2.SCREENROUND_NO
else -> return false
}
config.screenLayout2 =
maskAndApply(config.screenLayout2, ResTableConfig.SCREEN_LAYOUT2.SCREENROUND_MASK, value)
return true
}
fun parseWideColorGamut(part: String, config: ConfigDescription): Boolean {
val value = when (part) {
WILDCARD_NAME -> ResTableConfig.COLOR_MODE.WIDE_GAMUT_ANY
"widecg" -> ResTableConfig.COLOR_MODE.WIDE_GAMUT_YES
"nowidecg" -> ResTableConfig.COLOR_MODE.WIDE_GAMUT_NO
else -> return false
}
config.colorMode =
maskAndApply(config.colorMode, ResTableConfig.COLOR_MODE.WIDE_GAMUT_MASK, value)
return true
}
fun parseHdr(part: String, config: ConfigDescription): Boolean {
val value = when (part) {
WILDCARD_NAME -> ResTableConfig.COLOR_MODE.HDR_ANY
"highdr" -> ResTableConfig.COLOR_MODE.HDR_YES
"lowdr" -> ResTableConfig.COLOR_MODE.HDR_NO
else -> return false
}
config.colorMode = maskAndApply(config.colorMode, ResTableConfig.COLOR_MODE.HDR_MASK, value)
return true
}
fun parseOrientation(part: String, config: ConfigDescription): Boolean {
config.orientation = when (part) {
WILDCARD_NAME -> ResTableConfig.ORIENTATION.ANY
"port" -> ResTableConfig.ORIENTATION.PORT
"land" -> ResTableConfig.ORIENTATION.LAND
"square" -> ResTableConfig.ORIENTATION.SQUARE
else -> return false
}
return true
}
fun parseUiModeType(part: String, config: ConfigDescription): Boolean {
val value = when (part) {
WILDCARD_NAME -> ResTableConfig.UI_MODE.TYPE_ANY
"desk" -> ResTableConfig.UI_MODE.TYPE_DESK
"car" -> ResTableConfig.UI_MODE.TYPE_CAR
"television" -> ResTableConfig.UI_MODE.TYPE_TELEVISION
"appliance" -> ResTableConfig.UI_MODE.TYPE_APPLIANCE
"watch" -> ResTableConfig.UI_MODE.TYPE_WATCH
"vrheadset" -> ResTableConfig.UI_MODE.TYPE_VR_HEADSET
else -> return false
}
config.uiMode = maskAndApply(config.uiMode, ResTableConfig.UI_MODE.TYPE_MASK, value)
return true
}
fun parseUiModeNight(part: String, config: ConfigDescription): Boolean {
val value = when (part) {
WILDCARD_NAME -> ResTableConfig.UI_MODE.NIGHT_ANY
"night" -> ResTableConfig.UI_MODE.NIGHT_YES
"notnight" -> ResTableConfig.UI_MODE.NIGHT_NO
else -> return false
}
config.uiMode = maskAndApply(config.uiMode, ResTableConfig.UI_MODE.NIGHT_MASK, value)
return true
}
fun parseDensity(part: String, config: ConfigDescription): Boolean {
if (part.isNotEmpty() && part[0].isLetter()) {
config.density = when (part) {
WILDCARD_NAME -> ResTableConfig.DENSITY.DEFAULT
"anydpi" -> ResTableConfig.DENSITY.ANY
"nodpi" -> ResTableConfig.DENSITY.NONE
"ldpi" -> ResTableConfig.DENSITY.LOW
"mdpi" -> ResTableConfig.DENSITY.MEDIUM
"tvdpi" -> ResTableConfig.DENSITY.TV
"hdpi" -> ResTableConfig.DENSITY.HIGH
"xhdpi" -> ResTableConfig.DENSITY.XHIGH
"xxhdpi" -> ResTableConfig.DENSITY.XXHIGH
"xxxhdpi" -> ResTableConfig.DENSITY.XXXHIGH
else -> return false
}
return true
} else {
// we might be dealing with "<number>dpi"
if (!part.endsWith("dpi")) {
return false
}
val value = part.substring(0, part.length-3).toIntOrNull() ?: return false
config.density = value
return true
}
}
fun parseTouchscreen(part: String, config: ConfigDescription): Boolean {
config.touchscreen = when (part) {
WILDCARD_NAME -> ResTableConfig.TOUCHSCREEN.ANY
"notouch" -> ResTableConfig.TOUCHSCREEN.NOTOUCH
"stylus" -> ResTableConfig.TOUCHSCREEN.STYLUS
"finger" -> ResTableConfig.TOUCHSCREEN.FINGER
else -> return false
}
return true
}
fun parseKeysHidden(part: String, config: ConfigDescription): Boolean {
val value = when (part) {
WILDCARD_NAME -> ResTableConfig.INPUT_FLAGS.KEYSHIDDEN_ANY
"keysexposed" -> ResTableConfig.INPUT_FLAGS.KEYSHIDDEN_NO
"keyshidden" -> ResTableConfig.INPUT_FLAGS.KEYSHIDDEN_YES
"keyssoft" -> ResTableConfig.INPUT_FLAGS.KEYSHIDDEN_SOFT
else -> return false
}
config.inputFlags =
maskAndApply(config.inputFlags, ResTableConfig.INPUT_FLAGS.KEYSHIDDEN_MASK, value)
return true
}
fun parseKeyboard(part: String, config: ConfigDescription): Boolean {
config.keyboard = when (part) {
WILDCARD_NAME -> ResTableConfig.KEYBOARD.ANY
"nokeys" -> ResTableConfig.KEYBOARD.NOKEYS
"qwerty" -> ResTableConfig.KEYBOARD.QWERTY
"12key" -> ResTableConfig.KEYBOARD.TWELVEKEY
else -> return false
}
return true
}
fun parseNavHidden(part: String, config: ConfigDescription): Boolean {
val value = when (part) {
WILDCARD_NAME -> ResTableConfig.INPUT_FLAGS.NAVHIDDEN_ANY
"navexposed" -> ResTableConfig.INPUT_FLAGS.NAVHIDDEN_NO
"navhidden" -> ResTableConfig.INPUT_FLAGS.NAVHIDDEN_YES
else -> return false
}
config.inputFlags =
maskAndApply(config.inputFlags, ResTableConfig.INPUT_FLAGS.NAVHIDDEN_MASK, value)
return true
}
fun parseNavigation(part: String, config: ConfigDescription): Boolean {
config.navigation = when (part) {
WILDCARD_NAME -> ResTableConfig.NAVIGATION.ANY
"nonav" -> ResTableConfig.NAVIGATION.NONAV
"dpad" -> ResTableConfig.NAVIGATION.DPAD
"trackball" -> ResTableConfig.NAVIGATION.TRACKBALL
"wheel" -> ResTableConfig.NAVIGATION.WHEEL
else -> return false
}
return true
}
fun parseScreenSize(part: String, config: ConfigDescription): Boolean {
if (part == WILDCARD_NAME) {
config.screenWidth = 0
config.screenHeight = 0
return true
}
// Format is NxM
val width = part.substringBefore('x').toIntOrNull() ?: return false
val height = part.substringAfter('x').toIntOrNull() ?: return false
if (width < height) {
return false
}
config.screenWidth = width
config.screenHeight = height
return true
}
fun parseVersion(part: String, config: ConfigDescription): Boolean {
if (part == WILDCARD_NAME) {
config.sdkVersion = 0
config.minorVersion = 0
return true
}
// Format is vN
if (!part.startsWith('v')) {
return false
}
val version = part.substring(1).toIntOrNull()?.toShort() ?: return false
config.sdkVersion = version
config.minorVersion = 0
return true
}
fun maskAndApply(current: Byte, mask: Int, value: Byte) =
maskAndApply(current.toInt(), mask, value.toInt()).toByte()
fun maskAndApply(current: Int, mask: Int, value: Int): Int =
(current and mask.inv()) or value
@@ -0,0 +1,138 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.aaptcompiler
import com.android.aapt.Resources
import com.android.aaptcompiler.proto.serializeCompiledFileToPb
import com.google.protobuf.CodedOutputStream
import java.io.InputStream
import java.io.OutputStream
/**
* Represents and handles the output of resource compilation.
*
* [Container] represents a collection of the output from a single file being compiled by the
* ResourceCompiler. These can contain multiple entries, such as multiple flattened XMLs from
* aapt:attr outlining. The Container should only be used once the total number of required entries
* are known.
*/
class Container(val output: OutputStream, val totalEntryCount: Int) {
private var currentEntryCount = 0
enum class EntryType(val value: Int) {
RES_TABLE(0x00),
RES_FILE(0x01),
}
init {
val codedOut = CodedOutputStream.newInstance(output)
codedOut.writeFixed32NoTag(FORMAT_MAGIC)
codedOut.writeFixed32NoTag(FORMAT_VERSION)
codedOut.writeFixed32NoTag(totalEntryCount)
codedOut.flush()
}
fun addResTableEntry(table: Resources.ResourceTable) {
if (currentEntryCount >= totalEntryCount) {
error("Too many entries being serialized.")
}
++currentEntryCount
val codedOut = CodedOutputStream.newInstance(output)
// Write the type.
codedOut.writeFixed32NoTag(EntryType.RES_TABLE.value)
// Write the aligned size
val size = table.serializedSize
val padding = (4 - size % 4) % 4
codedOut.writeFixed64NoTag(size.toLong())
// Write the table
table.writeTo(codedOut)
// Write the padding
writePadding(padding, codedOut)
codedOut.flush()
closeIfFinished()
}
fun addFileEntry(input: InputStream, file: ResourceFile) =
addFileEntryImpl(input.readBytes(), file)
fun addXmlEntry(resource: XmlResource) =
addFileEntryImpl(resource.xmlProto.toByteArray(), resource.file)
private fun addFileEntryImpl(content: ByteArray, file: ResourceFile) {
if (currentEntryCount >= totalEntryCount) {
error("Too many entries being serialized.")
}
++currentEntryCount
val codedOut = CodedOutputStream.newInstance(output)
// Write the type.
codedOut.writeFixed32NoTag(EntryType.RES_FILE.value)
val compiledFile = serializeCompiledFileToPb(file)
// Write the aligned size.
val headerSize = compiledFile.serializedSize
val headerPadding = (4 - (headerSize % 4)) % 4
val dataSize = content.size
val dataPadding = (4 - (dataSize % 4)) % 4
val totalSize =
RES_FILE_ENTRY_HEADER_SIZE.toLong() + headerSize + headerPadding + dataSize + dataPadding
codedOut.writeFixed64NoTag(totalSize)
// Write the res file header size.
codedOut.writeFixed32NoTag(headerSize)
// Write the data payload size.
codedOut.writeFixed64NoTag(dataSize.toLong())
// Write the header (config etc.)
compiledFile.writeTo(codedOut)
writePadding(headerPadding, codedOut)
// Write the actual file content with padding.
codedOut.write(content, 0, dataSize)
writePadding(dataPadding, codedOut)
codedOut.flush()
closeIfFinished()
}
private fun writePadding(padding: Int, codedOut: CodedOutputStream) {
for (i in 1..padding) {
codedOut.writeRawByte(0.toByte())
}
}
private fun closeIfFinished() {
if (currentEntryCount == totalEntryCount) {
output.close()
}
}
companion object {
const val FORMAT_MAGIC = 0x54504141
const val FORMAT_VERSION = 1
const val RES_FILE_ENTRY_HEADER_SIZE = 12
}
}
@@ -0,0 +1,298 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.aaptcompiler
internal const val DEC_EXPONENT_MARKER = 'e'
internal const val HEX_EXPONENT_MARKER = 'p'
internal const val SINGLE_PRECISION_BIAS = 127
internal const val SINGLE_PRECISION_EXP_MIN = -126
internal const val SINGLE_PRECISION_EXP_MAX = 127
internal const val FLOAT_NEGATIVE_MASK = 0x80000000.toInt()
internal const val MANTISSA_EXPONENT_ADJUST_VALUE = 0xffffff
internal const val DENORMAL_EXPONENT_ADJUST_VALUE = MANTISSA_EXPONENT_ADJUST_VALUE - 1
internal const val SCALAR_SHIFT_1 = 1L shl 60
internal const val SCALAR_SHIFT_2 = 1L shl 61
internal const val SCALAR_SHIFT_3 = 1L shl 62
internal const val SCALAR_SHIFT_4 = 1L shl 63
internal const val DEC_SIGNIFICAND_LEADING_BIT = 1L shl 59
internal const val DEC_SIGNIFICAND_DOWN_SHIFT = 36
internal const val ROUND_VALUE = 1L shl 35
/**
* The significands of all powers of ten from 10^-1 to 10^-47 in that order. This is used in decimal
* parsing.
*
* The format of the significand is a fixed point where the 59th bit is the one's place, and the
* bits [58:0] represent the basic fraction. The 59th bit is guaranteed to be set in this format
* and is the highest order set bit.
*
* For example the significand of 10^-1 is 0x0ccccccccccccccc which in significand format would be:
*
* 1.100110011001100110011001100...
*
* The associated power of 2 which would multiply this significand to get the actual value is in the
* [negativeShifts] array.
*
* The reason why the 59th bit was chosen to be the most significant bit is that the significand can
* be safely multiplied by 9 (the highest digit value) without creating a overflow. This allows for
* us to take the advantage of Long precision while still allowing computation to be quick.
*
* Long values are used has they have an overwhelmingly higher precision than floats guaranteeing
* a lower margin of error.
*
* These values were generated.
*/
internal val negativeSignificands = arrayOf(
0x0ccccccccccccccc,
0x0a3d70a3d70a3d70,
0x083126e978d4fdf3,
0x0d1b71758e219652,
0x0a7c5ac471b47842,
0x08637bd05af6c69b,
0x0d6bf94d5e57a42b,
0x0abcc77118461cef,
0x089705f4136b4a59,
0x0dbe6fecebdedd5b,
0x0afebff0bcb24aaf,
0x08cbccc096f5088c,
0x0e12e13424bb40e1,
0x0b424dc35095cd80,
0x0901d7cf73ab0acd,
0x0e69594bec44de15,
0x0b877aa3236a4b44,
0x09392ee8e921d5d0,
0x0ec1e4a7db69561a,
0x0bce5086492111ae,
0x0971da05074da7be,
0x0f1c90080baf72cb,
0x0c16d9a0095928a2,
0x09abe14cd44753b5,
0x0f79687aed3eec55,
0x0c612062576589dd,
0x09e74d1b791e07e4,
0x0fd87b5f28300ca0,
0x0cad2f7f5359a3b3,
0x0a2425ff75e14fc3,
0x081ceb32c4b43fcf,
0x0cfb11ead453994b,
0x0a6274bbdd0fadd6,
0x084ec3c97da624ab,
0x0d4ad2dbfc3d0778,
0x0aa242499697392d,
0x0881cea14545c757,
0x0d9c7dced53c7225,
0x0ae397d8aa96c1b7,
0x08b61313bbabce2c,
0x0df01e85f912e37a,
0x0b267ed1940f1c61,
0x08eb98a7a9a5b04e,
0x0e45c10c42a2b3b0,
0x0b6b00d69bb55c8d,
0x09226712162ab070,
0x0e9d71b689dde71a
)
/**
* The array of all binary shifts of the powers of ten from 10^-1 to 10^-47 in that order. This is
* used in decimal parsing.
*
* The shift represents the power of 2 associated with the significand in the [negativeSignificands]
* array. That is to say the value of a power of ten that is negative can be represented as:
*
* negativeSignificands[n] * 2^(negativeShifts[n])
*
* For example 10^-1 significand is 1.100_1100_ where the _1100_ is infinitely repeating, and the
* negative shift is -4. Thus:
*
* 1.100_1100_ * 2^-4 = .0_0011_ = 10^-1
*
* These values were generated.
*/
internal val negativeShifts = arrayOf(
-4,
-7,
-10,
-14,
-17,
-20,
-24,
-27,
-30,
-34,
-37,
-40,
-44,
-47,
-50,
-54,
-57,
-60,
-64,
-67,
-70,
-74,
-77,
-80,
-84,
-87,
-90,
-94,
-97,
-100,
-103,
-107,
-110,
-113,
-117,
-120,
-123,
-127,
-130,
-133,
-137,
-140,
-143,
-147,
-150,
-153,
-157
)
/**
* The significands of all powers of ten from 10^1 to 10^38 in that order. This is used in decimal
* parsing.
*
* The format of the significand is a fixed point where the 59th bit is the one's place, and the
* bits [58:0] represent the basic fraction. The 59th bit is guaranteed to be set in this format
* and is the highest order set bit.
*
* For example the significand of 10^3 is 0x0c80000000000000, which in significand format would be:
*
* 1.100100..00
*
* Note: 1100100 in binary is equal to 100 in decimal.
*
* The associated power of 2 which would multiply this significand to get the actual value is in the
* [positiveShifts] array.
*
* The reason why the 59th bit was chosen to be the most significant bit is that the significand can
* be safely multiplied by 9 (the highest digit value) without creating a overflow. This allows for
* us to take the advantage of Long precision while still allowing computation to be quick.
*
* Long values are used as they have an overwhelmingly higher precision than floats guaranteeing
* a lower margin of error.
*
* These values were generated.
*/
internal val positiveSignificands = arrayOf(
0x0800000000000000,
0x0a00000000000000,
0x0c80000000000000,
0x0fa0000000000000,
0x09c4000000000000,
0x0c35000000000000,
0x0f42400000000000,
0x0989680000000000,
0x0bebc20000000000,
0x0ee6b28000000000,
0x09502f9000000000,
0x0ba43b7400000000,
0x0e8d4a5100000000,
0x09184e72a0000000,
0x0b5e620f48000000,
0x0e35fa931a000000,
0x08e1bc9bf0400000,
0x0b1a2bc2ec500000,
0x0de0b6b3a7640000,
0x08ac7230489e8000,
0x0ad78ebc5ac62000,
0x0d8d726b7177a800,
0x0878678326eac900,
0x0a968163f0a57b40,
0x0d3c21bcecceda10,
0x084595161401484a,
0x0a56fa5b99019a5c,
0x0cecb8f27f4200f3,
0x0813f3978f894098,
0x0a18f07d736b90be,
0x0c9f2c9cd04674ed,
0x0fc6f7c404581229,
0x09dc5ada82b70b59,
0x0c5371912364ce30,
0x0f684df56c3e01bc,
0x09a130b963a6c115,
0x0c097ce7bc90715b,
0x0f0bdc21abb48db2,
0x096769950b50d88f
)
/**
* The array of all binary shifts of the powers of ten from 10^0 to 10^38 in that order. This is
* used in decimal parsing.
*
* The shift represents the power of 2 associated with the significand in the [positiveSignificands]
* array. That is to say the value of a power of ten that is positive can be represented as:
*
* positiveSignificands[n] * 2^(positiveShifts[n])
*
* For example 10^3 significand is 1.1001 and the positive shift = 6
*
* 1.1001 * 10^6 = 1100100 = 64 + 32 + 4 = 100 (base ten) = 10^2
*
* These values were generated.
*/
internal val positiveShifts = arrayOf(
0,
3,
6,
9,
13,
16,
19,
23,
26,
29,
33,
36,
39,
43,
46,
49,
53,
56,
59,
63,
66,
69,
73,
76,
79,
83,
86,
89,
93,
96,
99,
102,
106,
109,
112,
116,
119,
122,
126
)
@@ -0,0 +1,679 @@
package com.android.aaptcompiler
/**
* Parses the float in a faster way than String.toFloat. Returns null on failure.
*
* Handles both decimal floats (ex. 3.14e0) and Hexadecimal floats (0x3.d2fp0).
* Does not handle denormal values (values that exist between 1.1754942e-38 and 0 which require
* special formatting.
*
* Also does not handle special values (positive/negative infinity, NAN, etc.)
* In other words, if this method returns null, it does not necessarily imply that the string is not
* a valid float value.
*
* Some float values are considered to be valid with [parseFloat] which are not considered valid
* with [String.toFloat]. Specifically, hexadecimal floating point values do not require a exponent
* marker. That is:
*
* 0x1.2p0 is valid for both [parseFloat] and [String.toFloat]
*
* 0x1.2 is valid for [parseFloat] but not [String.toFloat]
*
* @param text the text to be translated into a float value.
* @return The float representation of text, or null if the float could not be parsed.
*/
fun parseFloat(text: String): Float? {
if (text.isEmpty()) return null
val lowerText = text.toLowerCase()
if (lowerText.startsWith("0x") || lowerText.startsWith("0x", 1)) {
return parseFloatHex(lowerText)
}
return parseFloatDec(lowerText)
}
/**
* Gets the significand equivalent to the value 1 x 10^exponent. Used in decimal float parsing.
*
* This selects the correct index from either the [positiveSignificands] or [negativeSignificands].
* If the value is out of range 0L is returned.
*
* The power of 10 is represented as:
* [getValue] * 2^[getShift]
* where the significand is fixed point with 59 bits of precision.
* @see getShift
*
* @param exponent the power of 10 that the significand represents.
* @return the correct significand or 0L if the exponent is out of bounds.
*/
private fun getValue(exponent: Int) =
when {
exponent > positiveSignificands.size -> 0L
exponent >= 0 -> positiveSignificands[exponent]
exponent >= -negativeSignificands.size -> negativeSignificands[-exponent - 1]
else -> 0L
}
/**
* Gets the binary exponent associated with the value 1 x 10^exponent. Used in decimal float
* parsing.
*
* This selects the correct index from either the [positiveShifts] or [negativeShifts]. If the value
* is out of range 0 is returned.
*
* The power of 10 is represented as:
* [getValue] * 2^[getShift]
* where the significand is fixed point with 59 bits of precision.
* @see getShift
*
* @param exponent the power of 10, which corresponds the shift requested.
* @return the correct binary shift or 0 if the exponent is out of bounds.
*/
private fun getShift(exponent: Int) =
when {
exponent > positiveSignificands.size -> 0
exponent >= 0 -> positiveShifts[exponent]
exponent >= -negativeSignificands.size -> negativeShifts[-exponent - 1]
else -> 0
}
/**
* Returns the shift that would normalize the given value to proper significand format. Used in
* decimal float parsing.
*
* This method is used to normalize a significand multiplied by a scalar, by returning how many bits
* the value should be shifted in order to have the significant bit in the 59th position.
*
* @param valueToNormalize: The significand, after multiplication by a scalar, to be
* re-normalized.
* @return the amount the significand should be downshifted, as well as the amount the corresponding
* binary shift should be increased. For example (all significands are in binary):
*
* 100.1 * 2^12
* Normalizes to:
* 1.001 * 2^14
* So the getScalarShift(100.1) -> 2
*/
private fun getScalarShift(valueToNormalize: Long) =
when {
(valueToNormalize and SCALAR_SHIFT_4) != 0L -> 4
(valueToNormalize and SCALAR_SHIFT_3) != 0L -> 3
(valueToNormalize and SCALAR_SHIFT_2) != 0L -> 2
(valueToNormalize and SCALAR_SHIFT_1) != 0L -> 1
else -> 0
}
/**
* Parse the decimal text as a 32-bit floating point number.
*
* Mechanically this is done in a number of steps. We'll use "03.01e1" as an example.
*
* First, find the exponent of the leading significant figure. The leading zero is ignored and
* therefore 3 is the leading significant figure. Although the 3 is in the one's place, the "e1"
* means that it is actually representing the ten's place. I.e. 3.01e1 = 30.1. So the exponent is 1.
*
* Second, we compute the significand and shift of the leading figure.
*
* 1.010 * 2^3 (10^1 in binary)
* * 3 (leading figure in decimal)
* = 11.110 * 2^3 (significand in binary)
*
* (normalize the significand):
* 11.110 * 2^3
* = 1.1110 * 2^4 (1)
*
* Third go through each decimal and add its significand (shifted relative to the significand of
* the leading figure) to the running total. For example:
*
* 0 in 3.01e1 does not contribute to the significand, so it is skipped.
*
* 1 in 3.01e1 represents the tenth's place, So compute the significand.
*
* 1.100_1100_ * 2^-4 (1/10 in binary, the _1100_ means repeating ad infinitum)
* * 1 (the value in the tenth's place in decimal)
* = 1.100_1100_ * 2^-4 (significand of the tenth's place in binary)
*
* (normalization is skipped because it is already normalized)
*
* (make the significand align with the shift of the leading figure in (1)):
* 1.100_1100_ * 2^-4
* = .0000000_1100_ * 2^4 (2)
*
* (Add to the accumulated significand)
* 1.1110 * 2^4 (The significand so far in binary from (1))
* + 0.0000000_1100_ * 2^4 (The aligned tenth's significand in binary from (2))
* = 1.1110000_1100_ * 2^4 (The accumulated significand) (3)
*
* (normalization is skipped, as the sum is already normalized.)
*
* Fourth and finally, convert the accumulated significand to IEEE Floating Point format:
*
* 1.1110000_1100_ * 2^4 (from (3))
*
* (cut the leading bit in the significand as it is assumed in the format):
* 1.1110000_1100_
* -> .1110000_1100_
*
* (limit to 23 bits of precision (rounded)):
* .1110000_1100_
* -> .11100001100110011001101 (significand in floating point format)
*
* (add the required bias to the exponent, as specified by the format):
* 4 (exponent of significand)
* + 127 (bias)
* = 131 (biased exponent)
* = 10000011 (in binary) (biased exponent in floating point format)
*
* (set the sign bit if the float was negative):
* 0 (0 means positive float)
*
* (combine the bits. sign - biased exponent - significand)
* 0 10000011 11100001100110011001101
*
* @param lowerCaseText: The string representing the float in lower case. The float should be of the
* form:
*
* [+/-]XXXXX[.]XXXXX[e[+/-]XXXXX]
*
* Where XXXXX represents zero or more digits.
* 1. Both the decimal (.) and the specified exponent (eXXXXX) is optional.
* 2. At least one digit is required that is not part of the specified exponent. This digit may
* be before or after the decimal. I.e.
* "1.e12" is okay.
* "1." is okay
* "-.0" is okay
* ".2e-2" is okay.
* "3.14" is okay.
* "15" is okay.
* "e12" is not okay.
* "." is not okay.
* 3. An optional sign ("+" or "-") can come before the significand and/or the exponent value.
* I.e.
* "+12" is okay
* "5e-2" is okay
* "-2.1e+0" is okay.
* "++2" is not okay.
* "4e+-4" is not okay.
*
* If the float is not well formed, the returned result will be null.
*
* If the float has a magnitude less than 1.1754942e-38 and is not zero, then null is returned
* (These values are called *denormal* and are not supported).
*
* If the float has a magnitude greater than 3.4028235e38, then null is returned (This would
* return +/- Infinity on [String.toFloat] which is not supported by this parser)
*
* Lastly, values such as "+Infinity", "-Infinity", "NaN", or other *special non-number* cases are
* not supported and will return null.
*
* @return The float value closest to the value represented by the text (within margin of error).
* If the text is malformed or the text represents a value that is not supported, null is returned
* instead.
*/
fun parseFloatDec(lowerCaseText: String): Float? {
// Save this for formatting later.
val negative = lowerCaseText[0] == '-'
// move past the sign if any.
var currentIndex = if (lowerCaseText[0] == '+' || negative) 1 else 0
// First step of parsing a float is to find the significant digit, and what power of 10 this
// represents. To start this, we run through the significand to find the first
// non-zero digit and its position relative to the decimal.
// Ex.:
// 1.3 -> 0
// 0.12 -> -1
// 0000111.14 -> 2
// 142 -> 2 (decimal is implicitly at the end of the number.)
// In auxiliary, we do 2 things during this step.
// 1. verify that the significand is in a valid format.
// 2. Save the position of the most significant figure for the computation step.
var significandBegin = currentIndex
var significandStart = false
var validSignificand = false
var subInteger = false
var mostSignificantExponent = 0
// If we hit the exponent marker 'e', we are done with the significand.
while (currentIndex < lowerCaseText.length &&
lowerCaseText[currentIndex] != DEC_EXPONENT_MARKER) {
// Hitting a decimal, means we've move into sub-integer positions.
if (lowerCaseText[currentIndex] == '.') {
if (subInteger) {
// hit second decimal, which is not allowed.
return null
} else {
subInteger = true
++currentIndex
continue
}
}
if (significandStart && !subInteger) {
++mostSignificantExponent
} else if (!significandStart && subInteger) {
--mostSignificantExponent
}
when (lowerCaseText[currentIndex]) {
in '1'..'9' -> {
validSignificand = true
if (!significandStart) {
significandStart = true
// Found the beginning of the significand.
significandBegin = currentIndex
}
}
'0' -> validSignificand = true
else -> return null
}
++currentIndex
}
val significandEnd = currentIndex
// We never hit a digit in the significand. E.x. "+.e12"
if (!validSignificand) {
return null
}
// Now that we have what the digit represents in the significand. We need to adjust this value
// based on what is in the specified exponent (the portion after the 'e')
// So we need to compute the exponent and adjust the exponent of the significant figure
// appropriately. For example:
//
// "299.792458e+6"
// has a significant digit of '2' in the hundred's position (significandExponent = 2)
//
// The "e+6" has a specified exponent of 6 (declaredExponent = 6)
// So the '2' actually represents the hundred-million's position (significandExponent = 8)
//
// In auxiliary, we validate that the declared exponent is a valid integer.
var declaredExponent = 0
if (currentIndex < lowerCaseText.length) {
// The float should not end in the exponent marker. I.e. "244e"
if (currentIndex == lowerCaseText.length - 1) {
return null
}
++currentIndex
val negativeExponent = lowerCaseText[currentIndex] == '-'
if (negativeExponent || lowerCaseText[currentIndex] == '+') {
++currentIndex
}
while (currentIndex < lowerCaseText.length) {
val currentValue = when (lowerCaseText[currentIndex]) {
in '0'..'9' -> lowerCaseText[currentIndex] - '0'
else -> return null
}
declaredExponent = declaredExponent*10 + currentValue
++currentIndex
}
if (negativeExponent) declaredExponent = -declaredExponent
}
// Since we've validated the format of the whole float, we can now handle the case of
// a significand with no significant figure (all zeros).
if (!significandStart) {
return if (negative) -(0.0.toFloat()) else 0.0.toFloat()
}
mostSignificantExponent += declaredExponent
// Now we begin step 2: computing the value of the significant figure.
currentIndex = significandBegin
// gather the initial value and shift from the significant figure.
var significandValue = getValue(mostSignificantExponent)
var baseBinaryShift = getShift(mostSignificantExponent)
// If the significant figure is out-of-range, we fail.
if (significandValue == 0L) {
return null
}
// Multiply by the declared scalar and re-normalize the significand.
significandValue *= lowerCaseText[currentIndex] - '0'
val significandScalarShift = getScalarShift(significandValue)
significandValue = significandValue ushr significandScalarShift
baseBinaryShift += significandScalarShift
var currentExponent = mostSignificantExponent - 1
++currentIndex
// Third go through each decimal and add its significand (shifted relative to the significand of
// the leading figure) to the running total.
compute@ while (currentIndex < significandEnd) {
val scalarValue = when (lowerCaseText[currentIndex]) {
in '1'..'9'-> (lowerCaseText[currentIndex] - '0')
'0' -> {
++currentIndex
--currentExponent
continue@compute
}
// We hit the decimal, which we don't need it anymore.
else -> {
++currentIndex
continue@compute
}
}
var currentValue = getValue(currentExponent)*scalarValue
var relativeDownShift = baseBinaryShift - getShift(currentExponent)
// make sure to re-normalize the significand before shifting.
val scalarShift = getScalarShift(currentValue)
currentValue = currentValue ushr scalarShift
relativeDownShift -= scalarShift
// control, no longer care about new decimals because the precision is too small to change
// the significand.
if (relativeDownShift > 59) {
break@compute
}
significandValue += currentValue ushr relativeDownShift
// Handle overflow from addition,
if ((significandValue and SCALAR_SHIFT_1) != 0L) {
significandValue = significandValue ushr 1
baseBinaryShift += 1
}
--currentExponent
++currentIndex
}
//Finally convert the Information into single precision floating point format.
// round the significand
significandValue += ROUND_VALUE
// handle overflow
if ((significandValue and SCALAR_SHIFT_1) != 0L) {
significandValue = significandValue ushr 1
baseBinaryShift += 1
}
// Handle the cases where the binary exponent is out-of-bounds.
if (baseBinaryShift < SINGLE_PRECISION_EXP_MIN || baseBinaryShift > SINGLE_PRECISION_EXP_MAX ) {
return null
}
// Trim off the front bit and shift down to floating point precision for standard format.
val mantissa =
(significandValue and DEC_SIGNIFICAND_LEADING_BIT.inv()).ushr(
DEC_SIGNIFICAND_DOWN_SHIFT).toInt()
// Bias the exponent
val biasedExp = baseBinaryShift + SINGLE_PRECISION_BIAS
return Float.fromBits(
(if (negative) FLOAT_NEGATIVE_MASK else 0) or mantissa or (biasedExp shl 23))
}
/**
* Parse the hexadecimal text as a 32 floating point number.
*
* Mechanically this is done in three steps. We'll use "-0x3a.1p+5"
*
* First, process the significand, to acquire 2 pieces of information.
* 1. The normalized value of the significand. Unlike in decimal parsing, we can compute the
* precision exactly in base16. Because of this, we only need 24 bits (+1 for rounding) for the
* significand.
*
* 3 a. 1
* = 00111010.0001 (3a.1 in binary)
* = 1.110100001 (normalized significand)
*
* 2. the binary offset of the leading bit of the significand. Basically how much the decimal
* needs to be shifted left for this significant figure to be in the one's position.
*
* 111010.0001 << 5
* = 1.110100001 (1)
*
* Second, compute the signed specified exponent (ex. "p+5") and add it to the computed offset.
*
* "p+5" => 5
* 5 (specified exponent)
* + 5 (exponent of leading bit)
* = 10 (total shift.) (2)
*
* Third and finally, convert the significand and shift to IEEE floating point format:
*
* 1.110100001 * 2^10 (from (1) and (2))
*
* (cut the leading bit in the significand as it is assumed in the format):
* 1.110100001
* -> .110100001
*
* (limit to 23 bits of precision (rounded)):
* .110100001
* -> .1101000010000000000000 (significand in floating point format)
*
* (add the required bias to the exponent as specified by the format):
* 10 (exponent of significand)
* + 127 (bias)
* = 137 (biased exponent)
* = 10001001 (in binary) (biased exponent in floating point format)
*
* (set the sign bit if the float was negative):
* 1 (1 means negative float)
*
* (combine the bits. sign - biased exponent - significand)
* 1 10001001 1101000010000000000000
*
* @param lowerCaseText: The string representing the float in lower case. The float should be of the
* form:
*
* [+/-]0xYYYYY[.]YYYYY[p[+/-]XXXXX]
*
* Where XXXXX represents 0 or more decimal digits, and YYYYY represents 0 or more hexadecimal
* digits.
* 1. The significand must be lead by "0x".
* 2. Both the decimal (.) and the specified exponent (pXXXXX) is optional.
* 3. At least one digit is required that is not part of the specified exponent. This digit may
* be before or after the decimal. I.e.
* "0x.8p-4" is okay.
* "0x1." is okay
* "-0x.0" is okay
* "0x.ap-2" is okay.
* "0x3.ef" is okay.
* "0xf" is okay.
* "0xp12" is not okay.
* "0x." is not okay.
* "0x" is not okay.
* 4. An optional sign ("+" or "-") can come before the significand and/or the exponent value.
* I.e.
* "+0xa" is okay
* "0x1ce-3" is okay
* "-0x2.8p+10" is okay.
* "++0xa" is not okay.
* "0x4p+-19" is not okay.
*
* If the float is not well formed, the returned result will be null.
*
* If the float has a magnitude less than 0x1.fffffep-127 and is not zero, then null is returned
* (These values are called *denormal* and are not supported).
* Note: 0x1.fffffep-127 rounds up to 1p-126, which is the minimum normal value.
*
* If the float has a magnitude greater than 1.fffffep127, then null is returned (This would
* return +/- Infinity on [String.toFloat] which is not supported by this parser)
*
* Lastly, values such as "+Infinity", "-Infinity", "NaN", or other *special non-number* cases are
* not supported and will return null.
*
* @return The float value closest to the value represented by the text (within margin of error).
* If the text is malformed or the text represents a value that is not supported, null is returned
* instead.
*/
fun parseFloatHex(lowerCaseText: String): Float? {
// Save for formatting later.
val negative = lowerCaseText[0] == '-'
val signed = lowerCaseText[0] == '+' || negative
// move past the sign and the '0x'
var currentIndex = if (signed) 3 else 2
if (lowerCaseText.substring(currentIndex - 2, currentIndex) != "0x") {
return null
}
// First step of parsing the float is to normalizing the significand.
// That is:
// abcd -> 1.579a * 2^11
// 1 -> 1. * 2^0
// a -> 1.4 * 2^3
// Technically, we compute the mantissa (everything after the leading bit of the significand).
var currentMantissa = 0
var currentSkew = 0
var mantissaBits = 0
var mantissaStart = false
var subInteger = false
var validMantissa = false
// Compute the mantissa of the float.
mantissa@ while (currentIndex < lowerCaseText.length &&
lowerCaseText[currentIndex] != HEX_EXPONENT_MARKER) {
val indexValue = when (lowerCaseText[currentIndex]) {
in '0'..'9' -> {
validMantissa = true
(lowerCaseText[currentIndex] - '0')
}
in 'a'..'f' -> {
validMantissa = true
(lowerCaseText[currentIndex] - 'a') + 10
}
'.' -> {
if (subInteger) {
// hit 2 decimal points in the mantissa, which is not allowed.
return null
} else {
// Hit first decimal point. So we've gone subinteger.
subInteger = true
++currentIndex
continue@mantissa
}
}
// Invalid character.
else -> return null
}
// Start the mantissa, recording how many bits are required.
if (!mantissaStart && indexValue !=0) {
mantissaStart = true
currentMantissa = indexValue
// The number of bits in the mantissa is decided by the leading bit in the hex value.
when {
indexValue >= 8 -> {
currentSkew -= 1
mantissaBits = 3
}
indexValue >= 4 -> {
currentSkew -= 2
mantissaBits = 2
}
indexValue >= 2 -> {
currentSkew -= 3
mantissaBits = 1
}
else -> {
currentSkew -= 4
mantissaBits = 0
}
}
// In the case that mantissa has been started and we can acquire more precision, do so.
} else if (mantissaStart && mantissaBits < 24) {
currentMantissa = (currentMantissa shl 4) + indexValue
mantissaBits += 4
}
// Even though we can't get more precise than 23 bits (with 1 extra bit for rounding),
// we still need to search for the exponent marker and adjust the skew.
// Adjust the skew of the mantissa.
if (mantissaStart && !subInteger) {
currentSkew += 4
} else if (!mantissaStart && subInteger) {
currentSkew -= 4
}
++currentIndex
}
if (!validMantissa) {
return null
}
// Part 2, Solve the for the declared exponent if the exponent marker exists.
var declaredExponent = 0
if (currentIndex < lowerCaseText.length) {
// The float should not end in the exponent marker.
if (currentIndex == lowerCaseText.length - 1) {
return null
}
++currentIndex
val negativeExponent = lowerCaseText[currentIndex] == '-'
if (negativeExponent || lowerCaseText[currentIndex] == '+') {
++currentIndex
}
while (currentIndex < lowerCaseText.length) {
val currentValue = when (lowerCaseText[currentIndex]) {
in '0'..'9' -> lowerCaseText[currentIndex] - '0'
else -> return null
}
declaredExponent = declaredExponent*10 + currentValue
++currentIndex
}
if (negativeExponent) declaredExponent = -declaredExponent
}
// Align the mantissa
// MantissaBits may be greater than or less than 24 bits, so we need to adjust.
currentMantissa = if (24 - mantissaBits < 0) {
currentMantissa ushr (mantissaBits - 24)
} else {
currentMantissa shl (24 - mantissaBits)
}
// Remove the leading 1
currentMantissa = currentMantissa and MANTISSA_EXPONENT_ADJUST_VALUE
// Compute the exponent, taking care of rounding for the mantissa.
val exponent =
when {
(currentMantissa == MANTISSA_EXPONENT_ADJUST_VALUE) -> {
// mantissa overflowed after rounding.
currentMantissa = 0
declaredExponent + currentSkew + 1
}
(currentMantissa == DENORMAL_EXPONENT_ADJUST_VALUE &&
declaredExponent + currentSkew + 1 == SINGLE_PRECISION_EXP_MIN) -> {
currentMantissa = 0
declaredExponent + currentSkew + 1
}
else -> {
currentMantissa = ((currentMantissa + 1) ushr 1)
declaredExponent + currentSkew
}
}
// Sanity checks
// Zero and negative zero are special cases
if (!mantissaStart) return if (negative) -(0.0.toFloat()) else 0.0.toFloat()
// Bounds check the exponent
if (exponent < SINGLE_PRECISION_EXP_MIN || exponent > SINGLE_PRECISION_EXP_MAX) {
return null
}
// Finally, put the floating point data into standard format.
// Bias the exponent
val biasedExp = exponent + SINGLE_PRECISION_BIAS
return Float.fromBits(
(if (negative) FLOAT_NEGATIVE_MASK else 0) or currentMantissa or (biasedExp shl 23))
}
@@ -0,0 +1,146 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.aaptcompiler
import com.android.aaptcompiler.android.ResTableConfig
class LocaleValue {
// Language should be stored completely as lower case.
var language: String = ""
set(word) {
field = word.toLowerCase()
}
// Region should be stored completely as upper case.
var region: String = ""
set(word) {
field = word.toUpperCase()
}
// Script needs to start with a capital letter, and the rest needs to be lower case.
var script: String = ""
set(word) {
field = word.capitalize()
}
// Variant is left as-is.
var variant: String = ""
fun initFromParts(parts: List<String>, index: Int): Int {
val part = parts[index]
if (part.startsWith("b+")) {
// This is a "modified" BCP 47 language tag. Same semantics as BCP 47 tags, except that
// the separator is "+" and not "-". Skip the prefix "b+".
initFromBcp47TagImpl(part.substring(2), '+')
// BCP 47 only takes one part, so return 1.
return 1
} else {
if ((part.length == 2 || part.length == 3) && isAlpha(part) && part != "car") {
language = part
// It's also possible that the region is also set in the next word.
if (index + 1 < parts.size) {
val maybeRegion = parts[index+1]
if (maybeRegion.startsWith('r') && maybeRegion.length == 3) {
region = maybeRegion.substring(1)// skip the 'r'
// We consumed two parts, so return 2.
return 2
}
}
// If no region present, then we only consumed one part.
return 1
}
}
return 0
}
fun writeTo(config: ResTableConfig) {
if (language.isNotEmpty()) {
config.packLanguage(language)
}
if (region.isNotEmpty()) {
config.packRegion(region)
}
if (script.isNotEmpty()) {
if (script.length > config.localeScript.size) {
throw IllegalStateException(
"Locale script '$script' exceeds ${config.localeScript.size} characters.")
}
script.toByteArray().copyInto(config.localeScript, 0, 0, script.length)
}
if (variant.isNotEmpty()) {
if (variant.length > config.localeVariant.size) {
throw IllegalStateException(
"Locale variant '$variant' exceeds ${config.localeVariant.size} characters.")
}
variant.toByteArray().copyInto(config.localeVariant, 0, 0, variant.length)
}
}
fun initFromBcp47Tag(word: String) = initFromBcp47TagImpl(word, '-')
private fun isAlpha(word: String): Boolean = word.all { it.isLetter() }
private fun initFromBcp47TagImpl(word: String, separator: Char): Boolean {
val subTags = word.split(separator).map { it.toLowerCase() }
when (subTags.size) {
1 -> language = subTags[0]
2 -> {
language = subTags[0]
when (subTags[1].length) {
2, 3 -> region = subTags[1]
4 -> {
if (subTags[1][0].isDigit()) {
// This is a variant
variant = subTags[1]
} else {
script = subTags[1]
}
}
in 5..8 -> variant = subTags[1]
else -> return false
}
}
3 -> {
// Language is always the front.
language = subTags[0]
// The second is a script or a region code.
when (subTags[1].length) {
2, 3 -> region = subTags[1]
4 -> script = subTags[1]
else -> return false
}
// The third tag is either a region (if 2nd was a script), else a variant code.
if (subTags[2].length >= 4) {
variant = subTags[2]
} else {
region = subTags[2]
}
}
4 -> {
language = subTags[0]
script = subTags[1]
region = subTags[2]
variant = subTags[3]
}
else -> return false
}
return true
}
}
@@ -0,0 +1,268 @@
package com.android.aaptcompiler
import com.android.aaptcompiler.android.ResTableConfig
/**
* The struct that represents both [Span] and [UntranslatableSection] objects.
*
* @param tag The tag description if and only if the UnifiedSpan represents a [Span], otherwise a
* value of {@code null} if the UnifiedSpan represents a [UntranslatableSection]
* @param firstChar The UTF-16 index into the string where this span starts.
* @param lastChar The UTF-16 index into the string where this span ends, inclusive.
*/
data class UnifiedSpan(
val tag: String?, var firstChar: Int, var lastChar: Int): Comparable<UnifiedSpan> {
constructor(span: Span): this(span.name, span.firstChar, span.lastChar)
constructor(span: StringPool.Span): this(span.name.value(), span.firstChar, span.lastChar)
constructor(section: UntranslatableSection): this(null, section.startIndex, section.endIndex - 1)
fun isSpan() = tag != null
fun isUntranslatable() = tag == null
fun toSpan() = if (isSpan()) Span(tag!!, firstChar, lastChar) else null
override fun compareTo(other: UnifiedSpan) =
when {
firstChar != other.firstChar -> firstChar.compareTo(other.firstChar)
lastChar != other.lastChar -> lastChar.compareTo(other.lastChar)
else -> 0
}
}
internal fun mergeSpans(string: StyledString): List<UnifiedSpan> {
val unifiedSpans = mutableListOf<UnifiedSpan>()
string.spans().forEach {
unifiedSpans.add(UnifiedSpan(it))
}
string.untranslatableSections.forEach {
unifiedSpans.add(UnifiedSpan(it))
}
unifiedSpans.sort()
return unifiedSpans
}
internal fun pseudolocalizeStyledString(
original: StyledString, method: Pseudolocalizer.Method, pool: StringPool): StyledString {
val localizer = Pseudolocalizer(method)
// Collect the spans and untranslatable sections into one set of spans, sorted by first_char.
// This will effectively subdivide the string into multiple sections that can be individually
// pseudolocalized, while keeping the span indices synchronized.
val mergedSpans = mergeSpans(original)
// The new string to be created.
val newText = StringBuilder(localizer.start())
// Stack keeping track of the indices of what nested Span we're in.
val spanStack = mutableListOf<Int>()
val originalText = original.ref.value()
// The current position in the original text.
var cursor = 0
// We assume no nesting of untranslatable section, as XLIFF does not allow it.
var inTranslatable = true
var currentSpanId = 0
while (currentSpanId < mergedSpans.size || spanStack.isNotEmpty()) {
val currentSpan = mergedSpans.getOrNull(currentSpanId)
val parentSpan = if (spanStack.isEmpty()) null else mergedSpans[spanStack.last()]
if (currentSpan != null) {
if (parentSpan == null || parentSpan.lastChar >= currentSpan.lastChar) {
// There is no parent, or this span is the child of the parent.
// Pseudolocalize all the text until this span.
val substr = originalText.substring(cursor, currentSpan.firstChar)
cursor = currentSpan.firstChar
// Pseudolocalize the substring
val newSubstr = if (inTranslatable) localizer.text(substr) else substr
newText.append(newSubstr)
// rewrite the first char
currentSpan.firstChar = newText.length
if (currentSpan.isUntranslatable()) {
// An untranslatable section has begun.
inTranslatable = false
}
spanStack.add(currentSpanId)
++currentSpanId
continue
}
}
if (parentSpan != null) {
// There is a parent, and either this span is not a child of it, or there are no more spans.
// As such, we can finish up with the parent span.
val substr = originalText.substring(cursor, parentSpan.lastChar + 1)
cursor = parentSpan.lastChar + 1
// Pseudolocalize the substring
val newSubstring = if (inTranslatable) localizer.text(substr) else substr
newText.append(newSubstring)
// rewrite the last char
parentSpan.lastChar = newText.length - 1
if (parentSpan.isUntranslatable()) {
// Exiting an untranslatable section.
inTranslatable = true
}
spanStack.removeAt(spanStack.lastIndex)
}
}
// Finish the pseudolocalization at the end of the string.
newText.append(localizer.text(originalText.substring(cursor)))
newText.append(localizer.end())
val spans = mutableListOf<Span>()
mergedSpans.forEach {
if (it.isSpan()) {
spans.add(it.toSpan()!!)
}
}
val styleString = StyleString(newText.toString(), spans)
return StyledString(pool.makeRef(styleString), listOf())
}
internal fun pseudolocalizeBasicString(
original: BasicString, method: Pseudolocalizer.Method, pool: StringPool): BasicString {
try {
val localizer = Pseudolocalizer(method)
val originalText = original.ref.value()
val newText = StringBuilder(localizer.start())
// Pseudolocalize only the translatable sections.
var start = 0
for (section in original.untranslatables) {
// Pseudolocalize the content before the untranslatable section.
if (section.startIndex != start) {
newText.append(localizer.text(originalText.substring(start, section.startIndex)))
}
// Copy in the untranslatable content.
newText.append(originalText.substring(section.startIndex, section.endIndex))
start = section.endIndex
}
// Pseudolocalize the content after the last untranslatable section.
if (start != originalText.length) {
newText.append(localizer.text(originalText.substring(start)))
}
newText.append(localizer.end())
return BasicString(pool.makeRef(newText.toString()))
} catch (e: Exception) {
// TODO: add to list of errors
error("Failed to pseudo-localize string: $original")
}
}
internal fun pseudolocalizePlural(
original: Plural, method: Pseudolocalizer.Method, pool: StringPool): Plural {
val localizedPlural = Plural()
for (i in original.values.indices) {
val value = original.values[i]
if (value != null) {
localizedPlural.values[i] = when (value) {
is BasicString -> pseudolocalizeBasicString(value, method, pool)
is StyledString -> pseudolocalizeStyledString(value, method, pool)
else -> value.clone(pool)
}
}
}
return localizedPlural
}
class PseudolocaleGenerator{
private fun modifyForLocale(
config: ConfigDescription, method: Pseudolocalizer.Method): ConfigDescription {
val modified = ConfigDescription(config)
when (method) {
Pseudolocalizer.Method.ACCENT -> {
modified.language[0] = 'e'.toByte()
modified.language[1] = 'n'.toByte()
modified.country[0] = 'X'.toByte()
modified.country[1] = 'A'.toByte()
}
Pseudolocalizer.Method.BIDI -> {
modified.language[0] = 'a'.toByte()
modified.language[1] = 'r'.toByte()
modified.country[0] = 'X'.toByte()
modified.country[1] = 'B'.toByte()
}
else -> {}
}
return modified
}
// A value is pseudolocalizable if it does not define a locale (or is the default locale) and is
// translatable.
private fun isPseudolocalizable(configValue: ResourceConfigValue): Boolean {
val diff = configValue.config.diff(ConfigDescription())
if ((diff and ResTableConfig.CONFIG_LOCALE) != 0) {
return false
}
return configValue.value!!.translatable
}
private fun pseudolocalizeIfNeeded(
method: Pseudolocalizer.Method,
originalValue: ResourceConfigValue,
pool: StringPool,
entry: ResourceEntry) {
val valueToLocalize = originalValue.value
valueToLocalize ?: return
val localizedValue = when (valueToLocalize) {
is BasicString -> pseudolocalizeBasicString(valueToLocalize, method, pool)
is StyledString -> pseudolocalizeStyledString(valueToLocalize, method, pool)
is Plural -> pseudolocalizePlural(valueToLocalize, method, pool)
else -> return
}
localizedValue.source = valueToLocalize.source
localizedValue.weak = valueToLocalize.weak
val modifiedConfig = modifyForLocale(originalValue.config, method)
val newConfigValue = entry.findOrCreateValue(modifiedConfig, originalValue.product)
// Only use auto-generated pseudo-localization if none is defined.
if (newConfigValue.value == null) {
newConfigValue.value = localizedValue
}
}
fun consume (table: ResourceTable) {
for (resourcePackage in table.packages) {
for (resourceGroup in resourcePackage.groups) {
for (idToEntry in resourceGroup.entries.values) {
for (entry in idToEntry.values) {
val valuesToLocalize = entry.values.filter { isPseudolocalizable(it) }
for (value in valuesToLocalize) {
pseudolocalizeIfNeeded(Pseudolocalizer.Method.ACCENT, value, table.stringPool, entry)
pseudolocalizeIfNeeded(Pseudolocalizer.Method.BIDI, value, table.stringPool, entry)
}
}
}
}
}
}
}
@@ -0,0 +1,414 @@
package com.android.aaptcompiler
import com.android.aaptcompiler.android.isTruthy
// Placeholder marks
private const val PLACEHOLDER_OPEN = "\u00bb"
private const val PLACEHOLDER_CLOSE = "\u00ab"
// Special unicode characters to override directionality of the words
private const val RLM = "\u200f"
private const val RLO = "\u202e"
private const val PDF = "\u202c"
// Argument markers for text
private const val ARG_START = '{'
private const val ARG_END = '}'
private fun Char.pseudoLocalize() = when (this) {
'a' -> "\u00e5"
'b' -> "\u0253"
'c' -> "\u00e7"
'd' -> "\u00f0"
'e' -> "\u00e9"
'f' -> "\u0192"
'g' -> "\u011d"
'h' -> "\u0125"
'i' -> "\u00ee"
'j' -> "\u0135"
'k' -> "\u0137"
'l' -> "\u013c"
'm' -> "\u1e3f"
'n' -> "\u00f1"
'o' -> "\u00f6"
'p' -> "\u00fe"
'q' -> "\u0051"
'r' -> "\u0155"
's' -> "\u0161"
't' -> "\u0163"
'u' -> "\u00fb"
'v' -> "\u0056"
'w' -> "\u0175"
'x' -> "\u0445"
'y' -> "\u00fd"
'z' -> "\u017e"
'A' -> "\u00c5"
'B' -> "\u03b2"
'C' -> "\u00c7"
'D' -> "\u00d0"
'E' -> "\u00c9"
/* No 'F' */
'G' -> "\u011c"
'H' -> "\u0124"
'I' -> "\u00ce"
'J' -> "\u0134"
'K' -> "\u0136"
'L' -> "\u013b"
'M' -> "\u1e3e"
'N' -> "\u00d1"
'O' -> "\u00d6"
'P' -> "\u00de"
'Q' -> "\u0071"
'R' -> "\u0154"
'S' -> "\u0160"
'T' -> "\u0162"
'U' -> "\u00db"
'V' -> "\u03bd"
'W' -> "\u0174"
'X' -> "\u00d7"
'Y' -> "\u00dd"
'Z' -> "\u017d"
'!' -> "\u00a1"
'?' -> "\u00bf"
'$' -> "\u20ac"
else -> null
}
private fun Char.isPossiblePlaceHolderEnd() = when (this) {
's', 'S',
'c', 'C',
'd',
'o',
'x', 'X',
'f',
'e', 'E',
'g', 'G',
'a', 'A',
'b', 'B',
'h', 'H',
'%',
'n' -> true
else -> false
}
abstract class PseudoMethodImpl {
open fun start() = ""
open fun end() = ""
abstract fun text(originalText: String): String
abstract fun placeholder(originalText: String): String
}
object PseudoMethodNone : PseudoMethodImpl() {
override fun text(originalText: String) = originalText
override fun placeholder(originalText: String) = originalText
}
object PseudoMethodBidi: PseudoMethodImpl() {
private const val ESCAPE_CHAR = '\\'
override fun text(originalText: String): String {
val result = StringBuilder()
var lastSpace = true
var space = true
var escape = false
for (i in originalText.indices) {
val currentChar = originalText[i]
if (!escape && currentChar == ESCAPE_CHAR) {
escape = true
continue
}
space = (!escape && currentChar.isWhitespace()) ||
(escape && (currentChar == 'n' || currentChar == 't'))
if (lastSpace && !space) {
// Word start.
result.append(RLM + RLO)
} else if (!lastSpace && space) {
// Word end.
result.append(PDF + RLM)
}
lastSpace = space
if (escape) {
result.append(ESCAPE_CHAR)
}
result.append(currentChar)
}
if (!lastSpace) {
// End of last word
result.append(PDF + RLM)
}
return result.toString()
}
override fun placeholder(originalText: String): String =
// Surround a placeholder with directionality change sequence
RLM + RLO + originalText + PDF + RLM
}
class PseudoMethodAccent: PseudoMethodImpl() {
var depth = 0
var wordCount = 0
var length = 0
override fun start(): String {
var result = ""
if (depth == 0) {
result = "["
}
wordCount = 0
length = 0
++depth
return result
}
override fun end(): String {
val result = StringBuilder()
if (length.isTruthy()) {
result.append(' ')
result.append(pseudoGenerateExpansion(if (wordCount > 3) length else length/2))
}
wordCount = 0
length = 0
--depth
if (depth == 0) {
result.append("]")
}
return result.toString()
}
/**
* Converts characters so they look like they've been localized.
*
* Note: This leaves placeholder syntax untouched.
*/
override fun text(originalText: String): String {
val result = StringBuilder()
val size = originalText.length
var lastSpace = true
var currentIndex = 0
while (currentIndex < size) {
val currentChar = originalText[currentIndex]
when (currentChar) {
'%' -> {
// Placeholder syntax, no need to pseudolocalize.
currentIndex = processMaybePlaceholder(result, originalText, currentIndex)
}
'<', '&' -> {
// Html syntax, no need to pseudolocalize
currentIndex = processHtml(result, originalText, currentIndex)
}
else -> {
// This is a pure text that should be pseudolocalized.
val localized = currentChar.pseudoLocalize()
if (localized != null) {
result.append(localized)
} else {
val space = currentChar.isWhitespace()
if (lastSpace && !space) {
++wordCount
}
lastSpace = space
result.append(currentChar)
}
// Count only pseudolocalizable chars and delimiters
++length
}
}
++currentIndex
}
return result.toString()
}
override fun placeholder(originalText: String): String =
// Surround a placeholder with brackets
PLACEHOLDER_OPEN + originalText + PLACEHOLDER_CLOSE
private fun pseudoGenerateExpansion(length: Int): String {
val result = StringBuilder(EXPANSION_STRING)
while (result.length < length) {
result.append(" ")
result.append(EXPANSION_STRING)
}
val lastSpace = result.indexOf(' ', length + 1)
return result.substring(0, if (lastSpace == -1) result.length else lastSpace)
}
private fun processMaybePlaceholder(
partial: StringBuilder, source: String, startIndex: Int) : Int {
val size = source.length
var currentIndex = startIndex
var currentChar = source[currentIndex]
val chunk = StringBuilder().append(currentChar)
while (currentIndex + 1 < size) {
++currentIndex
currentChar = source[currentIndex]
chunk.append(currentChar)
if (currentChar.isPossiblePlaceHolderEnd()) {
break
}
if (currentIndex + 1 < size && currentChar == 't') {
++currentIndex
currentChar = source[currentIndex]
chunk.append(currentChar)
break
}
}
// Treat chunk as a placeholder unless it ends with '%'.
partial.append(if(currentChar == '%') chunk else placeholder(chunk.toString()))
return currentIndex
}
private fun processHtml(sourcePartial: StringBuilder, source: String, startIndex: Int): Int {
val size = source.length
var currentIndex = startIndex
var currentChar = source[currentIndex]
val htmlPartial = StringBuilder()
htmlTag@ while (currentIndex < size) {
if (currentChar == '&') {
val escapeText = StringBuilder().append(currentChar)
var htmlCodePosition = currentIndex
htmlCode@ while (htmlCodePosition + 1 < size) {
++htmlCodePosition
currentChar = source[htmlCodePosition]
escapeText.append(currentChar)
when (currentChar) {
';' -> {
// valid html code.
currentIndex = htmlCodePosition
break@htmlCode
}
'#', in 'a'..'z', in 'A'..'Z', in '0'..'9' -> {
// Valid html, skip.
}
else -> {
// Wrong html code.
break@htmlCode
}
}
}
htmlPartial.append(escapeText)
if (escapeText.toString() != "&lt;") {
break@htmlTag
}
continue
}
if (currentChar == '>') {
htmlPartial.append('>')
break@htmlTag
}
htmlPartial.append(currentChar)
++currentIndex
if (currentIndex >= size) {
// we didn't find the closing bracket, it wasn't valid HTML. Needs to be pseudolocalized, so
// return starting index, and append the first character to the partial string builder.
sourcePartial.append(source[startIndex])
return startIndex
}
currentChar = source[currentIndex]
}
sourcePartial.append(htmlPartial.toString())
return currentIndex
}
// Yes, "fiveteen".
private val EXPANSION_STRING = "one two three four five six seven eight nine ten eleven " +
"twelve thirteen fourteen fiveteen sixteen seventeen nineteen twenty"
}
class Pseudolocalizer(method: Method) {
private var implementation: PseudoMethodImpl
var lastDepth = 0
private set
enum class Method {
NONE,
ACCENT,
BIDI;
fun getMethod() = when (this) {
Method.NONE -> PseudoMethodNone
Method.ACCENT -> PseudoMethodAccent()
Method.BIDI -> PseudoMethodBidi
}
}
init {
implementation = method.getMethod()
}
fun setMethod(method: Method) {
implementation = method.getMethod()
}
fun start() = implementation.start()
fun end() = implementation.end()
fun text(originalText: String): String {
val out = StringBuilder()
var depth = lastDepth
var lastPos = 0
var pos = 0
val size = originalText.length
var escaped = false
while (pos < size) {
val currentChar = originalText[pos]
if (escaped) {
escaped = false
++pos
continue
}
if (currentChar == '\'') {
escaped = true
++pos
continue
}
if (currentChar == ARG_START) {
++depth
} else if (currentChar == ARG_END && depth.isTruthy()) {
--depth
}
if (lastDepth != depth || pos == size - 1) {
val pseudo = (lastDepth % 2) == 0
var nextPos = pos
if (!pseudo || depth == lastDepth) {
++nextPos
}
val chunkSize = nextPos - lastPos
if (chunkSize.isTruthy()) {
var chunk = originalText.substring(lastPos, nextPos)
if (pseudo) {
chunk = implementation.text(chunk)
} else if (originalText[lastPos] == ARG_START && originalText[nextPos - 1] == ARG_END) {
chunk = implementation.placeholder(chunk)
}
out.append(chunk)
}
if (pseudo && depth < lastDepth) {
out.append(implementation.end())
} else if (!pseudo && depth > lastDepth) {
out.append(implementation.start())
}
lastPos = nextPos
lastDepth = depth
}
++pos
}
return out.toString()
}
}
@@ -0,0 +1,37 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.aaptcompiler
import java.lang.RuntimeException
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class ResourceCompilationException(message: String, throwable: Throwable?): RuntimeException(message, throwable)
@@ -0,0 +1,382 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:JvmName("ResourceCompiler")
package com.android.aaptcompiler
import com.android.aaptcompiler.proto.serializeTableToPb
import com.itsaky.androidide.layoutlib.resources.ResourceType
import com.itsaky.androidide.layoutlib.resources.ResourceVisibility
import com.android.utils.FileUtils
import java.io.File
private const val VALUES_DIRECTORY_PREFIX = "values"
private const val XML_EXTENSION = "xml"
private const val RESOURCE_TABLE_EXTENSION = "arsc"
private const val PATCH_9_EXTENSION = "9.png"
private const val PNG_EXTENSION = "png"
/**
* The different options available while compiling resources.
*
* @param generateSymbolsPathData Path where the R.txt file should be written to. If null, symbols
* will not be generated.
* @param visibility The visibility of resources in the given file to process. If specified, then
* compilation of table files will fail if either the "public" or "public group" tags are
* encountered.
* @param requirePngCrunching whether PNG file crunching is enabled
* @param pseudolocalize Whether or not the pseudolocales en-XA and ar-XB are generated for default
* string resources.
* @param partialRFile file to output symbols defined in the resource file (currently only
* supported for non-raw XML files (e.g. values or layouts) to match AAPT2's behaviour)
* @param legacyMode Whether or not to error or warn on positional args not being specified in a
* translatable string with parameters.
* @param verbose Whether or not to show verbose logs.
* @param sourcePath Specified source path to be written in the compiled resource.
*/
data class ResourceCompilerOptions(
val generateSymbolsPathData: ResourcePathData?= null,
val visibility: ResourceVisibility? = null,
val requirePngCrunching: Boolean = false,
val pseudolocalize: Boolean = false,
val partialRFile: File? = null,
val legacyMode: Boolean = false,
val verbose: Boolean = false,
val sourcePath: String? = null)
/**
* Shows whether the ResourceCompiler can compile the given Resource File.
*
* @param file The file to check
* @return true if and only if the ResourceCompiler currently supports the ResourceFile.
*/
fun canCompileResourceInJvm(file: File, requirePngCrunching: Boolean): Boolean {
// Hidden files, while skipped, are still supported.
if (file.isHidden) return true
val pathData = extractPathData(file)
if (pathData.resourceDirectory == VALUES_DIRECTORY_PREFIX
&& pathData.extension == XML_EXTENSION) {
// file is a values table.
return true
} else {
val type = ResourceType.fromFolderName(pathData.resourceDirectory) ?: return false
if (type != ResourceType.RAW) {
if (pathData.extension == XML_EXTENSION) {
return true
} else if (pathData.extension.endsWith(PNG_EXTENSION)) {
// If we don't need to perform patch9 processing or png crunching we can process.
return pathData.extension != PATCH_9_EXTENSION && !requirePngCrunching
}
}
}
return true
}
/**
* Compiles the given resource file and puts the compiled process in the given output directory.
*
* @param file file to processed. How it is processed is determined by the resource compiler.
* @param options the [ResourceCompilerOptions] that should be considered while compiling the
* resource.
*
* Values files are compiled by extracting the all the resources and their values into a Resource
* Table to be used in Linking.
*
* Xml resource files are compiled by extracting all new (@+) ids and extracting all inlined
* aapt:attr attributes. The resulting xmls and aapt:attr are flattened.
*
* Png go through crunching (if not debug) and additional processing if the png is a patch-9 file.
*
* Lastly raw files are flattened straight to protocol buffers.
*
* See [canCompileResourceInJvm] to see what is supported.
*/
fun compileResource(
file: File, outputDirectory: File, options: ResourceCompilerOptions, logger: BlameLogger) {
// Skip hidden files.
if (file.isHidden) {
logger?.warning("Omitting file ${file.absolutePath} because it is hidden.")
return
}
// Extract resource type information from the full path.
val pathData = extractPathData(file, options.sourcePath ?: file.absolutePath)
// Determine how to compile the file based on its type.
val compileFunction = getCompileMethod(pathData, logger)
try {
compileFunction(pathData, outputDirectory, options, logger)
} catch (e: Exception) {
logger?.info("Failed to compile file", blameSource(pathData.source))
val message =
"Resource compilation failed (${e.message}. Cause: ${e.cause}). " +
"Check logs for more details."
throw ResourceCompilationException(message, e)
}
}
private fun getCompileMethod(pathData: ResourcePathData, logger: BlameLogger):
(ResourcePathData, File, ResourceCompilerOptions, BlameLogger) -> Unit {
if (pathData.resourceDirectory == VALUES_DIRECTORY_PREFIX &&
pathData.extension == XML_EXTENSION) {
pathData.extension = RESOURCE_TABLE_EXTENSION
return ::compileTable
} else {
val type = ResourceType.fromFolderName(pathData.resourceDirectory)
if (type == null) {
val errorMsg = "Invalid resource type '${pathData.resourceDirectory}'" +
" for file ${pathData.file.absolutePath}"
logger?.warning(errorMsg)
error(errorMsg)
}
if (type != ResourceType.RAW) {
if (pathData.extension == XML_EXTENSION) {
return ::compileXml
} else if (pathData.extension.endsWith(PNG_EXTENSION)) {
return ::compilePng
}
}
}
return ::compileFile
}
/**
* Compiles a xml values file into a flattened [ResourceTable] proto.
*
* After all resources are extracted from the given xml, if pseudolocalization is set,
* pseudolocale strings are created for each default string resource in the xml. The result is
* flattened to a file in the output directory.
*
* @param pathData the file to be processed.
* @param outputDirectory the directory to which the compiled file is to be placed.
* @throws IllegalStateException A failure occurred in processing this resource.
*/
private fun compileTable(
pathData: ResourcePathData,
outputDirectory: File,
options: ResourceCompilerOptions,
logger: BlameLogger) {
val outputFile = File(outputDirectory, pathData.getIntermediateContainerFilename())
logger?.info("Compiling XML table ${pathData.file.absolutePath} to $outputFile")
val table = ResourceTable(logger = logger)
val extractorOptions = TableExtractorOptions(
translatable = !pathData.name.contains("donottranslate"),
errorOnPositionalArgs = !options.legacyMode,
visibility = options.visibility)
val tableExtractor =
TableExtractor(table, pathData.source, pathData.config, extractorOptions, logger)
pathData.file.inputStream().use {
try {
tableExtractor.extract(it)
} catch (e: Exception) {
// For merged values there's no need to re-write as we don't know which line failed. The
// actual error will be raised by the table extractor.
throw ResourceCompilationException(
"Failed to compile values resource file ${pathData.file}", e
)
}
// Adds the fake locales: en-XA and ar-XB for each default-defined string resource. This is used
// for debugging apps with long text (en-XA) or rtl (ar-XB) language support.
if (options.pseudolocalize && extractorOptions.translatable) {
PseudolocaleGenerator().consume(table)
}
}
// Ensure we have the compilation package at least.
table.createPackage("")
table.sort()
val container = Container(outputFile.outputStream(), 1)
val pbTable = serializeTableToPb(table)
container.addResTableEntry(pbTable)
if (options.partialRFile != null) {
val builder = StringBuilder()
table.packages.forEach { pkg ->
// Only print resources defined locally, e.g. don't write android attributes
if (pkg.name.isEmpty()) {
pkg.groups.forEach { group ->
// Macros do not define resources and therefore don't belong in the R.txt.
if (group.type != AaptResourceType.MACRO) {
val javaType = if (group.type == AaptResourceType.STYLEABLE) "int[]" else "int"
group.entries.forEach { entry ->
val visibility = getVisibility(entry.value.values, group.type)
builder.appendln("${visibility.getName()} $javaType ${group.type.tagName} ${entry.key}")
if (group.type == AaptResourceType.STYLEABLE) {
// We also need to write down styleable children (entries)
group.getStyleable(entry).entries.forEach {
val childPackage =
if (!it.name.pck.isNullOrEmpty()) "_${it.name.pck}" else ""
// styleables and their children are always public
builder.appendln("public int styleable ${entry.key}${childPackage}_${it.name.entry}")
}
}
}
}
}
}
FileUtils.writeToFile(options.partialRFile, builder.toString())
}
}
}
private fun getVisibility(values: Collection<ResourceEntry>, type: AaptResourceType): ResourceVisibility {
// all declare styleables need to be marked as public so they're visible in the R classes
if (type == AaptResourceType.STYLEABLE) return ResourceVisibility.PUBLIC
var foundPublic = false
var foundPrivate = false
values.forEach {
// We only care about public and private since only these can clash
if (it.visibility.level == ResourceVisibility.PUBLIC) foundPublic = true
else if (it.visibility.level == ResourceVisibility.PRIVATE) foundPrivate = true
}
if (foundPublic && foundPrivate)
error("Resource cannot be both public and private: ${type.tagName} ${values.first().name}")
if (foundPublic) return ResourceVisibility.PUBLIC
if (foundPrivate) return ResourceVisibility.PRIVATE
return ResourceVisibility.PRIVATE_XML_ONLY // default
}
/**
* Compiles the raw file to proto, with no extra processing
*
* @param pathData the file to flatten.
* @param outputDirectory the directory to which the compiled file is to be placed.
*/
private fun compileFile(
pathData: ResourcePathData,
outputDirectory: File,
options: ResourceCompilerOptions,
logger: BlameLogger?) {
val outputFile = File(outputDirectory, pathData.getIntermediateContainerFilename())
logger?.info("Compiling file ${pathData.file.absolutePath} to $outputFile")
pathData.file.inputStream().use {
val resourceFile = ResourceFile(
ResourceName("", resourceTypeFromTag(pathData.resourceDirectory)!!, pathData.name),
pathData.config,
pathData.source,
ResourceFile.Type.Unknown
)
val container = Container(outputFile.outputStream(), 1)
container.addFileEntry(it, resourceFile)
if (options.partialRFile != null) {
val partialR = "default int ${pathData.type!!.tagName} ${pathData.name}"
FileUtils.writeToFile(options.partialRFile, partialR)
}
}
}
/**
* Compiles the XML resource file (such as layouts, drawables, ect. to a flattened proto file.
*
* This process has two primary steps:
*
* 1. Extract all new ids from the xml into a symbol table. "@+id/myView" for example.
*
* 2. Find and extract all inline (aapt:attr) XML resources. This includes assigning the declared
* attribute on the (aapt:attr) to the generated xml resource name.
*
* After both these steps, the symbol table and both the extracted XMLs and modified original XML
* are flattened to a single [Container] and written to the output directory.
*
* @param pathData the file to be processed.
* @param outputDirectory the directory to which the compiled file is to be placed.
* @throws IllegalStateException A failure occurred in processing this resource.
*/
private fun compileXml(
pathData: ResourcePathData,
outputDirectory: File,
options: ResourceCompilerOptions,
logger: BlameLogger?) {
val outputFile = File(outputDirectory, pathData.getIntermediateContainerFilename())
logger?.info("Compiling xml file ${pathData.file.absolutePath} to $outputFile")
val fileToProcess = ResourceFile(
ResourceName("", pathData.type!!, pathData.name),
pathData.config,
pathData.source,
ResourceFile.Type.ProtoXml
)
pathData.file.inputStream().use {
val xmlProcessor = XmlProcessor(source = pathData.source, logger = logger)
try {
xmlProcessor.process(fileToProcess, it)
} catch (e: Exception) {
throw ResourceCompilationException(
"Failed to compile resource file: " +
logger?.getOutputSource(blameSource(pathData.source)),
e
)
}
val container =
Container(outputFile.outputStream(), xmlProcessor.xmlResources.size)
for (resource in xmlProcessor.xmlResources) {
container.addXmlEntry(resource)
}
if (options.partialRFile != null) {
val builder = StringBuilder()
builder.appendln("default int ${pathData.type.tagName} ${pathData.name}")
fileToProcess.exportedSymbols.forEach { id ->
builder.appendln("default int id ${id.name.entry}")
}
FileUtils.writeToFile(options.partialRFile, builder.toString())
}
}
}
/**
* Compiles the given png file.
*
* This is broken up into 2 primary steps:
*
* If the input is a patch9 file, (*.9.png), it will undergo patch9 processing.
*
* If we are not running in debug mode, the png will undergo additional crunching.
*
* Finally, the processed file is written to the output directory. It is possible that no
* processing is done to the png file, and if so it will just be written exactly as if it were
* a raw file.
*
* @param pathData the file to process.
* @param outputDirectory the directory in which the processed file will be placed.
*/
private fun compilePng(
pathData: ResourcePathData,
outputDirectory: File,
options: ResourceCompilerOptions,
logger: BlameLogger?) {
logger?.info("Compiling image file ${pathData.file.absolutePath}")
if (pathData.extension == PATCH_9_EXTENSION) {
error("Patch 9 PNG processing is not supported with the JVM Android resource compiler.")
}
if (options.requirePngCrunching) {
error("PNG crunching is not supported with the JVM Android resource compiler.")
}
compileFile(pathData, outputDirectory, options, logger)
}
@@ -0,0 +1,95 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.aaptcompiler
data class ResourceFile(
val name: ResourceName,
val configuration: ConfigDescription,
val source: Source,
val type: Type,
val exportedSymbols: MutableList<SourcedResourceName> = mutableListOf()) {
override fun equals(other: Any?): Boolean {
if (other !is ResourceFile) return false
return name == other.name
}
override fun hashCode(): Int {
return name.hashCode()
}
enum class Type {
Unknown,
Png,
BinaryXml,
ProtoXml
}
}
data class ResourceName(
val pck: String?,
val type: AaptResourceType = AaptResourceType.RAW,
val entry: String? = null): Comparable<ResourceName> {
override fun compareTo(other: ResourceName): Int {
val pckCompare = when {
pck === other.pck -> 0
pck == null -> -1
other.pck == null -> 1
else -> pck.compareTo(other.pck)
}
if (pckCompare != 0) {
return pckCompare
}
val typeCompare = type.compareTo(other.type)
if (typeCompare != 0) {
return typeCompare
}
return when {
entry === other.entry -> 0
entry == null -> -1
other.entry == null -> 1
else -> entry.compareTo(other.entry)
}
}
override fun toString() : String {
val maybePck = if (!pck.isNullOrEmpty()) "$pck:" else ""
return "$maybePck${type.tagName}/$entry"
}
companion object {
val EMPTY = ResourceName("", AaptResourceType.RAW, "")
}
}
data class SourcedResourceName(val name: ResourceName, val line: Int)
fun Int.isValidId(): Boolean = (this and 0xff000000.toInt()) != 0 && this.isValidDynamicId()
fun Int.isValidDynamicId(): Boolean = (this and 0x00ff0000) != 0
fun Int.getPackageId(): Byte = (this shr 24).toByte()
fun Int.getTypeId(): Byte = (this shr 16).toByte()
fun Int.getEntryId(): Short = this.toShort()
fun resourceIdFromParts(packageId: Byte, typeId: Byte, entryId: Short): Int =
(packageId.toInt() shl 24) or (typeId.toInt() shl 16) or (entryId.toInt())
@@ -0,0 +1,106 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.aaptcompiler
import com.itsaky.androidide.layoutlib.resources.ResourceType
import java.io.File
/**
* Class for keeping track of Resource File of any kind.
*
* @param source The original source of this file.
* @param extension The extension of the file as recognized by the Kotlin Resource Compiler. The
* extension will always be the lowercase of the original extension.
* @param name The name of the file.
* @param resourceDirectory The generic name for the directory type that this resource was found in.
* i.e. ".../values-en/strings.xml" would have a [resourceDirectory] or "values"
* @param configString the configuration associated with this resource.
*/
data class ResourcePathData(
var source: Source,
var extension: String,
var name: String,
var resourceDirectory: String,
// Keep the original and append extra configs at the end.
var configString: String,
var file: File,
var config: ConfigDescription
) {
val type: AaptResourceType? = resourceTypeFromTag(resourceDirectory)
fun getIntermediateContainerFilename(): String {
return compilationRename(file)
}
}
fun extractPathData(file: File, sourcePath: String = file.absolutePath) : ResourcePathData {
val extension = if (file.name.toLowerCase().endsWith(".9.png")) {
"9.png"
} else {
file.name.substringAfterLast(".", "")
}
// Again the file name can contain multiple dots, and also because of the '.9.png' we can't just
// get substringBeforeLast('.').
val resName =
if (extension.isNotEmpty()) file.name.substringBefore(".$extension")
else file.name
val source = Source(sourcePath)
// TODO(b/142481190): think about obfuscation
val parentName = file.parentFile.name
val type = parentName.substringBefore("-")
val config = if (type != parentName) {
file.parentFile.name.substringAfter(type).substring(1)
} else {
""
}
val configDescription = parse(config)
return ResourcePathData(
source, extension.toLowerCase(), resName, type, config, file, configDescription)
}
/**
* Obtains the renaming for compilation for the given file. When compiling a file, `aapt2`
* will output a file with a name that depends on the file being compiled, as well as its path.
* This method will compute what the output name is for a given input.
*
* @param f the file
* @return the new file's name (this will take the file's path into consideration)
* @throws IllegalStateException cannot analyze file path
*/
internal fun compilationRename(f: File): String {
var fileName = f.name
val fileParent = f.parentFile
?: error("Could not get parent of file '" + f.absolutePath + "'")
val parentName = fileParent.name
// Split fileName into fileName and ext. If fileName does not have an extension, make ext empty.
val extIdx = fileName.lastIndexOf('.')
var ext = if (extIdx == -1) "" else fileName.substring(extIdx)
fileName = if (extIdx == -1) fileName else fileName.substring(0, extIdx)
// Values are compiled to arsc. This mirrors the implementation of the Compile method in
// frameworks/base/tools/aapt2/compile/Compile.cpp
// e.g. values/strings.xml becomes values_strings.arsc.flat and not values_strings.xml.flat.
if (parentName.startsWith("values") && ext == ".xml") {
ext = ".arsc"
}
return parentName + "_" + fileName + ext + ".flat"
}
@@ -0,0 +1,737 @@
package com.android.aaptcompiler
import com.android.aapt.Resources
import com.itsaky.androidide.layoutlib.resources.ResourceVisibility
import java.io.File
import java.util.SortedMap
import java.util.TreeMap
/**
* The container and index for all resources defined for a given app.
*
* <p> The Resource Table is an organizer of all resources. It sorts the resources by package, then
* by type, then by name, by config, and finally by product, if applicable. [addResource],
* [addFileReference] are used to add resources declared in the current app and have their resource
* names validated. While [addResourceMangled] and [addFileReferenceMangled] are used to add
* resources from libraries and are not validated.
*/
class ResourceTable(val validateResources: Boolean = false, val logger: BlameLogger? = null) {
/**
* The string pool used by this resource table. Values that reference strings must use
* this pool to create their strings.
*/
val stringPool = StringPool()
/** The list of packages in this table. */
internal val packages = mutableListOf<ResourceTablePackage>()
/**
* Set of dynamic packages that this table may reference. Their package names get encoded into the
* resources.arsc along with their compile-time assigned IDs.
*/
private val includedPackages = mutableMapOf<Int, String>()
enum class CollisionResult {
KEEP_ORIGINAL,
CONFLICT,
TAKE_NEW
}
data class SearchResult(
val tablePackage: ResourceTablePackage, val group: ResourceGroup, val entry: ResourceEntry)
/**
* Adds a resource to the table with the given value.
*
* @param name The full name of this resource. This includes package name, resource type, and the
* entry name of the resource.
* @param config The configuration this value for the given resource apply.
* @param product The product for which this value applies.
* @param value The value associated with the given resource.
* @return Returns false if and only if the resource was not able to be added. This is caused if
* the resource name failed to be validated or there exists a conflict with a resource already in
* the table.
*/
fun addResource(
name: ResourceName, config: ConfigDescription, product: String, value: Value): Boolean =
addResourceImpl(
name, 0, config, product, value, ::resourceNameValidator, ::resolveValueCollision)
/**
* Adds a resource to the table with the given id and value.
*
* @param name The full name of this resource. This includes package name, resource type, and the
* entry name of the resource.
* @param id the id of the given resource.
* @param config The configuration this value for the given resource apply.
* @param product The product for which this value applies.
* @param value The value associated with the given resource.
* @return Returns false if and only if the resource was not able to be added. This is caused if
* the resource name failed to be validated, there exists a conflict with a resource already in
* the table, or the id is a dynamic id but does not match with the package, type, or entry.
*/
fun addResourceWithId(
name: ResourceName,
id: Int,
config: ConfigDescription,
product: String,
value: Value): Boolean =
addResourceImpl(
name, id, config, product, value, ::resourceNameValidator, ::resolveValueCollision)
/**
* Adds a file reference resource to the table with the given file path.
*
* @param name The full name of this resource. This includes package name, resource type and the
* entry name of the resource.
* @param config The configuration this value for the given file reference applies.
* @param source The place in source where the reference exists.
* @param path The path name to the file being referenced.
* @return Returns false if and only if the resource was not able to be added. This is caused if
* the resource name failed to be validated or there exists a conflict with a resource already in
* the table.
*/
fun addFileReference(
name: ResourceName,config: ConfigDescription, source: Source, path: String): Boolean =
addFileReferenceImpl(
name, config, source, path, null, ::resourceNameValidator)
/**
* Same as [addFileReference], but doesn't verify the name of the file reference resource. This is
* used when loading resources from an existing binary resource table that may have mangled
* resources.
*
* @param name The full name of this resource. This includes package name, resource type and the
* entry name of the resource.
* @param config The configuration this value for the given file reference applies.
* @param source The place in source where the reference exists.
* @param path The path name to the file being referenced.
* @param file The file object referenced by this resource.
* @return Returns false if and only if the resource was not able to be added. This is caused if
* the resource name failed to be validated or there exists a conflict with a resource already in
* the table.
*/
fun addFileReferenceMangled(
name: ResourceName,
config: ConfigDescription,
source: Source,
path: String,
file: File): Boolean =
addFileReferenceImpl(name, config, source, path, file, ::skipNameValidator)
/**
* Same as [addResource], but doesn't verify the name of the resource. This is used when loading
* resources from an existing binary resource table that may have mangled names.
*
* @param name The full name of this resource. This includes package name, resource type, and the
* entry name of the resource.
* @param config The configuration this value for the given resource apply.
* @param product The product for which this value applies.
* @param value The value associated with the given resource.
* @return Returns false if and only if the resource was not able to be added. This is caused if
* there exists a conflict with a resource already in the table.
*/
fun addResourceMangled(
name: ResourceName, config: ConfigDescription, product: String, value: Value): Boolean =
addResourceImpl(
name, 0, config, product, value, ::skipNameValidator, ::resolveValueCollision)
/**
* Same as [addResourceWithId], but doesn't verify the name of the resource. This is used when
* loading resources from an existing binary resource table that may have mangled names.
*
* @param name The full name of this resource. This includes package name, resource type, and the
* entry name of the resource.
* @param id the id of the given resource.
* @param config The configuration this value for the given resource apply.
* @param product The product for which this value applies.
* @param value The value associated with the given resource.
* @return Returns false if and only if the resource was not able to be added. This is caused if
* there exists a conflict with a resource already in the table, or the id is a dynamic id but
* does not match with the package, type, or entry.
*/
fun addResourceWithIdMangled(
name: ResourceName,
id: Int,
config: ConfigDescription,
product: String,
value: Value) =
addResourceImpl(
name, id, config, product, value, ::skipNameValidator, ::resolveValueCollision)
/**
* Sets the resource with the given name to the set visibility. If this resource has no value in
* the table, the resource will be created with no value set to the given visibility.
*
* @param name The full name of the resource, whose visibility will be modified.
* @param visibility the new Visibility of the resource.
* @return true, if and only if the visibility was able to be updated. This may not happen if the
* visibility has already been set to at least as visible as the new visibility.
*/
fun setVisibility(name: ResourceName, visibility: Visibility) =
setVisibilityImpl(name, visibility, 0, ::resourceNameValidator)
fun setVisibilityWithId(name: ResourceName, visibility: Visibility, id: Int) =
setVisibilityImpl(name, visibility, id, ::resourceNameValidator)
fun setVisibilityMangled(name: ResourceName, visibility: Visibility) =
setVisibilityImpl(name, visibility, 0, ::skipNameValidator)
fun setVisibilityWithIdMangled(name: ResourceName, visibility: Visibility, id: Int) =
setVisibilityImpl(name, visibility, id, ::skipNameValidator)
fun setAllowNew(name: ResourceName, allowNew: AllowNew) =
setAllowNewImpl(name, allowNew, ::resourceNameValidator)
fun setAllowNewMangled(name: ResourceName, allowNew: AllowNew) =
setAllowNewImpl(name, allowNew, ::skipNameValidator)
fun setOverlayable(name: ResourceName, overlayable: OverlayableItem) =
setOverlayableImpl(name, overlayable, ::resourceNameValidator)
fun setOverlayableMangled(name: ResourceName, overlayable: OverlayableItem) =
setOverlayableImpl(name, overlayable, ::skipNameValidator)
fun findResource(name: ResourceName): SearchResult? {
val tablePackage = findPackage(name.pck!!)
tablePackage ?: return null
val group = tablePackage.findGroup(name.type)
group ?: return null
val entry = group.findEntry(name.entry!!)
entry ?: return null
return SearchResult(tablePackage, group, entry)
}
/**
* Returns the package struct with the given name, or null if such a package does not
* exist. The empty string is a valid package and typically is used to represent the
* 'current' package before it is known to the ResourceTable.
*
* @param name the name of the package.
* @return the [ResourceTablePackage] with the requested name or {@code null} if that package
* does not exist in the table.
*/
fun findPackage(name: String): ResourceTablePackage? {
return packages.find { it.name == name }
}
fun findPackageById(id: Byte): ResourceTablePackage? {
return packages.find { it.id == id }
}
fun createPackage(name: String, id: Byte = 0): ResourceTablePackage? {
val tablePackage = findOrCreatePackage(name)
if (id != 0.toByte()) {
when {
tablePackage.id == null -> {
tablePackage.id = id
return tablePackage
}
tablePackage.id != id -> return null
}
}
return tablePackage
}
/**
* Attempts to find a package having the specified name and ID. If not found, a new package
* of the specified parameters is created and returned.
*/
fun createPackageAllowingDuplicateNames(name: String, id: Byte): ResourceTablePackage {
val match = packages.find { it.name == name && it.id == id }
if (match != null) {
return match
}
val newPackage = ResourceTablePackage(name, id)
packages.add(newPackage)
return newPackage
}
private fun findOrCreatePackage(name: String): ResourceTablePackage {
val tablePackage = findPackage(name)
return when (tablePackage) {
null -> {
val newPackage = ResourceTablePackage()
newPackage.name = name
packages.add(newPackage)
newPackage
}
else -> tablePackage
}
}
fun sort() {
packages.sortWith(compareBy({it.name}, {it.id}))
for (pkg in packages) {
pkg.groups.sortWith(compareBy({it.type.ordinal}, {it.id}))
for (group in pkg.groups) {
for (entryByName in group.entries.values) {
for (entry in entryByName.values) {
entry.values.sortWith(compareBy({ it.config }, { it.product }))
}
}
}
}
}
private fun logError(source: BlameLogger.Source?, message: String) {
logger?.error(message, source)
}
private fun validateName(
nameValidator: (String) -> String, name: ResourceName, source: Source): Boolean {
val badCodePoint = nameValidator.invoke(name.entry!!)
if (badCodePoint.isNotEmpty()) {
logError(
blameSource(source),
"Resource '$name' has invalid entry name '${name.entry}'. " +
"Invalid character '$badCodePoint'.")
return false
}
return true
}
private fun addResourceImpl(
name: ResourceName,
id: Int,
config: ConfigDescription,
product: String,
value: Value,
nameValidator: (String) -> String,
conflictResolver: (Value, Value) -> CollisionResult): Boolean {
val source = value.source
if (!validateName(nameValidator, name, source)) {
return false
}
val tablePackage = findOrCreatePackage(name.pck!!)
val packageId = tablePackage.id
if (id.isValidDynamicId() && packageId != null && id.getPackageId() != packageId) {
logError(
blameSource(value.source),
"Failed to add resource '$name' with ID ${id.toString(16)} because " +
"package '${tablePackage.name}' already has ID ${packageId.toString(16)}.")
return false
}
val checkId = validateResources && id.isValidDynamicId()
val useId = !validateResources && id.isValidDynamicId()
val resourceGroup =
tablePackage.findOrCreateGroup(name.type, if (useId) id.getTypeId() else null)
val groupId = resourceGroup.id
if (checkId && groupId != null && groupId != id.getTypeId()) {
logError(
blameSource(value.source),
"Failed to add resource '$name' with ID ${id.toString(16)} because type " +
"'${resourceGroup.type.tagName}' already has ID ${groupId.toString(16)}.")
return false
}
val resourceEntry =
resourceGroup.findOrCreateEntry(name.entry!!, if (useId) id.getEntryId() else null)
val entryId = resourceEntry.id
if (checkId && entryId != null && id.getEntryId() != entryId) {
logError(
blameSource(value.source),
"Failed to add resource '$name' with ID ${id.toString(16)}, because resource already" +
" has ID ${resourceIdFromParts(packageId!!, groupId!!, entryId).toString(16)}.")
return false
}
val configValue = resourceEntry.findOrCreateValue(config, product)
val oldValue = configValue.value
if (oldValue == null) {
// Resource does not exist, add it now.
configValue.value = value
} else {
when (conflictResolver.invoke(oldValue, value)) {
CollisionResult.TAKE_NEW -> configValue.value = value
CollisionResult.CONFLICT -> {
val previousSource =
logger?.getOriginalSource(blameSource(oldValue.source)) ?: blameSource(oldValue.source)
logError(
blameSource(value.source),
"Duplicate value for resource '$name' with config '$config' and product '$product'. " +
"Resource was previously defined here: $previousSource.")
return false
}
CollisionResult.KEEP_ORIGINAL -> {}
}
}
if (id.isValidDynamicId()) {
tablePackage.id = id.getPackageId()
resourceGroup.id = id.getTypeId()
resourceEntry.id = id.getEntryId()
}
return true
}
private fun addFileReferenceImpl(
name: ResourceName,
config: ConfigDescription,
source: Source,
path: String,
file: File?,
nameValidator: (String) -> String): Boolean {
val fileReference = FileReference(stringPool.makeRef(path))
fileReference.source = source
fileReference.file = file
return addResourceImpl(
name, 0, config, "", fileReference, nameValidator, ::resolveValueCollision)
}
private fun setVisibilityImpl(
name: ResourceName,
visibility: Visibility,
id: Int,
nameValidator: (String) -> String) : Boolean {
val source = visibility.source
if (!validateName(nameValidator, name, source)) {
return false
}
val tablePackage = findOrCreatePackage(name.pck!!)
val packageId = tablePackage.id
if (id.isValidDynamicId() && packageId != null && id.getPackageId() != packageId) {
logError(
blameSource(source),
"Failed to add resource '$name' with ID ${id.toString(16)} because package " +
"'${tablePackage.name}' already has ID ${packageId.toString(16)}.")
return false
}
val checkId = validateResources && id.isValidDynamicId()
val useId = !validateResources && id.isValidDynamicId()
val resourceGroup =
tablePackage.findOrCreateGroup(name.type, if(useId) id.getTypeId() else null)
val groupId = resourceGroup.id
if (checkId && groupId != null && groupId != id.getTypeId()) {
logError(
blameSource(source),
"Failed to add resource '$name' with ID ${id.toString(16)} because type " +
"'${resourceGroup.type.tagName}' already has ID ${groupId.toString(16)}.")
return false
}
val resourceEntry =
resourceGroup.findOrCreateEntry(name.entry!!, if (useId) id.getEntryId() else null)
val entryId = resourceEntry.id
if (checkId && entryId != null && id.getEntryId() != entryId) {
logError(
blameSource(source),
"Failed to add resource '$name' with ID ${id.toString(16)}, because resource already " +
"has ID ${resourceIdFromParts(packageId!!, groupId!!, entryId).toString(16)}.")
return false
}
if (id.isValidDynamicId()) {
tablePackage.id = id.getPackageId()
resourceGroup.id = id.getTypeId()
resourceEntry.id = id.getEntryId()
}
if (visibility.level == ResourceVisibility.PUBLIC) {
// TODO: verify what resourceGroup's visibility is used for.
resourceGroup.visibility = ResourceVisibility.PUBLIC
}
when {
visibility.level == ResourceVisibility.UNDEFINED &&
resourceEntry.visibility.level != ResourceVisibility.UNDEFINED -> {
// We can't undefine a symbol. Ignore
}
visibility.level == ResourceVisibility.PRIVATE &&
resourceEntry.visibility.level == ResourceVisibility.PUBLIC -> {
logError(
blameSource(source),
"Failed to add resource '$name' as private (java-symbol) because it was " +
"previously defined as public.")
return false
}
visibility.level == ResourceVisibility.PUBLIC &&
resourceEntry.visibility.level == ResourceVisibility.PRIVATE -> {
logError(
blameSource(source),
"Failed to add resource '$name' as public because it was previously defined as " +
"private (java-symbol).")
return false
}
else -> {
// This symbol definition takes precedence.
resourceEntry.visibility = visibility
}
}
return true
}
private fun setAllowNewImpl(
name: ResourceName, allowNew: AllowNew, nameValidator: (String) -> String): Boolean {
if (!validateName(nameValidator, name, allowNew.source)) {
return false
}
val tablePackage = findOrCreatePackage(name.pck!!)
val group = tablePackage.findOrCreateGroup(name.type)
val entry = group.findOrCreateEntry(name.entry!!)
entry.allowNew = allowNew
return true
}
private fun setOverlayableImpl(
name: ResourceName, overlayable: OverlayableItem, nameValidator: (String) -> String): Boolean {
if (!validateName(nameValidator, name, overlayable.source)) {
return false
}
val tablePackage = findOrCreatePackage(name.pck!!)
val group = tablePackage.findOrCreateGroup(name.type)
val entry = group.findOrCreateEntry(name.entry!!)
val oldEntry = entry.overlayable
if (oldEntry != null) {
val previousSource =
logger?.getOriginalSource(blameSource(oldEntry.source)) ?: blameSource(oldEntry.source)
logError(
blameSource(overlayable.source),
"Failed to add overlayable declaration for resource '$name', because resource already " +
"has an overlayable defined here: $previousSource.")
return false
}
entry.overlayable = overlayable
return true
}
companion object {
fun resolveValueCollision(existing: Value, incoming: Value):CollisionResult {
val existingAttr = existing as? AttributeResource
val incomingAttr = incoming as? AttributeResource
incomingAttr ?: return when {
incoming.weak -> CollisionResult.KEEP_ORIGINAL
existing.weak -> CollisionResult.TAKE_NEW
else -> CollisionResult.CONFLICT
}
existingAttr ?: return when {
existing.weak -> CollisionResult.TAKE_NEW
else -> CollisionResult.CONFLICT
}
// Attribute specific handling. Since declarations and definitions of attributes can happen
// almost anywhere, we need special handling to see which definition sticks.
if (existingAttr.isCompatibleWith(incomingAttr)) {
// The two attributes are both DECLs, but they are plain attributes with compatible formats.
// keep the strongest.
return if (existingAttr.weak) CollisionResult.TAKE_NEW else CollisionResult.KEEP_ORIGINAL
}
if (existingAttr.weak && existingAttr.typeMask == Resources.Attribute.FormatFlags.ANY_VALUE) {
// Any incoming attribute is better than this.
return CollisionResult.TAKE_NEW
}
if (incomingAttr.weak && incomingAttr.typeMask == Resources.Attribute.FormatFlags.ANY_VALUE) {
// The incoming attribute may be a USE instead of a DECL. Keep the existing attribute.
return CollisionResult.KEEP_ORIGINAL
}
return CollisionResult.CONFLICT
}
fun resourceNameValidator(name: String): String =
if (isValidResourceEntryName(name)) "" else name
fun skipNameValidator(name: String): String {
return ""
}
}
}
/** the public status of a resource */
class Visibility(
val source: Source = Source(""),
val comment: String = "",
val level: ResourceVisibility = ResourceVisibility.UNDEFINED)
/** Represents <add-resource> in an overlay */
class AllowNew (
val source: Source,
val comment: String)
class Overlayable (
val name: String,
val actor: String,
val source: Source) {
constructor(): this("","", Source(""))
companion object {
const val ACTOR_SCHEME = "overlay"
const val ACTOR_SCHEME_URI = "$ACTOR_SCHEME://"
}
override fun equals(other: Any?): Boolean {
if (other is Overlayable) {
return name == other.name && actor == other.actor
}
return false
}
}
class OverlayableItem(
val overlayable: Overlayable,
val policies: Int = Policy.NONE,
val comment: String = "",
val source: Source = Source("")) {
override fun equals(other: Any?): Boolean {
if (other is OverlayableItem) {
return overlayable == other.overlayable && policies == other.policies
}
return false
}
/** Represents the types of overlays that are allowed to overlay the resource. */
object Policy {
const val NONE = 0
/** The resource can be overlaid by any overlay. */
const val PUBLIC = 1 shl 0
/** The resource can be overlaid by any overlay on the system partition. */
const val SYSTEM = 1 shl 1
/** The resource can be overlaid by any overlay on the vendor partition. */
const val VENDOR = 1 shl 2
/** The resource can be overlaid by any overlay on the product partition. */
const val PRODUCT = 1 shl 3
/** The resource can be overlaid by any overlay signed with the same signature as its actor. */
const val SIGNATURE = 1 shl 4
/** The resource can be overlaid by any overlay on the odm partition. */
const val ODM = 1 shl 5
/** The resource can be overlaid by any overlay on the oem partition. */
const val OEM = 1 shl 6
}
}
/** Represents all groups by type under a single package. */
class ResourceTablePackage(var name: String = "", var id: Byte? = null) {
internal val groups = mutableListOf<ResourceGroup>()
fun findGroup(type: AaptResourceType, groupId: Byte? = null) =
if (groupId != null) {
groups.find {type == it.type && groupId == it.id} ?:
groups.find {type == it.type && it.id == null}
} else {
groups.find {type == it.type}
}
fun findOrCreateGroup(type: AaptResourceType, groupId: Byte? = null): ResourceGroup {
val group = findGroup(type, groupId)
return when(group) {
null -> {
val newGroup = ResourceGroup(type)
newGroup.id = groupId
groups.add(newGroup)
newGroup
}
else -> group
}
}
}
/**
* Represents all resource entries grouped under a resource type (eg. string, drawable, layout,
* etc.).
*/
class ResourceGroup(val type : AaptResourceType) {
var id: Byte? = null
var visibility = ResourceVisibility.UNDEFINED
internal val entries = sortedMapOf<String, SortedMap<Short?, ResourceEntry>>()
// To get Styleable's children we need to reach the ResourceEntry's value first
internal fun getStyleable(entry: Map.Entry<String, SortedMap<Short?, ResourceEntry>>): Styleable {
// To get the actual value we need to find the correct Item that's nested deep in the map
// TODO: Keep children at map value level.
val mapEntry = entry.value
val styleableContainer = mapEntry.values.first().values
// Only one Item should be held in the container
if (styleableContainer.size != 1)
error("Too many resources in one entry: ${styleableContainer.size}")
// The children will be present under the Styleable Item.
return styleableContainer[0].value!! as Styleable
}
fun findEntry(name: String, entryId: Short? = null): ResourceEntry? {
val nameGroup = entries[name] ?: return null
return if (entryId != null) {
nameGroup[entryId] ?: nameGroup[null]
} else {
nameGroup[nameGroup.firstKey()]
}
}
fun findOrCreateEntry(name: String, entryId: Short? = null): ResourceEntry {
val entry = findEntry(name, entryId)
return when(entry) {
null -> {
val newEntry = ResourceEntry(name)
newEntry.id = entryId
entries.getOrPut(name) { TreeMap(nullsFirst()) }[entryId] = newEntry
newEntry
}
else -> entry
}
}
}
/** Represents a resource entry, which may have varying values for each defined configuration. */
class ResourceEntry(val name : String) {
var id: Short? = null
var visibility = Visibility(Source(""), "", ResourceVisibility.UNDEFINED)
var allowNew: AllowNew? = null
var overlayable: OverlayableItem? = null
internal val values = mutableListOf<ResourceConfigValue>()
fun findValue(config: ConfigDescription, product: String = ""): ResourceConfigValue? {
return values.find { it.config == config && it.product == product }
}
fun findOrCreateValue(config: ConfigDescription, product: String = ""): ResourceConfigValue {
val configValue = findValue(config, product)
return when (configValue) {
null -> {
val newConfigValue = ResourceConfigValue(config, product)
values.add(newConfigValue)
newConfigValue
}
else -> configValue
}
}
}
/**
* Represents a single value for an entry for a given Configuration.
*
* @property config The configuration for which this value is defined.
* @property product The product name for which this value is defined.
* @property value The actual Value.
*/
data class ResourceConfigValue(
val config: ConfigDescription,
val product : String,
var value: Value? = null)
@@ -0,0 +1,642 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.aaptcompiler
import com.android.aapt.Resources
import com.android.aaptcompiler.android.ResValue
import com.android.aaptcompiler.android.parseHex
import com.android.aaptcompiler.android.stringToFloat
import com.android.aaptcompiler.android.stringToInt
fun tryParseBool(string: String) : BinaryPrimitive? {
val boolean = parseAsBool(string)
boolean ?: return null
val data = if (boolean) -1 else 0
return BinaryPrimitive(ResValue(ResValue.DataType.INT_BOOLEAN, data))
}
fun parseAsBool(string: String) : Boolean? =
when (string.trim()) {
"true", "True", "TRUE" -> true
"false", "False", "FALSE" -> false
else -> null
}
fun tryParseNullOrEmpty(value: String): Item? {
val trimmedValue = value.trim()
return when (trimmedValue) {
"@null" -> makeNull()
"@empty" -> makeEmpty()
else -> null
}
}
fun makeNull() : Reference {
return Reference()
}
fun makeEmpty() : BinaryPrimitive {
return BinaryPrimitive(ResValue(ResValue.DataType.NULL, ResValue.NullFormat.EMPTY))
}
fun tryParseInt(value: String) : BinaryPrimitive? {
val trimmedValue = value.trim()
val resValue = stringToInt(trimmedValue)
return if (resValue != null) BinaryPrimitive(resValue) else null
}
fun parseResourceId(value: String): Int? {
val resValue = stringToInt(value)
if (resValue != null &&
resValue.dataType == ResValue.DataType.INT_HEX &&
resValue.data.isValidDynamicId()) {
return resValue.data
}
return null
}
fun tryParseFloat(value: String) : BinaryPrimitive? {
val floatResource = stringToFloat(value)
floatResource ?: return null
return BinaryPrimitive(floatResource)
}
fun tryParseColor(value: String): BinaryPrimitive? {
val colorStr = value.trim()
if (colorStr.isEmpty() || colorStr[0] != '#') {
return null
}
val dataType: ResValue.DataType
var data = 0
var error = false
when (colorStr.length) {
4 -> {
dataType = ResValue.DataType.INT_COLOR_RGB4
for (i in 1..3) {
val hexValue = parseHex(colorStr.codePointAt(i))
if (hexValue == -1) {
error = true
break
}
data = (data shl 8) or (hexValue + (hexValue shl 4))
}
data = data or 0xff000000.toInt()
}
5 -> {
dataType = ResValue.DataType.INT_COLOR_ARGB4
for (i in 1..4) {
val hexValue = parseHex(colorStr.codePointAt(i))
if (hexValue == -1) {
error = true
break
}
data = (data shl 8) or (hexValue + (hexValue shl 4))
}
}
7 -> {
dataType = ResValue.DataType.INT_COLOR_RGB8
for (i in 1..6) {
val hexValue = parseHex(colorStr.codePointAt(i))
if (hexValue == -1) {
error = true
break
}
data = (data shl 4) or hexValue
}
data = data or 0xff000000.toInt()
}
9 -> {
dataType = ResValue.DataType.INT_COLOR_ARGB8
for (i in 1..8) {
val hexValue = parseHex(colorStr.codePointAt(i))
if (hexValue == -1) {
error = true
break
}
data = (data shl 4) or hexValue
}
}
else -> return null
}
return if (error) BinaryPrimitive(ResValue()) else BinaryPrimitive(ResValue(dataType, data))
}
data class ReferenceInfo(val reference: Reference, val createNew: Boolean = false)
fun tryParseReference(value: String) : ReferenceInfo? {
val parsedReference = parseReference(value)
if (parsedReference != null) {
return parsedReference
}
val parsedAttribute = parseAttributeReference(value)
if (parsedAttribute != null) {
return parsedAttribute
}
return null
}
/**
* Parses the attribute of a xml element as a resource (attr) reference.
*
* <p> The reference must be of the form "<entry>" or "<package>:<entry>". If the type is specified,
* it can only be an attribute type. That is, something of the form "attr/<entry>" or
* "<package>:attr/<entry>". If the value fails to parse {@code null} is returned.
*
* @param value: String the name of the xml attribute to be parsed as a attribute reference.
* @return A [ReferenceInfo] representing the reference, or {@code null} if the input was invalid.
*
* [ReferenceInfo.createNew] will be false, as Attributes cannot be created.
*/
fun parseAttributeReference(value: String) : ReferenceInfo? {
val trimmedValue = value.trim()
if (trimmedValue.isEmpty()) {
return null
}
if (trimmedValue[0] != '?') {
return null
}
val possibleResourceName = extractResourceName(trimmedValue.substring(1))
if (!possibleResourceName.success) {
return null
}
if (!possibleResourceName.typeName.isEmpty() && possibleResourceName.typeName != "attr") {
return null
}
if (possibleResourceName.entry.isEmpty()) {
return null
}
val reference = Reference()
reference.name =
ResourceName(
possibleResourceName.packageName, AaptResourceType.ATTR, possibleResourceName.entry)
reference.referenceType = Reference.Type.ATTRIBUTE
return ReferenceInfo(reference, false)
}
/**
* Attempts to parse the string as a reference. This may be a value for an attribute in a layout, or
* may be a value for a resource defined in a values file.
*
* <p> The reference must be of one of the following forms:
*
* + "@<entry>"
* + "@<type>/<entry>"
* + "@<package>:<entry>"
* + "@<package>:<type>/<entry>"
*
* <p> Optionally, the '@' symbol can be followed by one of '+' or '*'. '+' means that the resource
* will be created and added to the resource table. This is only usable with id resources. The '*',
* however, represents that this resource has private visibility. A resource cannot be both private
* and created.
*
* @param value The string to be parsed.
* @return A [ReferenceInfo] representing the reference, or {@code null} if the input was invalid.
*
* + [ReferenceInfo.createNew] will be set if and only if a '+' followed the '@' and the reference
* had a type of id.
* + [ReferenceInfo.reference] will be set as private if and only if a '*' followed the '@' in the
* input.
*/
fun parseReference(value: String) : ReferenceInfo? {
val trimmedValue = value.trim()
if (trimmedValue.isEmpty()) {
return null
}
if (trimmedValue[0] == '@') {
var create = false
var offset = 1
if (trimmedValue.length > 1 && trimmedValue[1] == '+') {
create = true
offset += 1
}
val referenceNameInfo = parseResourceName(trimmedValue.substring(offset))
referenceNameInfo ?: return null
if (create && referenceNameInfo.isPrivate) {
return null
}
if (create && referenceNameInfo.resourceName.type != AaptResourceType.ID) {
return null
}
val reference = Reference()
reference.name = referenceNameInfo.resourceName
reference.isPrivate = referenceNameInfo.isPrivate
return ReferenceInfo(reference, create)
}
return null
}
data class ResourceNameInfo(val resourceName: ResourceName, val isPrivate: Boolean)
/**
* Attempts to parse the string as a Android Resource identifier. I.e. "lib:string/foo",
* "string/foo", or "*lib:string/foo".
*
* <p> The resource name must be in one of the following forms:
*
* + "<entry>"
* + "<type>/<entry>"
* + "<package>:<entry>"
* + "<package>:<type>/<entry>"
*
* The value may optionally be preceded by a '*' to represent that the resource name is private.
*
* @param value the value to be parsed.
* @return If parsing was successful, this function returns the [ResourceNameInfo] holding both the
* valid [ResourceName] and whether the parsed name identifies a private resource. If the value
* failed to parsed {@code null} is returned instead.
*/
fun parseResourceName(value: String) : ResourceNameInfo? {
if (value.isEmpty()) {
return null
}
var offset = 0
var isPrivate = false
if (value[0] == '*') {
isPrivate = true
offset = 1
}
val possibleResourceName = extractResourceName(value.substring(offset))
if (!possibleResourceName.success) {
return null
}
val resourceType = resourceTypeFromTag(possibleResourceName.typeName)
resourceType ?: return null
if (possibleResourceName.entry.isEmpty()) {
return null
}
return ResourceNameInfo(
ResourceName(possibleResourceName.packageName, resourceType, possibleResourceName.entry),
isPrivate)
}
data class PossibleResourceName(
val packageName: String, val typeName: String, val entry: String, val success: Boolean = true)
fun extractResourceName(value: String): PossibleResourceName {
var packageName = ""
var typeName = ""
var hasPackageSeparator = false
var hasTypeSeparator = false
var offsetCurrent = 0
if (value.isNotEmpty() && value[0] == '@') {
++offsetCurrent
}
var currentChar = offsetCurrent
while (currentChar < value.length) {
when {
typeName.isEmpty() && value[currentChar] == '/' -> {
hasTypeSeparator = true
typeName = value.substring(offsetCurrent, currentChar)
offsetCurrent = currentChar + 1
}
packageName.isEmpty() && value[currentChar] == ':' -> {
hasPackageSeparator = true
packageName = value.substring(offsetCurrent, currentChar)
offsetCurrent = currentChar + 1
}
}
++currentChar
}
val entryName = value.substring(offsetCurrent)
val success = !(hasPackageSeparator && packageName.isEmpty()) &&
!((hasTypeSeparator) && typeName.isEmpty())
return PossibleResourceName(packageName, typeName, entryName, success)
}
fun tryParseItemForAttribute(
value: String,
resourceTypeMask: Int,
onCreateReference: ((name: ResourceName) -> Boolean)? = null): Item? {
val nullOrEmpty = tryParseNullOrEmpty(value)
if (nullOrEmpty != null) {
return nullOrEmpty
}
val reference = tryParseReference(value)
if (reference != null) {
reference.reference.typeFlags = resourceTypeMask
if (reference.createNew) {
val result = onCreateReference?.invoke(reference.reference.name)
if (result != null && !result) {
return null
}
}
return reference.reference
}
if ((resourceTypeMask and Resources.Attribute.FormatFlags.COLOR_VALUE) != 0) {
val color = tryParseColor(value)
if (color != null) {
return color
}
}
if ((resourceTypeMask and Resources.Attribute.FormatFlags.BOOLEAN_VALUE) != 0) {
val boolean = tryParseBool(value)
if (boolean != null) {
return boolean
}
}
if ((resourceTypeMask and Resources.Attribute.FormatFlags.INTEGER_VALUE) != 0) {
val integer = tryParseInt(value)
if (integer != null) {
return integer
}
}
val floatMask = Resources.Attribute.FormatFlags.FLOAT_VALUE or
Resources.Attribute.FormatFlags.DIMENSION_VALUE or
Resources.Attribute.FormatFlags.FRACTION_VALUE
if ((resourceTypeMask and floatMask) != 0) {
val floatingPoint = tryParseFloat(value)
if (floatingPoint != null &&
(androidTypeToAttributeTypeMask(floatingPoint.resValue.dataType) and resourceTypeMask) != 0) {
return floatingPoint
}
}
return null
}
fun androidTypeToAttributeTypeMask(type: ResValue.DataType) =
when (type) {
ResValue.DataType.NULL,
ResValue.DataType.REFERENCE,
ResValue.DataType.ATTRIBUTE,
ResValue.DataType.DYNAMIC_REFERENCE -> Resources.Attribute.FormatFlags.REFERENCE_VALUE
ResValue.DataType.STRING -> Resources.Attribute.FormatFlags.STRING_VALUE
ResValue.DataType.FLOAT -> Resources.Attribute.FormatFlags.FLOAT_VALUE
ResValue.DataType.DIMENSION -> Resources.Attribute.FormatFlags.DIMENSION_VALUE
ResValue.DataType.FRACTION -> Resources.Attribute.FormatFlags.FRACTION_VALUE
ResValue.DataType.INT_DEC,
ResValue.DataType.INT_HEX -> {
Resources.Attribute.FormatFlags.INTEGER_VALUE or
Resources.Attribute.FormatFlags.ENUM_VALUE or
Resources.Attribute.FormatFlags.FLAGS_VALUE
}
ResValue.DataType.INT_BOOLEAN -> Resources.Attribute.FormatFlags.BOOLEAN_VALUE
ResValue.DataType.INT_COLOR_ARGB8,
ResValue.DataType.INT_COLOR_RGB8,
ResValue.DataType.INT_COLOR_ARGB4,
ResValue.DataType.INT_COLOR_RGB4 -> Resources.Attribute.FormatFlags.COLOR_VALUE
else -> 0
}
fun verifyJavaStringFormat(string: String): Boolean {
var argumentCount = 0
var nonpositionalArgs = false
var currentIndex = 0
while (currentIndex < string.length) {
val codePoint = string.codePointAt(currentIndex)
if (codePoint == '%'.toInt() && currentIndex + 1 < string.length) {
++currentIndex
if (string.codePointAt(currentIndex) == '%'.toInt()) {
++currentIndex
continue
}
++argumentCount
val numDigits = consumeDigits(string.substring(currentIndex))
when {
numDigits > 0 -> {
currentIndex += numDigits
if (currentIndex < string.length && string.codePointAt(currentIndex) != '$'.toInt()) {
// The digits were a size, but not a positional argument
nonpositionalArgs = true
}
}
currentIndex < string.length && string.codePointAt(currentIndex) == '<'.toInt() -> {
// Reusing last argument, bad idea since positions can be moved around during translation
nonpositionalArgs = true
++currentIndex
// Optionally we can have a $ after
if (currentIndex < string.length && string.codePointAt(currentIndex) == '$'.toInt()) {
++currentIndex
}
}
else -> nonpositionalArgs = true
}
// Ignore size, width, flags, etc.
search@ while (currentIndex < string.length) {
when (string.codePointAt(currentIndex)) {
'-'.toInt(),
'#'.toInt(),
'+'.toInt(),
' '.toInt(),
','.toInt(),
'('.toInt(),
in '0'.toInt()..'9'.toInt() -> ++currentIndex
else -> break@search
}
}
/*
* This is a shortcut to detect strings that are going to Time.format()
* instead of String.format()
*
* Comparison of String.format() and Time.format() args:
*
* String: ABC E GH ST X abcdefgh nost x
* Time: DEFGHKMS W Za d hkm s w yz
*
* Therefore we know it's definitely Time if we have:
* DFKMWZkmwyz
*/
if (currentIndex < string.length) {
when (string.codePointAt(currentIndex)) {
'D'.toInt(),
'F'.toInt(),
'K'.toInt(),
'M'.toInt(),
'W'.toInt(),
'Z'.toInt(),
'k'.toInt(),
'm'.toInt(),
'w'.toInt(),
'y'.toInt(),
'z'.toInt() -> return true
}
}
}
++currentIndex
}
if (argumentCount > 1 && nonpositionalArgs) {
// Multiple arguments were specified, but some or all were non positional. Translated strings
// may rearrange the order of the arguments, which will break the string.
return false
}
return true
}
fun consumeDigits(string: String): Int {
var currentIndex = 0
while (currentIndex < string.length && string[currentIndex].isDigit()) {
++currentIndex
}
return currentIndex
}
data class ParsedParentInfo(val parent: Reference?, val errorString: String) {
companion object {
val EMPTY = ParsedParentInfo(null, "")
}
}
fun parseStyleParentReference(str: String): ParsedParentInfo {
if (str.isEmpty()) {
ParsedParentInfo.EMPTY
}
var name = str
var hasLeadingIdentifiers = false
var privateRef = false
// Skip over these identifiers. A style's parent is a normal reference.
val nameStart = name.codePointAt(0)
if (nameStart == '@'.toInt() || nameStart == '?'.toInt()) {
hasLeadingIdentifiers = true
name = name.substring(1)
}
if (name.codePointAt(0) == '*'.toInt()) {
privateRef = true
name = name.substring(1)
}
val possibleResourceName = extractResourceName(name)
// if we have a type make sure it is a Style
if (possibleResourceName.typeName.isNotEmpty()) {
val parsedType = resourceTypeFromTag(possibleResourceName.typeName)
if (parsedType != AaptResourceType.STYLE) {
val errorString = "Invalid resource type ${possibleResourceName.typeName} for parent of style"
return ParsedParentInfo(null, errorString)
}
}
val resourceName =
ResourceName(
possibleResourceName.packageName, AaptResourceType.STYLE, possibleResourceName.entry)
if (!hasLeadingIdentifiers &&
resourceName.pck!!.isEmpty() &&
possibleResourceName.typeName.isNotEmpty()) {
val errorString = "Invalid parent reference '$str'"
return ParsedParentInfo(null, errorString)
}
val result = Reference()
result.name = resourceName
result.isPrivate = privateRef
return ParsedParentInfo(result, "")
}
fun parseXmlAttributeName(str: String): Reference {
val name = str.trim()
val result = Reference()
var startOffset = 0
if (name.isNotEmpty() && name.codePointAt(0) == '*'.toInt()) {
++startOffset
result.isPrivate = true
}
var packageName = ""
var entryName = ""
for (i in startOffset..(name.length - 1)) {
if (name.codePointAt(i) == ':'.toInt()) {
packageName = name.substring(startOffset, i)
entryName = name.substring(i+1)
}
}
result.name =
ResourceName(packageName, AaptResourceType.ATTR, if (entryName.isEmpty()) name else entryName)
return result
}
fun tryParseFlagSymbol(attribute: AttributeResource, value: String): BinaryPrimitive? {
val flagsType = ResValue.DataType.INT_HEX
var flagsData = 0
if (value.trim().isEmpty()) {
// Empty string is a valid flag (0)
return BinaryPrimitive(ResValue(flagsType, flagsData))
}
for (part in value.split('|')) {
val trimmedPart = part.trim()
var flagSet = false
for (symbol in attribute.symbols) {
// Flag symbols are stored as @package:id/symbol resources,
// so we need to match against the 'entry' part of the identifier
val flagResourceName = symbol.symbol.name
if (trimmedPart == flagResourceName.entry) {
flagsData = flagsData or symbol.value
flagSet = true
break
}
}
if (!flagSet) {
return null
}
}
return BinaryPrimitive(ResValue(flagsType, flagsData))
}
@@ -0,0 +1,561 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.aaptcompiler
import com.android.aapt.Resources
import com.android.aaptcompiler.android.ResValue
import com.android.aaptcompiler.android.deviceToHost
import com.android.aaptcompiler.android.hostToDevice
import com.android.utils.ILogger
import java.io.File
open class Value {
var source: Source = Source.EMPTY
var comment = ""
var weak = false
var translatable = true
}
abstract class Item : Value() {
abstract fun clone(newPool: StringPool): Item
abstract fun flatten(): ResValue?
}
/**
* An ID resource. Has no real value, just a place holder.
*/
class Id: Item() {
init {
weak = true
}
override fun equals(other: Any?) = other is Id
override fun flatten(): ResValue? {
return ResValue(ResValue.DataType.INT_BOOLEAN, 0.hostToDevice())
}
override fun clone(newPool: StringPool): Id {
val newId = Id()
newId.weak = weak
newId.comment = comment
newId.source = source
return newId
}
}
/**
* A reference to another resource. This maps to android::Res_value::TYPE_REFERENCE.
*
* A reference can be symbolic (with the name set to a valid resource name) or be
* numeric (the id is set to a valid resource ID).
*/
class Reference(var name: ResourceName = ResourceName.EMPTY): Item() {
enum class Type {
RESOURCE,
ATTRIBUTE
}
var id : Int? = null
var referenceType : Reference.Type = Reference.Type.RESOURCE
var isPrivate = false
var isDynamic = false
// Only used for macros, which can contain any format type
var typeFlags: Int? = Resources.Attribute.FormatFlags.ANY_VALUE
// Only used for macros, allos macros to have raw strings in them, not just references.
var allowRaw = true
override fun flatten(): ResValue? {
if (name?.type == AaptResourceType.MACRO) {
return null
}
val resId = id ?: 0
val dynamic = resId.isValidDynamicId() && isDynamic
val dataType = when {
referenceType == Reference.Type.RESOURCE ->
if (dynamic) ResValue.DataType.DYNAMIC_REFERENCE else ResValue.DataType.REFERENCE
else ->
if (dynamic) ResValue.DataType.DYNAMIC_ATTRIBUTE else ResValue.DataType.ATTRIBUTE
}
return ResValue(dataType, resId.hostToDevice())
}
override fun equals(other: Any?): Boolean {
if (other is Reference) {
return referenceType == other.referenceType &&
isPrivate == other.isPrivate &&
id == other.id &&
name == other.name &&
allowRaw == other.allowRaw &&
typeFlags == other.typeFlags
}
return false
}
override fun clone(newPool: StringPool): Reference {
val newRef = Reference(this.name)
newRef.id = id
newRef.referenceType = referenceType
newRef.isPrivate = isPrivate
newRef.isDynamic = isDynamic
newRef.comment = comment
newRef.source = source
return newRef
}
}
class FileReference(val path: StringPool.Ref): Item() {
// Handle to the file object from which this file can be read. This is only transient, and not
// persisted in any format.
var file: File? = null
// FileType of the file pointed to by `file` This is used to know how to inflate the file, or
// if to inflate at all (just copy)
var type: ResourceFile.Type = ResourceFile.Type.Unknown
override fun equals(other: Any?): Boolean {
if (other is FileReference) {
return path.value() == other.path.value() &&
type == other.type
}
return false
}
override fun flatten(): ResValue? {
return ResValue(ResValue.DataType.STRING, path.index().hostToDevice())
}
override fun clone(newPool: StringPool): FileReference {
val newFileRef = FileReference(newPool.makeRef(path))
newFileRef.file = file
newFileRef.type = type
newFileRef.comment = comment
newFileRef.source = source
return newFileRef
}
}
data class BinaryPrimitive(val resValue: ResValue): Item() {
override fun flatten(): ResValue? {
return ResValue(resValue.dataType, resValue.data.hostToDevice())
}
override fun clone(newPool: StringPool): BinaryPrimitive {
val newPrimitive = BinaryPrimitive(resValue)
newPrimitive.comment = comment
newPrimitive.source = source
return newPrimitive
}
}
data class AttributeResource(var typeMask: Int = 0, val logger: ILogger? = null): Value() {
data class Symbol(val symbol: Reference, val value: Int, val type: Byte)
var minInt = Int.MIN_VALUE
var maxInt = Int.MAX_VALUE
val symbols = mutableListOf<Symbol>()
fun matches(item: Item): Boolean {
val value = item.flatten()!!
val flattenedData = value.data.deviceToHost()
// Always allow references
val actualType = androidTypeToAttributeTypeMask(value.dataType)
// Only one type must match between the actual and the expected.
if ((actualType and (typeMask or Resources.Attribute.FormatFlags.REFERENCE_VALUE) == 0)) {
val errorMsg = "%s, Expected %s but got %s"
logger?.error(null, errorMsg, blameSource(source), maskToString(typeMask), item)
return false
}
// Enums and flags are encoded as integers, so check them first before doing any range checks.
if ((typeMask and actualType and Resources.Attribute.FormatFlags.ENUM_VALUE) != 0) {
for (symbol in symbols) {
if (flattenedData == symbol.value) {
return true
}
}
// If the attribute accepts integers, we can't fail here.
if ((typeMask and Resources.Attribute.FormatFlags.INTEGER_VALUE) == 0) {
val errorMsg = "%s, %s is not a valid enum."
logger?.error(null, errorMsg, blameSource(source), item)
return false
}
}
if ((typeMask and actualType and Resources.Attribute.FormatFlags.FLAGS_VALUE) != 0) {
var allFlags = 0
for (symbol in symbols) {
allFlags = allFlags or symbol.value
}
// Check if the flattened data is covered by the flag bit mask.
if ((allFlags and flattenedData) == flattenedData) {
return true
}
// If the attribute accepts integers, we can't fail here.
if ((typeMask and Resources.Attribute.FormatFlags.INTEGER_VALUE) == 0) {
val errorMsg = "%s, %s is not a valid flag."
logger?.error(null, errorMsg, blameSource(source), item)
return false
}
}
// If the value is an integer, we can't out of range.
return true
}
fun isCompatibleWith(other: AttributeResource): Boolean {
// if the high bits are set on any of these attribute type masks, then they are incompatible.
// We don't check that flags and enums are identical.
if ((typeMask and Resources.Attribute.FormatFlags.ANY_VALUE.inv()) != 0 ||
(other.typeMask and Resources.Attribute.FormatFlags.ANY_VALUE.inv()) != 0) {
return false
}
// Every attribute accepts a reference.
val thisTypeMask = typeMask or Resources.Attribute.FormatFlags.REFERENCE_VALUE
val otherTypeMask = other.typeMask or Resources.Attribute.FormatFlags.REFERENCE_VALUE
return thisTypeMask == otherTypeMask
}
}
private fun maskToString(typeMask: Int, target: Int, success: String) =
if ((typeMask and target) == target) success else ""
private fun maskToString(typeMask: Int): String {
val result = StringBuilder()
result.append(maskToString(typeMask, Resources.Attribute.FormatFlags.BOOLEAN_VALUE, "| boolean"))
result.append(maskToString(typeMask, Resources.Attribute.FormatFlags.COLOR_VALUE, "| color"))
result.append(
maskToString(typeMask, Resources.Attribute.FormatFlags.DIMENSION_VALUE, "| dimension"))
result.append(maskToString(typeMask, Resources.Attribute.FormatFlags.ENUM_VALUE, "| enum"))
result.append(maskToString(typeMask, Resources.Attribute.FormatFlags.FLAGS_VALUE, "| flags"))
result.append(maskToString(typeMask, Resources.Attribute.FormatFlags.FLOAT_VALUE, "| float"))
result.append(
maskToString(typeMask, Resources.Attribute.FormatFlags.FRACTION_VALUE, "| fraction"))
result.append(
maskToString(typeMask, Resources.Attribute.FormatFlags.INTEGER_VALUE, "| integer"))
result.append(
maskToString(typeMask, Resources.Attribute.FormatFlags.REFERENCE_VALUE, "| reference"))
result.append(
maskToString(typeMask, Resources.Attribute.FormatFlags.STRING_VALUE, "| string"))
return if (result.isNotEmpty()) result.substring(2) else ""
}
data class UntranslatableSection(var startIndex: Int, var endIndex: Int = startIndex) {
fun shift(offset : Int): UntranslatableSection {
return UntranslatableSection(startIndex + offset, endIndex + offset)
}
}
/**
* A raw, unprocessed string. This may contain quotations, escape sequences, and whitespace. This
* shall *NOT* end up in the final resource table.
*/
class RawString(val value: StringPool.Ref) : Item() {
override fun equals(other: Any?): Boolean {
if (other is RawString) {
return value.value() == other.value.value()
}
return false
}
override fun clone(newPool: StringPool): RawString {
val newRaw = RawString(newPool.makeRef(value))
newRaw.source = source
newRaw.comment = comment
return newRaw
}
override fun flatten(): ResValue? {
return ResValue(ResValue.DataType.STRING, value.index().hostToDevice())
}
}
/**
* A processed string resource. Unlike [StyledString], the string does not contain any spans, and
* is represented a single string.
*
* @param ref The reference to this basic string in the associated [StringPool].
* @param untranslatables The list of indexed sections of this string that should not be translated.
*/
class BasicString(
val ref: StringPool.Ref, val untranslatables: List<UntranslatableSection> = listOf()) : Item() {
override fun toString(): String {
return ref.value()
}
override fun equals(other: Any?): Boolean {
if (other is BasicString) {
if (toString() != other.toString()) {
return false
}
return untranslatables == other.untranslatables
}
return false
}
override fun clone(newPool: StringPool): BasicString {
val newString = BasicString(newPool.makeRef(ref), untranslatables)
newString.comment = comment
newString.source = source
return newString
}
override fun flatten(): ResValue? {
return ResValue(ResValue.DataType.STRING, ref.index().hostToDevice())
}
}
/**
* A processed string resource with xml spans. For example: "Hello <b>world!</b>"
*
* @param ref The reference to this StyledString in the associated [StringPool]. Use
* [spans] to find the spans associated with this string.
* @param untranslatableSections The list of indexed sections of this string that should not be
* translated.
*/
class StyledString(
val ref: StringPool.StyleRef,
val untranslatableSections: List<UntranslatableSection>) : Item() {
override fun equals(other: Any?): Boolean {
if (other is StyledString) {
return ref.value() == other.ref.value() &&
ref.spans() == other.ref.spans()
}
return false
}
override fun toString(): String {
return ref.value()
}
override fun clone(newPool: StringPool): StyledString {
val newStyledString = StyledString(newPool.makeRef(ref), untranslatableSections)
newStyledString.comment = comment
newStyledString.source = source
return newStyledString
}
override fun flatten(): ResValue? {
return ResValue(ResValue.DataType.STRING, ref.index().hostToDevice())
}
fun spans() = ref.spans()
}
class ArrayResource: Value() {
val elements = mutableListOf<Item>()
override fun equals(other: Any?): Boolean {
if (other is ArrayResource) {
return elements == other.elements
}
return false
}
fun clone(newPool: StringPool): ArrayResource {
val newArray = ArrayResource()
newArray.source = source
newArray.comment = comment
for (item in elements) {
newArray.elements.add(item.clone(newPool))
}
return newArray
}
}
class Style: Value() {
data class Entry(val key: Reference, val value: Item?)
var parent: Reference? = null
// If set to true, the parent was auto inferred from the style's name
var parentInferred = false
val entries = mutableListOf<Entry>()
fun clone(pool: StringPool): Style {
val newStyle = Style()
newStyle.parent = parent
newStyle.parentInferred = parentInferred
newStyle.comment = comment
newStyle.source = source
for (entry in entries){
newStyle.entries.add(
Entry(entry.key, entry.value?.clone(pool)))
}
return newStyle
}
override fun equals(other: Any?): Boolean {
if (other is Style) {
if (parent != other.parent) {
return false
}
val sortedEntriess = entries.sortedBy { it.key.name }
val otherSortedEntries = other.entries.sortedBy { it.key.name }
return sortedEntriess == otherSortedEntries
}
return false
}
fun mergeWith(other: Style, pool: StringPool) {
if (other.parent != null) {
parent = other.parent
}
val sortedEntries = entries.sortedBy { it.key.name }
val otherSortedEntries = other.entries.sortedBy { it.key.name }
val entryIterator = sortedEntries.iterator()
val otherIterator = otherSortedEntries.iterator()
val mergedEntries = mutableListOf<Entry>()
var carry = if (otherIterator.hasNext()) otherIterator.next() else null
while (entryIterator.hasNext()) {
val entry = entryIterator.next()
var entryOverridden = false
while (carry != null && carry.key.name <= entry.key.name) {
when {
carry.key.name < entry.key.name -> mergedEntries.add(carry)
carry.key.name == entry.key.name -> {
// The other overrides, when the keys are the same.
mergedEntries.add(carry)
entryOverridden = true
}
}
carry = if (otherIterator.hasNext()) otherIterator.next() else null
}
if (!entryOverridden) {
mergedEntries.add(entry)
}
}
if (carry != null) {
mergedEntries.add(carry)
while (otherIterator.hasNext()) {
mergedEntries.add(otherIterator.next())
}
}
entries.clear()
for (entry in mergedEntries) {
entries.add(Style.Entry(entry.key, entry.value?.clone(pool)))
}
}
override fun toString(): String {
val sb = StringBuilder(parent?.name.toString() +"\n")
for (entry in entries) {
sb.appendln(entry.key.name.toString() + " " + entry.value?.toString())
}
return sb.toString()
}
}
class Plural: Value() {
enum class Type{
ZERO,
ONE,
TWO,
FEW,
MANY,
OTHER;
companion object {
val TYPES = Type.values()
val NUM_TYPES = TYPES.size
}
}
val values = arrayOfNulls<Item?>(Type.NUM_TYPES)
fun setValue(type: Plural.Type, item: Item) {
values[type.ordinal] = item
}
override fun equals(other: Any?): Boolean {
if (other is Plural) {
return values contentEquals other.values
}
return false
}
fun clone(newPool: StringPool): Plural {
val newPlural = Plural()
newPlural.comment = comment
newPlural.source = source
for (i in 0.until(Type.NUM_TYPES)) {
newPlural.values[i] = values[i]?.clone(newPool)
}
return newPlural
}
}
class Styleable: Value() {
val entries = mutableListOf<Reference>()
override fun equals(other: Any?): Boolean {
if (other is Styleable) {
return entries == other.entries
}
return false
}
}
class Macro(
var rawValue: String? = null,
var styleString: StyleString? = null,
var untranslatables: List<UntranslatableSection> = listOf(),
var aliasNamespaces: List<Namespace> = listOf()): Value() {
class Namespace(
var alias: String? = null,
var packageName: String? = null,
var isPrivate: Boolean = false) {
override fun equals(other: Any?): Boolean {
if (other is Namespace) {
return alias == other.alias && packageName == other.packageName
&& isPrivate == isPrivate
}
return false
}
}
}
@@ -0,0 +1,10 @@
package com.android.aaptcompiler
internal object SDKConstants {
const val SDK_DONUT = 4.toShort()
const val SDK_FROYO = 8.toShort()
const val SDK_HONEYCOMB_MR2 = 13.toShort()
const val SDK_LOLLIPOP = 21.toShort()
const val SDK_MARSHMALLOW = 23.toShort()
const val SDK_O = 26.toShort()
}
@@ -0,0 +1,41 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.aaptcompiler
data class Source(val path: String, val line: Int? = null, private val archive: String? = null) {
override fun toString(): String {
var s = path
if (archive != null) {
s = "$archive@$s"
}
if (line != null) {
s += ":$line"
}
return s
}
fun isNotEmpty() = this != EMPTY
fun withLine(line: Int?) : Source {
return Source(path, line, archive)
}
companion object {
val EMPTY = Source("")
}
}
@@ -0,0 +1,431 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.aaptcompiler
import com.android.aaptcompiler.android.ChunkType
import com.android.aaptcompiler.android.ResChunkHeader
import com.android.aaptcompiler.android.ResStringPoolHeader
import com.android.aaptcompiler.android.ResStringPoolRef
import com.android.aaptcompiler.android.ResStringPoolSpan
import com.android.aaptcompiler.android.hostToDevice
import com.android.aaptcompiler.buffer.BigBuffer
import com.android.utils.ILogger
data class Span(val name: String, var firstChar: Int, var lastChar: Int = firstChar)
data class StyleString(val str : String, val spans : List<Span>)
class StringPool {
private val strings = mutableListOf<Entry>()
private val styles = mutableListOf<StyleEntry>()
private val indexedStrings = mutableMapOf<String, MutableList<Entry>>()
class Context(
var priority: Long = Priority.NORMAL.priority,
var config: ConfigDescription = ConfigDescription()) {
enum class Priority(val priority: Long) {
HIGH(1),
NORMAL(0x7fffffffL),
LOW(0xffffffffL)
}
}
class Ref (internal val entry: Entry) {
init {
++entry.ref
}
fun value(): String = entry.value
// must account for styles which come first.
fun index(): Int = entry.pool.styles.size + entry.index
fun context(): Context = entry.context
}
class Span(internal val name: Ref, var firstChar: Int, var lastChar: Int = firstChar) {
override fun equals(other: Any?): Boolean {
if (other is Span) {
return name.value() == other.name.value() &&
firstChar == other.firstChar &&
lastChar == other.lastChar
}
return false
}
}
class StyleRef(internal val styleEntry: StyleEntry) {
fun value(): String = styleEntry.value
// style strings come first
fun index(): Int = styleEntry.index
fun spans(): List<Span> = styleEntry.spans
}
class Entry(val value: String, val context: Context) {
var index: Int = 0
internal set
var ref: Int = 0
internal set
lateinit var pool: StringPool
internal set
}
class StyleEntry(val value: String, val context: Context, val spans: List<Span>) {
var index: Int = 0
internal set
var ref: Int = 0
internal set
}
fun size(): Int = styles.size + strings.size
fun entryAt(index: Int): StringPool.Entry {
return strings[index]
}
fun makeRef(str: String, context: Context = Context()) : Ref {
return makeRefImpl(str, context, true)
}
fun makeRef(ref: Ref): Ref{
if (ref.entry.pool == this) {
return ref
}
return makeRef(ref.value(), ref.context())
}
private fun makeRefImpl(str: String, context: Context, isUnique: Boolean) : Ref {
if (isUnique) {
val entries = indexedStrings[str]
if (entries != null) {
for (entry in entries) {
if (entry.context.priority == context.priority) {
return Ref(entry)
}
}
}
}
val entry = Entry(str, context)
entry.index = strings.size
entry.ref = 0
entry.pool = this
strings.add(entry)
if (!indexedStrings.contains(str)) {
indexedStrings[str] = mutableListOf()
}
indexedStrings[str]!!.add(entry)
return Ref(entry)
}
fun makeRef(styleString: StyleString, context: Context = Context()): StyleRef {
val spanList = mutableListOf<Span>()
for (span in styleString.spans) {
spanList.add(Span(makeRef(span.name), span.firstChar, span.lastChar))
}
val entry = StyleEntry(styleString.str, context, spanList)
entry.index = styles.size
entry.ref = 0
styles.add(entry)
return StyleRef(entry)
}
fun makeRef(styleRef: StyleRef): StyleRef {
val styleEntry =
StyleEntry(styleRef.styleEntry.value, styleRef.styleEntry.context, styleRef.styleEntry.spans)
styleEntry.index = styles.size
styleEntry.ref = 0
for (span in styleEntry.spans) {
makeRef(span.name)
}
styles.add(styleEntry)
return StyleRef(styleEntry)
}
fun flattenUtf8(out: BigBuffer, logger: ILogger?) = flatten(out, true, logger)
fun flattenUtf16(out: BigBuffer, logger: ILogger?) = flatten(out, false, logger)
fun sort(comparator: Comparator<Context>? = null) {
sortEntries(comparator)
sortStyles(comparator)
reAssignIndices()
}
private fun flatten(out: BigBuffer, utf8: Boolean, logger: ILogger?): Boolean {
var error = false
val header = ResStringPoolHeader(
ResChunkHeader(
ChunkType.STRING_POOL_TYPE.id.hostToDevice(),
ResStringPoolHeader.SIZE.hostToDevice()
),
size().hostToDevice(),
styles.size.hostToDevice())
if (utf8) {
header.flags = ResStringPoolHeader.UTF8_FLAG or header.flags
}
val headerStart = out.size
val headerBlock = out.nextBlock(ResStringPoolHeader.SIZE.toInt())
var currentStringIndex = 0
val stringIndexBlock =
if (size() != 0) out.nextBlock(4*size()) else null
var currentStyleIndex = 0
val stylesIndexBlock =
if (styles.size != 0) out.nextBlock(4*styles.size) else null
val beginStringsIndex = out.size
header.stringsStart = (out.size - headerStart).hostToDevice()
if (stringIndexBlock != null) {
// Styles come first
for (entry in styles) {
stringIndexBlock
.writeInt((out.size - beginStringsIndex).hostToDevice(), currentStringIndex)
currentStringIndex += 4
val stringError = !encodeString(entry.value, utf8, out, logger)
error = stringError || error
}
// Strings come next
for (entry in strings) {
stringIndexBlock.writeInt((out.size - beginStringsIndex).hostToDevice(), currentStringIndex)
currentStringIndex += 4
val stringError = !encodeString(entry.value, utf8, out, logger)
error = stringError || error
}
out.align4()
}
if (stylesIndexBlock != null) {
header.stylesStart = (out.size - headerStart).hostToDevice()
for (entry in styles) {
stylesIndexBlock.writeInt((out.size - header.stylesStart).hostToDevice(), currentStyleIndex)
currentStyleIndex +=4
if (entry.spans.isNotEmpty()) {
val spansBlock = out.nextBlock(ResStringPoolSpan.SIZE*entry.spans.size)
var currentSpanLocation = 0
for (span in entry.spans) {
val resSpan = ResStringPoolSpan(
ResStringPoolRef(
span.name.index().hostToDevice()),
span.firstChar.hostToDevice(),
span.lastChar.hostToDevice())
encodeSpan(resSpan, spansBlock, currentSpanLocation)
currentSpanLocation += ResStringPoolSpan.SIZE
}
}
val spanEnd = out.nextBlock(4)
spanEnd.writeInt(-1, 0)
}
// The error checking code in the platform looks for an entire span structure worth of
// 0xffffffff so fill in the remaining 2 32 bit words.
val padding = out.nextBlock(8)
padding.writeInt(-1, 0)
padding.writeInt(-1, 4)
out.align4()
}
// now we can set the size of the output in the header
header.header.size = (out.size - headerStart).hostToDevice()
encodeHeader(header, headerBlock)
return !error
}
private fun encodeHeader(resHeader: ResStringPoolHeader, out: BigBuffer.BlockRef) {
out.writeShort(resHeader.header.typeId, 0)
out.writeShort(resHeader.header.headerSize, 2)
out.writeInt(resHeader.header.size, 4)
out.writeInt(resHeader.stringCount, 8)
out.writeInt(resHeader.styleCount, 12)
out.writeInt(resHeader.flags, 16)
out.writeInt(resHeader.stringsStart, 20)
out.writeInt(resHeader.stylesStart, 24)
}
private fun encodeSpan(resSpan: ResStringPoolSpan, out: BigBuffer.BlockRef, location: Int) : Int {
out.writeInt(resSpan.name.index, location)
out.writeInt(resSpan.firstChar, location + 4)
out.writeInt(resSpan.lastChar, location + 8)
return 12
}
private fun encodeString(
str: String, utf8: Boolean, out: BigBuffer, logger: ILogger?): Boolean {
if (utf8) {
val utf16Length = str.length
val utf8Str = str.toByteArray()
val utf8Length = utf8Str.size
// Make sure the lengths to be encoded do not exceed the maximum length that can be encoded
// using chars
if (utf8Length > UTF8_ENCODE_LENGTH_MAX || utf16Length > UTF16_ENCODE_LENGTH_MAX) {
val errorMsg = "String too large to encode using UTF-8. Writing the string instead as '%s'."
logger?.error(null, errorMsg, STRING_TOO_LARGE)
encodeString(STRING_TOO_LARGE, utf8, out, logger)
return false
}
val blockSize = getLengthUtf8(utf8Length) + getLengthUtf8(utf16Length) + utf8Length + 1
val stringBlock = out.nextBlock(blockSize)
var locationToWrite = 0
// first encode the UTF16 length
locationToWrite += encodeLengthUtf8(utf16Length, stringBlock, locationToWrite)
// Now encode the size of the real UTF8 string
locationToWrite += encodeLengthUtf8(utf8Length, stringBlock, locationToWrite)
// Now write the string
for (i in 0.until(utf8Length)) {
stringBlock.writeByte(utf8Str[i], locationToWrite)
++locationToWrite
}
// Now write null terminator
stringBlock.writeByte(0x00, locationToWrite)
return true
} else {
val utf16Length = str.length
// Make sure the length to be encoded does not exceed the maximum possible length that can be
// encoded
if (utf16Length > UTF16_ENCODE_LENGTH_MAX) {
val errorMsg =
"String too large to encode using UTF-16. Writing the string instead as '%s'."
logger?.error(null, errorMsg, STRING_TOO_LARGE)
encodeString(STRING_TOO_LARGE, utf8, out, logger)
return false
}
val blockSize = getLengthUtf16(utf16Length) + utf16Length*2 + 2
val stringBlock = out.nextBlock(blockSize)
var locationToWrite = 0
// first encode the length
locationToWrite += encodeLengthUtf16(utf16Length, stringBlock, locationToWrite)
// Now encode the string
for (i in 0.until(utf16Length)) {
stringBlock.writeShort(str[i].toShort().hostToDevice(), locationToWrite)
locationToWrite += 2
}
// now write null terminator
stringBlock.writeShort(0x0000.toShort(), locationToWrite)
return true
}
}
private fun getLengthUtf8(length: Int) =
if (length <= ONE_BYTE_UTF8_ENCODE_LENGTH_MAX) 1 else 2
private fun encodeLengthUtf8(length: Int, out: BigBuffer.BlockRef, location: Int): Int {
return if (length <= ONE_BYTE_UTF8_ENCODE_LENGTH_MAX) {
out.writeByte(length.toByte(), location)
1
} else {
out.writeByte(((length shr 8) or TWO_BYTE_UTF8_LENGTH_SIGNIFIER).toByte(), location)
out.writeByte(length.toByte(), location + 1)
2
}
}
private fun getLengthUtf16(length: Int) =
if (length <= ONE_CHAR_UTF16_ENCODE_LENGTH_MAX) 2 else 4
private fun encodeLengthUtf16(length: Int, out: BigBuffer.BlockRef, location: Int): Int {
return if (length <= ONE_CHAR_UTF16_ENCODE_LENGTH_MAX) {
out.writeShort(length.toShort().hostToDevice(), location)
2
} else {
out.writeShort(
((length shr 16) or TWO_CHAR_UTF16_LENGTH_SIGNIFIER).toShort().hostToDevice(), location)
out.writeShort(length.toShort().hostToDevice(), location + 2)
4
}
}
private fun sortEntries(comparator: Comparator<Context>?) {
val entryComparator = if (comparator!= null) {
compareBy<Entry, Context>(comparator) {it.context}.thenComparing(ENTRY_ON_VALUE)
} else {
ENTRY_ON_VALUE
}
strings.sortWith(entryComparator)
}
private fun sortStyles(comparator: Comparator<Context>?) {
val entryComparator = if (comparator!= null) {
compareBy<StyleEntry, Context>(comparator) {it.context}.thenComparing(STYLE_ON_VALUE)
} else {
STYLE_ON_VALUE
}
styles.sortWith(entryComparator)
}
private fun reAssignIndices() {
// assign styles
for (index in 0.until(styles.size)) {
styles[index].index = index
}
// assign strings
for (index in 0.until(strings.size)) {
strings[index].index = index
}
}
companion object {
const val UTF8_ENCODE_LENGTH_MAX = 0x7fff
const val TWO_BYTE_UTF8_LENGTH_SIGNIFIER = 0x80
const val ONE_BYTE_UTF8_ENCODE_LENGTH_MAX = 0x7f
const val UTF16_ENCODE_LENGTH_MAX = 0x7fffffff
const val TWO_CHAR_UTF16_LENGTH_SIGNIFIER = 0x8000
const val ONE_CHAR_UTF16_ENCODE_LENGTH_MAX = 0x7fff
private const val STRING_TOO_LARGE = "STRING_TOO_LARGE"
val ENTRY_ON_VALUE = compareBy<Entry> {it.value}
val STYLE_ON_VALUE = compareBy<StyleEntry> {it.value}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,21 @@
package com.android.aaptcompiler
object ToolFingerprint {
const val TOOL_NAME = "Android Asset Packaging Tool (aapt)"
// DO NOT UPDATE, this is more of a marketing version
const val MAJOR_VERSION = 2
/*
* Represents the current version of Aapt2 that is being followed.
*
* Update minor version when features or flags are added to reflect the C++ value in
* //frameworks/base/tools/aapt2/util/Util.cpp under the function GetToolFingerprint
*/
const val MINOR_VERSION = 19
// TODO(@daniellabar): Find the build id of aapt2 Kotlin
val BUILD_NUMBER = "0"
val FINGERPRINT = "$MAJOR_VERSION.$MINOR_VERSION-$BUILD_NUMBER"
}
@@ -0,0 +1,714 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* transliterated from
* https://android.googlesource.com/platform/frameworks/base/+/master/tools/aapt2/text/Unicode.cpp
* and
* https://android.googlesource.com/platform/frameworks/base/+/master/tools/aapt2/text/Unicode_data.cpp
*/
package com.android.aaptcompiler
const val XID_START_FLAG = 1 shl 0
const val XID_CONTINUE_FLAG = 1 shl 1
const val XID_EITHER_FLAG = XID_START_FLAG or XID_CONTINUE_FLAG
private fun findCharacterProperties(utf32: Int): Int =
when (utf32) {
in 0x30..0x39 -> XID_CONTINUE_FLAG
in 0x41..0x5a -> XID_EITHER_FLAG
0x5f -> XID_CONTINUE_FLAG
in 0x61..0x7a,
0xaa,
0xb5 -> XID_EITHER_FLAG
0xb7 -> XID_CONTINUE_FLAG
0xba,
in 0xc0..0xd6,
in 0xd8..0xf6,
in 0xf8..0x2c1,
in 0x2c6..0x2d1,
in 0x2e0..0x2e4,
0x2ec,
0x2ee -> XID_EITHER_FLAG
in 0x300..0x36f -> XID_CONTINUE_FLAG
in 0x370..0x374,
in 0x376..0x377,
in 0x37b..0x37d,
0x37f,
0x386 -> XID_EITHER_FLAG
0x387 -> XID_CONTINUE_FLAG
in 0x388..0x38a,
0x38c,
in 0x38e..0x3a1,
in 0x3a3..0x3f5,
in 0x3f7..0x481 -> XID_EITHER_FLAG
in 0x483..0x487 -> XID_CONTINUE_FLAG
in 0x48a..0x52f,
in 0x531..0x556,
0x559,
in 0x561..0x587 -> XID_EITHER_FLAG
in 0x591..0x5bd,
0x5bf,
in 0x5c1..0x5c2,
in 0x5c4..0x5c5,
0x5c7 -> XID_CONTINUE_FLAG
in 0x5d0..0x5ea,
in 0x5f0..0x5f2 -> XID_EITHER_FLAG
in 0x610..0x61a -> XID_CONTINUE_FLAG
in 0x620..0x64a -> XID_EITHER_FLAG
in 0x64b..0x669 -> XID_CONTINUE_FLAG
in 0x66e..0x66f -> XID_EITHER_FLAG
0x670 -> XID_CONTINUE_FLAG
in 0x671..0x6d3,
0x6d5 -> XID_EITHER_FLAG
in 0x6d6..0x6dc,
in 0x6df..0x6e4 -> XID_CONTINUE_FLAG
in 0x6e5..0x6e6 -> XID_EITHER_FLAG
in 0x6e7..0x6e8,
in 0x6ea..0x6ed -> XID_CONTINUE_FLAG
in 0x6ee..0x6ef -> XID_EITHER_FLAG
in 0x6f0..0x6f9 -> XID_CONTINUE_FLAG
in 0x6fa..0x6fc,
0x6ff,
0x710 -> XID_EITHER_FLAG
0x711 -> XID_CONTINUE_FLAG
in 0x712..0x72f -> XID_EITHER_FLAG
in 0x730..0x74a -> XID_CONTINUE_FLAG
in 0x74d..0x7a5 -> XID_EITHER_FLAG
in 0x7a6..0x7b0 -> XID_CONTINUE_FLAG
0x7b1 -> XID_EITHER_FLAG
in 0x7c0..0x7c9 -> XID_CONTINUE_FLAG
in 0x7ca..0x7ea -> XID_EITHER_FLAG
in 0x7eb..0x7f3 -> XID_CONTINUE_FLAG
in 0x7f4..0x7f5,
0x7fa,
in 0x800..0x815 -> XID_EITHER_FLAG
in 0x816..0x819 -> XID_CONTINUE_FLAG
0x81a -> XID_EITHER_FLAG
in 0x81b..0x823 -> XID_CONTINUE_FLAG
0x824 -> XID_EITHER_FLAG
in 0x825..0x827 -> XID_CONTINUE_FLAG
0x828 -> XID_EITHER_FLAG
in 0x829..0x82d -> XID_CONTINUE_FLAG
in 0x840..0x858 -> XID_EITHER_FLAG
in 0x859..0x85b -> XID_CONTINUE_FLAG
in 0x8a0..0x8b4,
in 0x8b6..0x8bd -> XID_EITHER_FLAG
in 0x8d4..0x8e1,
in 0x8e3..0x903 -> XID_CONTINUE_FLAG
in 0x904..0x939 -> XID_EITHER_FLAG
in 0x93a..0x93c -> XID_CONTINUE_FLAG
0x93d -> XID_EITHER_FLAG
in 0x93e..0x94f -> XID_CONTINUE_FLAG
0x950 -> XID_EITHER_FLAG
in 0x951..0x957 -> XID_CONTINUE_FLAG
in 0x958..0x961 -> XID_EITHER_FLAG
in 0x962..0x963,
in 0x966..0x96f -> XID_CONTINUE_FLAG
in 0x971..0x980 -> XID_EITHER_FLAG
in 0x981..0x983 -> XID_CONTINUE_FLAG
in 0x985..0x98c,
in 0x98f..0x990,
in 0x993..0x9a8,
in 0x9aa..0x9b0,
0x9b2,
in 0x9b6..0x9b9 -> XID_EITHER_FLAG
0x9bc -> XID_CONTINUE_FLAG
0x9bd -> XID_EITHER_FLAG
in 0x9be..0x9c4,
in 0x9c7..0x9c8,
in 0x9cb..0x9cd -> XID_CONTINUE_FLAG
0x9ce -> XID_EITHER_FLAG
0x9d7 -> XID_CONTINUE_FLAG
in 0x9dc..0x9dd,
in 0x9df..0x9e1 -> XID_EITHER_FLAG
in 0x9e2..0x9e3,
in 0x9e6..0x9ef -> XID_CONTINUE_FLAG
in 0x9f0..0x9f1 -> XID_EITHER_FLAG
in 0xa01..0xa03 -> XID_CONTINUE_FLAG
in 0xa05..0xa0a,
in 0xa0f..0xa10,
in 0xa13..0xa28,
in 0xa2a..0xa30,
in 0xa32..0xa33,
in 0xa35..0xa36,
in 0xa38..0xa39 -> XID_EITHER_FLAG
0xa3c,
in 0xa3e..0xa42,
in 0xa47..0xa48,
in 0xa4b..0xa4d,
0xa51 -> XID_CONTINUE_FLAG
in 0xa59..0xa5c,
0xa5e -> XID_EITHER_FLAG
in 0xa66..0xa71 -> XID_CONTINUE_FLAG
in 0xa72..0xa74 -> XID_EITHER_FLAG
0xa75,
in 0xa81..0xa83 -> XID_CONTINUE_FLAG
in 0xa85..0xa8d,
in 0xa8f..0xa91,
in 0xa93..0xaa8,
in 0xaaa..0xab0,
in 0xab2..0xab3,
in 0xab5..0xab9 -> XID_EITHER_FLAG
0xabc -> XID_CONTINUE_FLAG
0xabd -> XID_EITHER_FLAG
in 0xabe..0xac5,
in 0xac7..0xac9,
in 0xacb..0xacd -> XID_CONTINUE_FLAG
0xad0,
in 0xae0..0xae1 -> XID_EITHER_FLAG
in 0xae2..0xae3,
in 0xae6..0xaef -> XID_CONTINUE_FLAG
0xaf9 -> XID_EITHER_FLAG
in 0xb01..0xb03 -> XID_CONTINUE_FLAG
in 0xb05..0xb0c,
in 0xb0f..0xb10,
in 0xb13..0xb28,
in 0xb2a..0xb30,
in 0xb32..0xb33,
in 0xb35..0xb39 -> XID_EITHER_FLAG
0xb3c -> XID_CONTINUE_FLAG
0xb3d -> XID_EITHER_FLAG
in 0xb3e..0xb44,
in 0xb47..0xb48,
in 0xb4b..0xb4d,
in 0xb56..0xb57 -> XID_CONTINUE_FLAG
in 0xb5c..0xb5d,
in 0xb5f..0xb61 -> XID_EITHER_FLAG
in 0xb62..0xb63,
in 0xb66..0xb6f -> XID_CONTINUE_FLAG
0xb71 -> XID_EITHER_FLAG
0xb82 -> XID_CONTINUE_FLAG
0xb83,
in 0xb85..0xb8a,
in 0xb8e..0xb90,
in 0xb92..0xb95,
in 0xb99..0xb9a,
0xb9c,
in 0xb9e..0xb9f,
in 0xba3..0xba4,
in 0xba8..0xbaa,
in 0xbae..0xbb9 -> XID_EITHER_FLAG
in 0xbbe..0xbc2,
in 0xbc6..0xbc8,
in 0xbca..0xbcd -> XID_CONTINUE_FLAG
0xbd0 -> XID_EITHER_FLAG
0xbd7,
in 0xbe6..0xbef,
in 0xc00..0xc03 -> XID_CONTINUE_FLAG
in 0xc05..0xc0c,
in 0xc0e..0xc10,
in 0xc12..0xc28,
in 0xc2a..0xc39,
0xc3d -> XID_EITHER_FLAG
in 0xc3e..0xc44,
in 0xc46..0xc48,
in 0xc4a..0xc4d,
in 0xc55..0xc56 -> XID_CONTINUE_FLAG
in 0xc58..0xc5a,
in 0xc60..0xc61 -> XID_EITHER_FLAG
in 0xc62..0xc63,
in 0xc66..0xc6f -> XID_CONTINUE_FLAG
0xc80 -> XID_EITHER_FLAG
in 0xc81..0xc83 -> XID_CONTINUE_FLAG
in 0xc85..0xc8c,
in 0xc8e..0xc90,
in 0xc92..0xca8,
in 0xcaa..0xcb3,
in 0xcb5..0xcb9 -> XID_EITHER_FLAG
0xcbc -> XID_CONTINUE_FLAG
0xcbd -> XID_EITHER_FLAG
in 0xcbe..0xcc4,
in 0xcc6..0xcc8,
in 0xcca..0xccd,
in 0xcd5..0xcd6 -> XID_CONTINUE_FLAG
0xcde,
in 0xce0..0xce1 -> XID_EITHER_FLAG
in 0xce2..0xce3,
in 0xce6..0xcef -> XID_CONTINUE_FLAG
in 0xcf1..0xcf2 -> XID_EITHER_FLAG
in 0xd01..0xd03 -> XID_CONTINUE_FLAG
in 0xd05..0xd0c,
in 0xd0e..0xd10,
in 0xd12..0xd3a,
0xd3d -> XID_EITHER_FLAG
in 0xd3e..0xd44,
in 0xd46..0xd48,
in 0xd4a..0xd4d -> XID_CONTINUE_FLAG
0xd4e,
in 0xd54..0xd56 -> XID_EITHER_FLAG
0xd57 -> XID_CONTINUE_FLAG
in 0xd5f..0xd61 -> XID_EITHER_FLAG
in 0xd62..0xd63,
in 0xd66..0xd6f -> XID_CONTINUE_FLAG
in 0xd7a..0xd7f -> XID_EITHER_FLAG
in 0xd82..0xd83 -> XID_CONTINUE_FLAG
in 0xd85..0xd96,
in 0xd9a..0xdb1,
in 0xdb3..0xdbb,
0xdbd,
in 0xdc0..0xdc6 -> XID_EITHER_FLAG
0xdca,
in 0xdcf..0xdd4,
0xdd6,
in 0xdd8..0xddf,
in 0xde6..0xdef,
in 0xdf2..0xdf3 -> XID_CONTINUE_FLAG
in 0xe01..0xe30 -> XID_EITHER_FLAG
0xe31 -> XID_CONTINUE_FLAG
0xe32 -> XID_EITHER_FLAG
in 0xe33..0xe3a -> XID_CONTINUE_FLAG
in 0xe40..0xe46 -> XID_EITHER_FLAG
in 0xe47..0xe4e,
in 0xe50..0xe59 -> XID_CONTINUE_FLAG
in 0xe81..0xe82,
0xe84,
in 0xe87..0xe88,
0xe8a,
0xe8d,
in 0xe94..0xe97,
in 0xe99..0xe9f,
in 0xea1..0xea3,
0xea5,
0xea7,
in 0xeaa..0xeab,
in 0xead..0xeb0 -> XID_EITHER_FLAG
0xeb1 -> XID_CONTINUE_FLAG
0xeb2 -> XID_EITHER_FLAG
in 0xeb3..0xeb9,
in 0xebb..0xebc -> XID_CONTINUE_FLAG
0xebd,
in 0xec0..0xec4,
0xec6 -> XID_EITHER_FLAG
in 0xec8..0xecd,
in 0xed0..0xed9 -> XID_CONTINUE_FLAG
in 0xedc..0xedf,
0xf00 -> XID_EITHER_FLAG
in 0xf18..0xf19,
in 0xf20..0xf29,
0xf35,
0xf37,
0xf39,
in 0xf3e..0xf3f -> XID_CONTINUE_FLAG
in 0xf40..0xf47,
in 0xf49..0xf6c -> XID_EITHER_FLAG
in 0xf71..0xf84,
in 0xf86..0xf87 -> XID_CONTINUE_FLAG
in 0xf88..0xf8c -> XID_EITHER_FLAG
in 0xf8d..0xf97,
in 0xf99..0xfbc,
0xfc6 -> XID_CONTINUE_FLAG
in 0x1000..0x102a -> XID_EITHER_FLAG
in 0x102b..0x103e -> XID_CONTINUE_FLAG
0x103f -> XID_EITHER_FLAG
in 0x1040..0x1049 -> XID_CONTINUE_FLAG
in 0x1050..0x1055 -> XID_EITHER_FLAG
in 0x1056..0x1059 -> XID_CONTINUE_FLAG
in 0x105a..0x105d -> XID_EITHER_FLAG
in 0x105e..0x1060 -> XID_CONTINUE_FLAG
0x1061 -> XID_EITHER_FLAG
in 0x1062..0x1064 -> XID_CONTINUE_FLAG
in 0x1065..0x1066 -> XID_EITHER_FLAG
in 0x1067..0x106d -> XID_CONTINUE_FLAG
in 0x106e..0x1070 -> XID_EITHER_FLAG
in 0x1071..0x1074 -> XID_CONTINUE_FLAG
in 0x1075..0x1081 -> XID_EITHER_FLAG
in 0x1082..0x108d -> XID_CONTINUE_FLAG
0x108e -> XID_EITHER_FLAG
in 0x108f..0x109d -> XID_CONTINUE_FLAG
in 0x10a0..0x10c5,
0x10c7,
0x10cd,
in 0x10d0..0x10fa,
in 0x10fc..0x1248,
in 0x124a..0x124d,
in 0x1250..0x1256,
0x1258,
in 0x125a..0x125d,
in 0x1260..0x1288,
in 0x128a..0x128d,
in 0x1290..0x12b0,
in 0x12b2..0x12b5,
in 0x12b8..0x12be,
0x12c0,
in 0x12c2..0x12c5,
in 0x12c8..0x12d6,
in 0x12d8..0x1310,
in 0x1312..0x1315,
in 0x1318..0x135a -> XID_EITHER_FLAG
in 0x135d..0x135f,
in 0x1369..0x1371 -> XID_CONTINUE_FLAG
in 0x1380..0x138f,
in 0x13a0..0x13f5,
in 0x13f8..0x13fd,
in 0x1401..0x166c,
in 0x166f..0x167f,
in 0x1681..0x169a,
in 0x16a0..0x16ea,
in 0x16ee..0x16f8,
in 0x1700..0x170c,
in 0x170e..0x1711 -> XID_EITHER_FLAG
in 0x1712..0x1714 -> XID_CONTINUE_FLAG
in 0x1720..0x1731 -> XID_EITHER_FLAG
in 0x1732..0x1734 -> XID_CONTINUE_FLAG
in 0x1740..0x1751 -> XID_EITHER_FLAG
in 0x1752..0x1753 -> XID_CONTINUE_FLAG
in 0x1760..0x176c,
in 0x176e..0x1770 -> XID_EITHER_FLAG
in 0x1772..0x1773 -> XID_CONTINUE_FLAG
in 0x1780..0x17b3 -> XID_EITHER_FLAG
in 0x17b4..0x17d3 -> XID_CONTINUE_FLAG
0x17d7,
0x17dc -> XID_EITHER_FLAG
0x17dd,
in 0x17e0..0x17e9,
in 0x180b..0x180d,
in 0x1810..0x1819 -> XID_CONTINUE_FLAG
in 0x1820..0x1877,
in 0x1880..0x18a8 -> XID_EITHER_FLAG
0x18a9 -> XID_CONTINUE_FLAG
0x18aa,
in 0x18b0..0x18f5,
in 0x1900..0x191e -> XID_EITHER_FLAG
in 0x1920..0x192b,
in 0x1930..0x193b,
in 0x1946..0x194f -> XID_CONTINUE_FLAG
in 0x1950..0x196d,
in 0x1970..0x1974,
in 0x1980..0x19ab,
in 0x19b0..0x19c9 -> XID_EITHER_FLAG
in 0x19d0..0x19da -> XID_CONTINUE_FLAG
in 0x1a00..0x1a16 -> XID_EITHER_FLAG
in 0x1a17..0x1a1b -> XID_CONTINUE_FLAG
in 0x1a20..0x1a54 -> XID_EITHER_FLAG
in 0x1a55..0x1a5e,
in 0x1a60..0x1a7c,
in 0x1a7f..0x1a89,
in 0x1a90..0x1a99 -> XID_CONTINUE_FLAG
0x1aa7 -> XID_EITHER_FLAG
in 0x1ab0..0x1abd,
in 0x1b00..0x1b04 -> XID_CONTINUE_FLAG
in 0x1b05..0x1b33 -> XID_EITHER_FLAG
in 0x1b34..0x1b44 -> XID_CONTINUE_FLAG
in 0x1b45..0x1b4b -> XID_EITHER_FLAG
in 0x1b50..0x1b59,
in 0x1b6b..0x1b73,
in 0x1b80..0x1b82 -> XID_CONTINUE_FLAG
in 0x1b83..0x1ba0 -> XID_EITHER_FLAG
in 0x1ba1..0x1bad -> XID_CONTINUE_FLAG
in 0x1bae..0x1baf -> XID_EITHER_FLAG
in 0x1bb0..0x1bb9 -> XID_CONTINUE_FLAG
in 0x1bba..0x1be5 -> XID_EITHER_FLAG
in 0x1be6..0x1bf3 -> XID_CONTINUE_FLAG
in 0x1c00..0x1c23 -> XID_EITHER_FLAG
in 0x1c24..0x1c37,
in 0x1c40..0x1c49 -> XID_CONTINUE_FLAG
in 0x1c4d..0x1c4f -> XID_EITHER_FLAG
in 0x1c50..0x1c59 -> XID_CONTINUE_FLAG
in 0x1c5a..0x1c7d,
in 0x1c80..0x1c88 -> XID_EITHER_FLAG
in 0x1cd0..0x1cd2,
in 0x1cd4..0x1ce8 -> XID_CONTINUE_FLAG
in 0x1ce9..0x1cec -> XID_EITHER_FLAG
0x1ced -> XID_CONTINUE_FLAG
in 0x1cee..0x1cf1 -> XID_EITHER_FLAG
in 0x1cf2..0x1cf4 -> XID_CONTINUE_FLAG
in 0x1cf5..0x1cf6 -> XID_EITHER_FLAG
in 0x1cf8..0x1cf9 -> XID_CONTINUE_FLAG
in 0x1d00..0x1dbf -> XID_EITHER_FLAG
in 0x1dc0..0x1df5,
in 0x1dfb..0x1dff -> XID_CONTINUE_FLAG
in 0x1e00..0x1f15,
in 0x1f18..0x1f1d,
in 0x1f20..0x1f45,
in 0x1f48..0x1f4d,
in 0x1f50..0x1f57,
0x1f59,
0x1f5b,
0x1f5d,
in 0x1f5f..0x1f7d,
in 0x1f80..0x1fb4,
in 0x1fb6..0x1fbc,
0x1fbe,
in 0x1fc2..0x1fc4,
in 0x1fc6..0x1fcc,
in 0x1fd0..0x1fd3,
in 0x1fd6..0x1fdb,
in 0x1fe0..0x1fec,
in 0x1ff2..0x1ff4,
in 0x1ff6..0x1ffc -> XID_EITHER_FLAG
in 0x203f..0x2040,
0x2054 -> XID_CONTINUE_FLAG
0x2071,
0x207f,
in 0x2090..0x209c -> XID_EITHER_FLAG
in 0x20d0..0x20dc,
0x20e1,
in 0x20e5..0x20f0 -> XID_CONTINUE_FLAG
0x2102,
0x2107,
in 0x210a..0x2113,
0x2115,
in 0x2118..0x211d,
0x2124,
0x2126,
0x2128,
in 0x212a..0x2139,
in 0x213c..0x213f,
in 0x2145..0x2149,
0x214e,
in 0x2160..0x2188,
in 0x2c00..0x2c2e,
in 0x2c30..0x2c5e,
in 0x2c60..0x2ce4,
in 0x2ceb..0x2cee -> XID_EITHER_FLAG
in 0x2cef..0x2cf1 -> XID_CONTINUE_FLAG
in 0x2cf2..0x2cf3,
in 0x2d00..0x2d25,
0x2d27,
0x2d2d,
in 0x2d30..0x2d67,
0x2d6f -> XID_EITHER_FLAG
0x2d7f -> XID_CONTINUE_FLAG
in 0x2d80..0x2d96,
in 0x2da0..0x2da6,
in 0x2da8..0x2dae,
in 0x2db0..0x2db6,
in 0x2db8..0x2dbe,
in 0x2dc0..0x2dc6,
in 0x2dc8..0x2dce,
in 0x2dd0..0x2dd6,
in 0x2dd8..0x2dde -> XID_EITHER_FLAG
in 0x2de0..0x2dff -> XID_CONTINUE_FLAG
in 0x3005..0x3007,
in 0x3021..0x3029 -> XID_EITHER_FLAG
in 0x302a..0x302f -> XID_CONTINUE_FLAG
in 0x3031..0x3035,
in 0x3038..0x303c,
in 0x3041..0x3096 -> XID_EITHER_FLAG
in 0x3099..0x309a -> XID_CONTINUE_FLAG
in 0x309d..0x309f,
in 0x30a1..0x30fa,
in 0x30fc..0x30ff,
in 0x3105..0x312d,
in 0x3131..0x318e,
in 0x31a0..0x31ba,
in 0x31f0..0x31ff,
in 0x3400..0x4db5,
in 0x4e00..0x9fd5,
in 0xa000..0xa48c,
in 0xa4d0..0xa4fd,
in 0xa500..0xa60c,
in 0xa610..0xa61f -> XID_EITHER_FLAG
in 0xa620..0xa629 -> XID_CONTINUE_FLAG
in 0xa62a..0xa62b,
in 0xa640..0xa66e -> XID_EITHER_FLAG
0xa66f,
in 0xa674..0xa67d -> XID_CONTINUE_FLAG
in 0xa67f..0xa69d -> XID_EITHER_FLAG
in 0xa69e..0xa69f -> XID_CONTINUE_FLAG
in 0xa6a0..0xa6ef -> XID_EITHER_FLAG
in 0xa6f0..0xa6f1 -> XID_CONTINUE_FLAG
in 0xa717..0xa71f,
in 0xa722..0xa788,
in 0xa78b..0xa7ae,
in 0xa7b0..0xa7b7,
in 0xa7f7..0xa801 -> XID_EITHER_FLAG
0xa802 -> XID_CONTINUE_FLAG
in 0xa803..0xa805 -> XID_EITHER_FLAG
0xa806 -> XID_CONTINUE_FLAG
in 0xa807..0xa80a -> XID_EITHER_FLAG
0xa80b -> XID_CONTINUE_FLAG
in 0xa80c..0xa822 -> XID_EITHER_FLAG
in 0xa823..0xa827 -> XID_CONTINUE_FLAG
in 0xa840..0xa873 -> XID_EITHER_FLAG
in 0xa880..0xa881 -> XID_CONTINUE_FLAG
in 0xa882..0xa8b3 -> XID_EITHER_FLAG
in 0xa8b4..0xa8c5,
in 0xa8d0..0xa8d9,
in 0xa8e0..0xa8f1 -> XID_CONTINUE_FLAG
in 0xa8f2..0xa8f7,
0xa8fb,
0xa8fd -> XID_EITHER_FLAG
in 0xa900..0xa909 -> XID_CONTINUE_FLAG
in 0xa90a..0xa925 -> XID_EITHER_FLAG
in 0xa926..0xa92d -> XID_CONTINUE_FLAG
in 0xa930..0xa946 -> XID_EITHER_FLAG
in 0xa947..0xa953 -> XID_CONTINUE_FLAG
in 0xa960..0xa97c -> XID_EITHER_FLAG
in 0xa980..0xa983 -> XID_CONTINUE_FLAG
in 0xa984..0xa9b2 -> XID_EITHER_FLAG
in 0xa9b3..0xa9c0 -> XID_CONTINUE_FLAG
0xa9cf -> XID_EITHER_FLAG
in 0xa9d0..0xa9d9 -> XID_CONTINUE_FLAG
in 0xa9e0..0xa9e4 -> XID_EITHER_FLAG
0xa9e5 -> XID_CONTINUE_FLAG
in 0xa9e6..0xa9ef -> XID_EITHER_FLAG
in 0xa9f0..0xa9f9 -> XID_CONTINUE_FLAG
in 0xa9fa..0xa9fe,
in 0xaa00..0xaa28 -> XID_EITHER_FLAG
in 0xaa29..0xaa36 -> XID_CONTINUE_FLAG
in 0xaa40..0xaa42 -> XID_EITHER_FLAG
0xaa43 -> XID_CONTINUE_FLAG
in 0xaa44..0xaa4b -> XID_EITHER_FLAG
in 0xaa4c..0xaa4d,
in 0xaa50..0xaa59 -> XID_CONTINUE_FLAG
in 0xaa60..0xaa76,
0xaa7a -> XID_EITHER_FLAG
in 0xaa7b..0xaa7d -> XID_CONTINUE_FLAG
in 0xaa7e..0xaaaf -> XID_EITHER_FLAG
0xaab0 -> XID_CONTINUE_FLAG
0xaab1 -> XID_EITHER_FLAG
in 0xaab2..0xaab4 -> XID_CONTINUE_FLAG
in 0xaab5..0xaab6 -> XID_EITHER_FLAG
in 0xaab7..0xaab8 -> XID_CONTINUE_FLAG
in 0xaab9..0xaabd -> XID_EITHER_FLAG
in 0xaabe..0xaabf -> XID_CONTINUE_FLAG
0xaac0 -> XID_EITHER_FLAG
0xaac1 -> XID_CONTINUE_FLAG
0xaac2,
in 0xaadb..0xaadd,
in 0xaae0..0xaaea -> XID_EITHER_FLAG
in 0xaaeb..0xaaef -> XID_CONTINUE_FLAG
in 0xaaf2..0xaaf4 -> XID_EITHER_FLAG
in 0xaaf5..0xaaf6 -> XID_CONTINUE_FLAG
in 0xab01..0xab06,
in 0xab09..0xab0e,
in 0xab11..0xab16,
in 0xab20..0xab26,
in 0xab28..0xab2e,
in 0xab30..0xab5a,
in 0xab5c..0xab65,
in 0xab70..0xabe2 -> XID_EITHER_FLAG
in 0xabe3..0xabea,
in 0xabec..0xabed,
in 0xabf0..0xabf9 -> XID_CONTINUE_FLAG
in 0xac00..0xd7a3,
in 0xd7b0..0xd7c6,
in 0xd7cb..0xd7fb,
in 0xf900..0xfa6d,
in 0xfa70..0xfad9,
in 0xfb00..0xfb06,
in 0xfb13..0xfb17,
0xfb1d -> XID_EITHER_FLAG
0xfb1e -> XID_CONTINUE_FLAG
in 0xfb1f..0xfb28,
in 0xfb2a..0xfb36,
in 0xfb38..0xfb3c,
0xfb3e,
in 0xfb40..0xfb41,
in 0xfb43..0xfb44,
in 0xfb46..0xfbb1,
in 0xfbd3..0xfc5d,
in 0xfc64..0xfd3d,
in 0xfd50..0xfd8f,
in 0xfd92..0xfdc7,
in 0xfdf0..0xfdf9 -> XID_EITHER_FLAG
in 0xfe00..0xfe0f,
in 0xfe20..0xfe2f,
in 0xfe33..0xfe34,
in 0xfe4d..0xfe4f -> XID_CONTINUE_FLAG
0xfe71,
0xfe73,
0xfe77,
0xfe79,
0xfe7b,
0xfe7d,
in 0xfe7f..0xfefc -> XID_EITHER_FLAG
in 0xff10..0xff19 -> XID_CONTINUE_FLAG
in 0xff21..0xff3a -> XID_EITHER_FLAG
0xff3f -> XID_CONTINUE_FLAG
in 0xff41..0xff5a,
in 0xff66..0xff9d -> XID_EITHER_FLAG
in 0xff9e..0xff9f -> XID_CONTINUE_FLAG
in 0xffa0..0xffbe,
in 0xffc2..0xffc7,
in 0xffca..0xffcf,
in 0xffd2..0xffd7,
in 0xffda..0xffdc -> XID_EITHER_FLAG
else -> 0
}
/**
* Returns true if the Unicode codepoint has the XID_Start property, meaning it can be used as the
* first character of a programming language identifier.
* http://unicode.org/reports/tr31/#Default_Identifier_Syntax
*
* XID_Start is a Unicode Derived Core Property. It is a variation of the ID_Start
* Derived Core Property, accounting for a few characters that, when normalized, yield valid
* characters in the ID_Start set.
*/
fun isXidStart(utf32: Int): Boolean = (findCharacterProperties(utf32) and XID_START_FLAG) != 0
/**
* Returns true if the Unicode codepoint has the XID_Continue property, meaning it can be used in
* any position of a programming language identifier, except the first.
* http://unicode.org/reports/tr31/#Default_Identifier_Syntax
*
* XID_Continue is a Unicode Derived Core Property. It is a variation of the ID_Continue
* Derived Core Property, accounting for a few characters that, when normalized, yield valid
* characters in the ID_Continue set.
*/
fun isXidContinue(utf32: Int): Boolean = (findCharacterProperties(utf32) and XID_CONTINUE_FLAG) != 0
/**
* Returns true if the UTF8 string can be used as a Java identifier.
* NOTE: This does not check against the set of reserved Java keywords.
*/
fun isJavaIdentifier(string: String): Boolean {
if (string.isEmpty()) {
return false
}
val firstCodePoint = string.codePointAt(0)
if (!isXidStart(firstCodePoint) &&
firstCodePoint != '_'.toInt() &&
firstCodePoint != '$'.toInt()) {
return false
}
for (i in 1.until(string.codePointCount(0, string.length))) {
val codePoint = string.codePointAt(i)
if (!isXidContinue(codePoint) && codePoint != '$'.toInt()) {
return false
}
}
return true
}
/**
* Returns true if the UTF8 string can be used as the entry name of a resource name.
* This is the `entry` part of package:type/entry.
*/
fun isValidResourceEntryName(string: String): Boolean {
if (string.isEmpty()) {
return false
}
// Resources can start with '_'.
val firstCodePoint = string.codePointAt(0)
if (!isXidStart(firstCodePoint) && firstCodePoint != '_'.toInt()) {
return false
}
for (i in 1.until(string.codePointCount(0, string.length))) {
val codePoint = string.codePointAt(i)
if (!isXidContinue(codePoint) && codePoint != '.'.toInt() && codePoint != '-'.toInt()) {
return false
}
}
return true
}
@@ -0,0 +1,535 @@
package com.android.aaptcompiler
import java.io.InputStream
import java.lang.RuntimeException
import javax.xml.namespace.QName
import javax.xml.stream.XMLEventReader
import javax.xml.stream.XMLStreamConstants
import javax.xml.stream.XMLStreamException
import javax.xml.stream.events.Attribute
import javax.xml.stream.events.Characters
import javax.xml.stream.events.EndElement
import javax.xml.stream.events.Namespace
import javax.xml.stream.events.StartElement
import javax.xml.stream.events.XMLEvent
private const val AAPT_ATTR_TAG = "attr"
/** Mangles outlined resources so they are only accessible from this file */
private fun mangleEntry(pck: String, entry: String) = "$pck${'$'}$entry"
/**
* Processes a XML file as a android resource.
*
* This consists of three primary processes:
* 1. Collect created ID resources.
* Gather all ids specified in xml attributes with the create marker. i.e.
*
* <TextView
* android:id="@+id/player_name"
* android:text="@+string/player_name"/>
* In this XML, the resource id/player_name would be exported by this xml file, since it has the "+"
* resource creation marker. Even though string/player_name is marked with the creation marker, it
* is not an id resource and is ignored.
*
* 2. Outline inlined XML resources.
* In android XML files (drawables, layouts, etc.) it is possible to write an XML resource inside of
* another XML resource.
*
* Consider drawable/player_background:
* <shape
* xmlns:android="http://schemas.android.com/apk/res/android"
* android:shape="rectangle">
* <gradient
* android:startColor="#FF000000"
* android:endColor="#FFAA0000"
* android:angle="35" />
* <corners
* android:radius="10dp" />
* </shape>
* And consider:
* <TextView
* android:id="@+id/player_name"
* android:text="@string/player_name"
* android:background=@"drawable/player_background"/>
*
* If this drawable is only used here we can simplify this to one file:
* <TextView
* android:id="@+id/player_name"
* android:text="@string/player_name">
* <aapt:attr name="android:background">
* <shape
* xmlns:android="http://schemas.android.com/apk/res/android"
* android:shape="rectangle">
* <gradient
* android:startColor="#FF000000"
* android:endColor="#FFAA0000"
* android:angle="35" />
* <corners
* android:radius="10dp" />
* </shape>
* </aapt:attr>
* </TextView>
*
* This is possible by outlining the aapt_attr element into a separate XML file. Effectively undoing
* the inlining by the developer. So this class outlines the inline XML and sets the parent elements
* attribute to reference the outlned attr.
*
* 3. Flatten XMLs to proto.
* Flatten all XML files (including outlined XMLs in (2.)) to proto to be written as output.
*
* The XMLProcessor handles all of these processes simultaneously to require only a single pass over
* the XML file.
*
* @property source: The source that this processor is going to process.
*/
class XmlProcessor(val source: Source, val logger: BlameLogger?) {
lateinit var primaryFile: ResourceFile
private set
var xmlResources = listOf<XmlResource>()
private set
/**
* Processes the XML resource.
*
* Outlines all inline XML aapt:attr resources, collects all created id resources, and flattens
* the resulting XMLs to proto.
*/
fun process(file: ResourceFile, inputFile: InputStream) {
primaryFile = file
var eventReader: XMLEventReader? = null
try {
eventReader = xmlInputFactory.createXMLEventReader(inputFile)
val collectedIds = mutableMapOf<ResourceName, SourcedResourceName>()
val protoBuilders = mutableMapOf<ResourceFile, XmlResourceBuilder>()
val documentStart = eventReader.nextEvent()
if (!documentStart.isStartDocument) {
error("Failed to find start of XML from ${blameSource(source)}")
}
var rootStart: XMLEvent? = null
while (eventReader.hasNext()) {
rootStart = eventReader.nextEvent()
// ignore comments and text before the root tag
if (rootStart.isStartElement) {
break
}
}
rootStart ?: return
val builder = XmlResourceBuilder(primaryFile)
protoBuilders[file] = builder
processElement(
rootStart.asStartElement(),
eventReader,
collectedIds,
protoBuilders,
builder
)
primaryFile.exportedSymbols.addAll(collectedIds.values.toList().sortedBy { it.name })
xmlResources = protoBuilders.values.map { it.build() }
.sortedWith { left, right ->
when {
// The primary file should come first in the result.
left === right -> 0
left.file.name === primaryFile.name -> -1
right.file.name === primaryFile.name -> 1
else -> left.file.name.compareTo(right.file.name)
}
}
} catch (xmlException: XMLStreamException) {
val message = xmlException.message ?: ""
if (!message.contains("Premature end of file", true)) {
// Having no root is not an error, but any other xml format exception is.
throw xmlException
}
} finally {
eventReader?.close()
}
}
/**
* Flattens the XML element, as well as all text, comments, and child elements underneath it.
*
* This method flattens the XML tree whose root is [startElement], aggregates all created ids,
* and outlines any given aapt:attr xml subtrees that may exist in the given XML.
*
* @param startElement the start element that represents the root of the XML tree to be
* flattened. The start element should not be pointing to a <aapt:attr> element, as those are
* processed in a different method.
* @param eventReader the stream from which the XML file is read. The event reader should be in
* a state where [startElement] was the last event read. When this method finishes,
* [eventReader] will be positioned immediately after the [EndElement] that aligns with the
* supplied [StartElement].
* @param collectedIds The map of all created ID resources in the XML document. Any created ids
* found in the XML tree represented by [startElement] will be added to this map.
* @param resourceBuilders The map of all XML resource builders created for this XML document.
* Any new builders created because of <aapt:attr> subelements of [startElement] will be added
* to this map.
* @param currentBuilder The current xml resource builder that this [startElement] is to be
* flattened to. When this function returns, current builder will have the same active element
* as it did when the method started (or will be finished if this element was the top element of
* the builder). The subtree represented by [startElement] will be added as a child element to
* the current element.
* @param inheritedNamespaceContext When creating the top element of an extracted <aapt:attr>
* element, this variable will contain the context for all the xml namespaces that are active in
* [StartElement] that will need to be flattened to proto.
*/
private fun processElement(
startElement: StartElement,
eventReader: XMLEventReader,
collectedIds: MutableMap<ResourceName, SourcedResourceName>,
resourceBuilders: MutableMap<ResourceFile, XmlResourceBuilder>,
currentBuilder: XmlResourceBuilder,
inheritedNamespaceContext: NamespaceContext? = null
) {
// If the current start element is an aapt:attr, then either it is the root of the xml or
// there exist consecutively nested aapt:attr tags. I.e.
// <ListView>
// <aapt:attr ...>
// <aapt:attr ...>
// ...
// </aapt:attr>
// </aapt:attr>
// </ListView>
// Both of these are invalid.
if (isAaptAttribute(startElement.name)) {
val exception = RuntimeException(
"${blameSource(source, startElement.location)}: " +
"<aapt:attr> blocks are not allowed as the root of documents, or " +
"as a child element under another <aapt:attr>."
)
walkToEndOfElement(startElement, eventReader)
throw exception
}
// First, gather any new ids in the attributes of the element.
gatherIds(startElement, collectedIds)
// Add the element to the proto builder.
val elementName = startElement.name
val elementLocation = startElement.location
currentBuilder.startElement(
elementName.localPart,
elementName.namespaceURI ?: "",
elementLocation.lineNumber,
elementLocation.columnNumber
)
// Gather all the namespace declarations.
val namespaceDeclarations = startElement.namespaces
val namespaces = mutableListOf<Namespace>()
while (namespaceDeclarations.hasNext()) {
namespaces.add(namespaceDeclarations.next() as Namespace)
}
// Add all inherited namespaces that are not defined by the current element (as they would
// be overwritten).
inheritedNamespaceContext?.namespacePrefixes()?.forEach { prefix ->
if (namespaces.firstOrNull { ns -> ns.prefix == prefix } == null) {
currentBuilder.addNamespaceDeclaration(
inheritedNamespaceContext.uriForPrefix(prefix)!!, prefix
)
}
}
// Add namespaces to proto.
for (namespace in namespaces) {
currentBuilder.addNamespaceDeclaration(
namespace.namespaceURI,
namespace.prefix,
elementLocation.lineNumber,
elementLocation.columnNumber
)
}
// Add attributes to proto.
val attributes = startElement.attributes
while (attributes.hasNext()) {
val attribute = attributes.next() as Attribute
val attrName = attribute.name
currentBuilder.addAttribute(
attrName.localPart,
attrName.namespaceURI,
attribute.value,
elementLocation.lineNumber,
elementLocation.columnNumber
)
}
// Add information from children.
while (eventReader.hasNext()) {
val nextEvent = eventReader.nextEvent()
if (nextEvent.isEndElement) {
// We're done with the current element.
break
}
if (nextEvent.isStartElement) {
// If the element is a aapt:attr, we need to extract that attr.
val elName = nextEvent.asStartElement().name
if (isAaptAttribute(elName)) {
outlineAttribute(
currentBuilder,
nextEvent.asStartElement(),
eventReader,
collectedIds,
resourceBuilders
)
} else {
// We're going down a level, so process that element.
processElement(
nextEvent.asStartElement(),
eventReader,
collectedIds,
resourceBuilders,
currentBuilder
)
}
continue
}
if (nextEvent.isCharacters) {
processText(nextEvent.asCharacters(), currentBuilder)
}
if (nextEvent.eventType == XMLStreamConstants.COMMENT) {
processComment(currentBuilder)
}
}
// Finally finish element
currentBuilder.endElement()
}
/**
* Outlines the aapt:attr and assigns the correct attibute in the parent XML to the outlined
* value.
*
* @param parentBuilder the xml proto builder that is currently set to the parent element of the
* aapt:attr. This is so we can set the attribute on the parent element to a reference of the
* outlined xml resource.
* @param attrElement the startElement corresponding to the aapt:attr to be outlined.
* @param eventReader the stream from which the XML file is read. The event reader should be in
* a state where [attrElement] was the last event read. When this method finishes, [eventReader]
* will be positioned immediately after the [EndElement] that aligns with the supplied
* [attrElement].
* @param collectedIds The map of all created ID resources in the XML document. Any created ids
* found in the XML tree represented by [attrElement] will be added to this map.
* @param resourceBuilders The map of all XML resource builders created for this XML document.
* At least one new builder will be added from the outlined aapt:attr (if successful), or more
* if there are nested aapt:attr definitions.
* @return true if and only if the element was successfully outlined.
*/
private fun outlineAttribute(
parentBuilder: XmlResourceBuilder,
attrElement: StartElement,
eventReader: XMLEventReader,
collectedIds: MutableMap<ResourceName, SourcedResourceName>,
resourceBuilders: MutableMap<ResourceFile, XmlResourceBuilder>
) {
val nameAttribute = attrElement.getAttributeByName(QName("name"))
if (nameAttribute == null) {
val exception = RuntimeException(
"${blameSource(source, attrElement.location)}:" +
"<${attrElement.name}> tag requires the 'name' attribute."
)
walkToEndOfElement(attrElement, eventReader)
throw exception
}
val reference = parseXmlAttributeName(nameAttribute.value)
val nameValue = reference.name
val extractedPackage = transformPackageAlias(attrElement, nameValue.pck!!)
if (extractedPackage == null) {
val exception = RuntimeException(
blameSource(source, attrElement.location).toString() +
"Invalid namespace prefix '${nameValue.pck}' " +
"for value of 'name' attribute '$nameValue'."
)
walkToEndOfElement(attrElement, eventReader)
throw exception
}
// Check whether the namespace is or resource is private in order to properly set the
// extracted attr package.
val isPrivateNs = extractedPackage.isPrivate || reference.isPrivate
// We need to differentiate between no-namespace defined, or the alias resolves to an empty
// package. If it is the former, we need to use the res-auto schema.
val attrUri = when {
nameValue.pck.isNullOrEmpty() -> ""
extractedPackage.packageName.isEmpty() -> SCHEMA_AUTO
else -> constructPackageUri(extractedPackage.packageName, isPrivateNs)
}
// Now to process the inline xml itself.
val resource = processAaptAttr(
attrElement,
eventReader,
collectedIds,
resourceBuilders,
parentBuilder.namespaceContext
)
// Check to see if the attribute would overwrite a previously defined one.
if (parentBuilder.findAttribute(nameValue.entry!!, attrUri) != null) {
error("Cannot find attribute ${nameValue.entry}")
}
// Now to add the new attribute to the parent xml element.
parentBuilder.addAttribute(
nameValue.entry,
attrUri,
"@${resource.name}",
attrElement.location.lineNumber,
attrElement.location.columnNumber
)
}
private fun isAaptAttribute(elName: QName): Boolean =
elName.namespaceURI == AAPT_ATTR_URI && elName.localPart == AAPT_ATTR_TAG
/**
* Processes the element that is the immediate child of an aapt:attr element. This is the root
* element of the outlined XML resource.
*
* @param startElement the start element that represents the root of the XML file to be
* extracted.
* @param eventReader the stream from which the XML file is read. The event reader should be in
* a state where [startElement] was the last event read. When this method finishes,
* [eventReader] will be positioned immediately after the [EndElement] that aligns with the
* supplied [StartElement].
* @param collectedIds The map of all created ID resources in the XML document. Any created ids
* found in the XML tree represented by [startElement] will be added to this map.
* @param resourceBuilders The map of all XML resource builders created for this XML document.
* Any new builders created because of <aapt:attr> subelements of [startElement] will be added
* to this map.
* @param namespaceContext All inherited xmlns definitions so far in the XML document before the
* root of this XML subtree.
* @return the ResourceFile corresponding to the outlined element. This value will be null if
* the outlining is unsuccessful.
*/
private fun processAaptAttr(
startElement: StartElement,
eventReader: XMLEventReader,
collectedIds: MutableMap<ResourceName, SourcedResourceName>,
resourceBuilders: MutableMap<ResourceFile, XmlResourceBuilder>,
namespaceContext: NamespaceContext
): ResourceFile {
val outputFile = getNextAttrResourceFile(resourceBuilders.size - 1, startElement)
val outputBuilder = XmlResourceBuilder(outputFile)
resourceBuilders[outputFile] = outputBuilder
val errors = mutableListOf<String>()
var foundChild = false
while (eventReader.hasNext()) {
val event = eventReader.nextEvent()
if (event.isEndElement) {
// we're done with the aapt:attr
break
}
if (event.isCharacters) {
// Non-whitespace text is not allowed as children of a aapt:attr
if (!event.asCharacters().isWhiteSpace) {
errors += "${blameSource(source, event.location)}: " +
"Can't extract text into its own resource."
}
continue
}
if (event.isStartElement) {
// An aapt:attr element cannot have more than one element child.
if (foundChild) {
errors += "${blameSource(source, event.location)}: " +
"Inline XML resources must have a single root."
walkToEndOfElement(event.asStartElement(), eventReader)
continue
}
foundChild = true
processElement(
event.asStartElement(),
eventReader,
collectedIds,
resourceBuilders,
outputBuilder,
namespaceContext
)
}
// Comments are skipped.
}
if (!foundChild) {
error(
"${blameSource(source, startElement.location)}:" +
" No resource to outline. <${startElement.name}> block is empty.")
}
if (errors.any()) {
error(errors.joinToString(separator = ","))
}
return outputFile
}
private fun processText(characters: Characters, builder: XmlResourceBuilder) =
builder.addText(characters.data)
private fun processComment(builder: XmlResourceBuilder) = builder.addComment()
private fun gatherIds(
startElement: StartElement,
collectedIds: MutableMap<ResourceName, SourcedResourceName>
) {
val iterator = startElement.attributes
while (iterator.hasNext()) {
val attribute = iterator.next() as Attribute
val parsedRef = parseReference(attribute.value)
parsedRef ?: continue
val resourceName = parsedRef.reference.name
if (parsedRef.createNew && resourceName.type == AaptResourceType.ID) {
if (!isValidResourceEntryName(resourceName.entry!!)) {
error("${blameSource(source, startElement.location)} " +
"Id '$resourceName' has an invalid entry name '${resourceName.entry}'.")
} else {
collectedIds.putIfAbsent(
resourceName,
SourcedResourceName(resourceName, startElement.location.lineNumber)
)
}
}
}
}
private fun getNextAttrResourceFile(
suffix: Int, element: StartElement
): ResourceFile {
val newEntryName = mangleEntry("", "${primaryFile.name.entry}__$suffix")
return ResourceFile(
primaryFile.name.copy(entry = newEntryName),
primaryFile.configuration,
primaryFile.source.withLine(element.location.lineNumber),
ResourceFile.Type.ProtoXml
)
}
}
@@ -0,0 +1,6 @@
package com.android.aaptcompiler
import com.android.aapt.Resources
data class XmlResource (
val file: ResourceFile, val xmlProto: Resources.XmlNode)
@@ -0,0 +1,211 @@
package com.android.aaptcompiler
import com.android.aapt.Resources
import com.google.common.base.Preconditions
/**
* Tracks the active namespaces of all active elements in the [XmlResourceBuilder]
*
* This maps prefix -> a stack of uris. As prefixes may be redefined as new child elements are
* added, and need to be undefined as elements are finished.
*/
class NamespaceContext {
private val prefixMap = mutableMapOf<String, MutableList<String>>()
internal fun push(prefix: String, uri: String) {
val prefixList = prefixMap[prefix] ?: mutableListOf()
prefixList.add(uri)
prefixMap[prefix] = prefixList
}
internal fun pop(prefix: String) {
Preconditions.checkState(prefixMap[prefix] != null)
val list = prefixMap[prefix]!!
list.removeAt(list.lastIndex)
if (list.isEmpty()) {
prefixMap.remove(prefix)
}
}
fun namespacePrefixes() = prefixMap.keys
fun uriForPrefix(prefix: String) = prefixMap[prefix]?.lastOrNull()
}
class XmlResourceBuilder(val file: ResourceFile, val skipWhitespaceText: Boolean = true) {
/** The stack of the current element and all ancestors. */
private val elementStack = mutableListOf<Resources.XmlElement.Builder>()
private val elementSourceStack = mutableListOf<Resources.SourcePosition>()
/** Storage for the completed XmlResource when it is finished building. */
private var result: XmlResource? = null
/**
* The text in the current text state, since consecutive text elements should be joined to be
* in line with aapt2.
*/
private var currentTextState = ""
private lateinit var textSource: Resources.SourcePosition
/**
* Tracks the active namespaces of all active elements.
*/
val namespaceContext = NamespaceContext()
fun build(): XmlResource {
Preconditions.checkState(isFinished())
return result!!
}
fun isFinished(): Boolean = result != null
fun startElement(
name: String,
namespace: String,
lineNumber: Int = 0,
columnNumber: Int = 0): XmlResourceBuilder {
Preconditions.checkState(!isFinished())
finishTextState()
val newElement = Resources.XmlElement.newBuilder()
.setName(name)
.setNamespaceUri(namespace)
elementStack.add(newElement)
elementSourceStack.add(
Resources.SourcePosition.newBuilder()
.setLineNumber(lineNumber)
.setColumnNumber(columnNumber)
.build()
)
return this
}
fun endElement(): XmlResourceBuilder {
Preconditions.checkState(elementStack.isNotEmpty())
finishTextState()
// Build the xml Node.
val node = Resources.XmlNode.newBuilder()
.setElement(elementStack.last())
.setSource(elementSourceStack.last())
elementStack.removeAt(elementStack.lastIndex)
elementSourceStack.removeAt(elementSourceStack.lastIndex)
// Clean up namespaces
for (namespace in node.getElement().getNamespaceDeclarationList()) {
namespaceContext.pop(namespace.getPrefix())
}
if (elementStack.isEmpty()) {
// We've finished the top level element so store it to be fetched later.
result = XmlResource(file, node.build())
return this
}
// If not, then this is a child element and should be added to the next stack element.
elementStack.last().addChild(node)
return this
}
fun addNamespaceDeclaration(
uri: String, prefix: String, lineNumber: Int = 0, columnNumber: Int = 0): XmlResourceBuilder {
Preconditions.checkState(elementStack.isNotEmpty())
val currentElement = elementStack.last()
currentElement.addNamespaceDeclaration(
Resources.XmlNamespace.newBuilder()
.setUri(uri)
.setPrefix(prefix)
.setSource(
Resources.SourcePosition.newBuilder()
.setLineNumber(lineNumber)
.setColumnNumber(columnNumber)))
// Manage the namespace context.
namespaceContext.push(prefix, uri)
return this
}
fun addAttribute(
name: String,
namespace: String,
value: String,
lineNumber: Int = 0,
columnNumber: Int = 0
): XmlResourceBuilder {
Preconditions.checkState(elementStack.isNotEmpty())
val currentElement = elementStack.last()
currentElement.addAttribute(
Resources.XmlAttribute.newBuilder()
.setName(name)
.setNamespaceUri(namespace)
.setValue(value)
.setSource(
Resources.SourcePosition.newBuilder()
.setLineNumber(lineNumber)
.setColumnNumber(columnNumber)))
return this
}
fun findAttribute(name: String, namespace: String): String? {
val attrList = elementStack.last().getAttributeList()
return attrList.find {it.getName() == name && it.getNamespaceUri() == namespace}?.getValue()
}
/**
* Adds a text element to the current XML element.
*
* The text is not immediately added, but is held by the builder so that consecutive text elements
* can be accumulated as a single element for the protocol buffer. Whenever the current XML tag is
* finished or a non-text element is encountered such as a comment or new tag, the current text
* state is flushed, and the text element is added.
*/
fun addText(text: String, lineNumber: Int = 0, columnNumber: Int = 0): XmlResourceBuilder {
if ((skipWhitespaceText && text.isBlank()) || text.isEmpty()) return this
if (currentTextState.isEmpty()) {
textSource = Resources.SourcePosition.newBuilder()
.setLineNumber(lineNumber)
.setColumnNumber(columnNumber)
.build()
currentTextState = text
return this
}
currentTextState += text
return this
}
/**
* Tells the proto builder that a comment was encountered in the XML.
*
* Comments are not written to the protocol buffer, but they do indicate that any previously
* recorded text is finished. I.e.
*
* <element>
* Some text.
* <!--comment-->
* Some other text.
* </element>
*
* should be treated differently than:
*
* <element>
* Some text
* that is continued here.
* </element>
*
*/
fun addComment(): XmlResourceBuilder {
Preconditions.checkState(elementStack.isNotEmpty())
finishTextState()
return this
}
private fun finishTextState() {
if (currentTextState.isEmpty()) return
val currentElement = elementStack.last()
currentElement.addChild(
Resources.XmlNode.newBuilder()
.setText(currentTextState)
.setSource(textSource))
currentTextState = ""
}
}
@@ -0,0 +1,371 @@
package com.android.aaptcompiler
import com.android.aaptcompiler.android.parseHex
import java.util.PrimitiveIterator
import kotlin.math.max
import kotlin.math.min
/**
* A string flattened from an XML hierarchy, which maintains tags and untranslatable sections in
* parallel data structures.
*
* @property rawString The raw string of the flattened xml.
* <p> This string will contain all removed spaces and escape characters. For the processed
* string see {@code styleString.str}.
* @property styleString The processed string with all spans generated.
* @property untranslatableSections all marked untranslatable sections from the xml.
* @property success Whether or not this resource was parsed without error.
*/
data class FlattenedXml(
val rawString: String,
val styleString: StyleString,
val untranslatableSections: List<UntranslatableSection>,
val success: Boolean)
/**
* Flattens an XML hierarchy into a FlattenedXml string, formatting the text, escaping characters,
* and removing whitespace, all while keeping the untranslatable sections and spans in sync with the
* transformations.
*
* <p> Specifically, the StringBuilder will handle escaped characters like \t, \n, \\, \', etc.
* Single quotes *must* be escaped, unless within a pair of double-quotes.
* Pairs of double-quotes disable whitespace stripping of the enclosed text.
* Unicode escape codes (\u0049) are interpreted and the represented Unicode character is inserted.
*
* <p> A NOTE ON WHITESPACE:
*
* <p> When preserve_spaces is false, and when text is not enclosed within double-quotes,
* StringBuilder replaces a series of whitespace with a single space character. This happens at the
* start and end of the string as well, so leading and trailing whitespace is possible, but only if
* the flattened xml being built contains spans.
*
* <p> When a Span is started or stopped, the whitespace counter is reset, meaning if whitespace
* is encountered directly after the span, it will be emitted. This leads to situations like the
* following: "This <b> is </b> spaced" -> "This is spaced". Without spans, this would be properly
* compressed: "This is spaced" -> "This is spaced".
*
* <p> Untranslatable sections do not have the same problem:
* "This <xliff:g> is </xliff:g> not spaced" -> "This is not spaced".
*
* <p> NOTE: This is all the way it is because AAPT1 did it this way. Maintaining backwards
* compatibility is important.
*
* @property preserveSpaces If true, whitespace removal is not performed.
* <p> Single quotations can be used without escaping when {@code preservedSpaces} is set to true
* as well.
*/
class XmlStringBuilder(private val preserveSpaces: Boolean = false) {
var inQuote = preserveSpaces
private set
var lastCodepointWasSpace = false
private set
private var lastCodepointWasBackslash = false
private var firstQuote = -1
private var lastQuote = -1
private var firstChar = -1
private var lastChar = -1
private var rawStringBuilder: StringBuilder = StringBuilder()
private var textBuilder: StringBuilder = StringBuilder()
private val spans = mutableListOf<Span>()
private val spanStack = mutableListOf<Span>()
private val untranslatableSections = mutableListOf<UntranslatableSection>()
private var inUntranslatable = false
/** @property error the last error found while constructing the flattened xml. */
var error = ""
private set
/** Resets the XmlStringBuilder state to an empty string. */
fun clear() {
inQuote = preserveSpaces
lastCodepointWasSpace = false
lastCodepointWasBackslash = false
firstQuote = -1
lastQuote = -1
firstChar = -1
lastChar = -1
rawStringBuilder = StringBuilder()
textBuilder = StringBuilder()
spans.clear()
spanStack.clear()
untranslatableSections.clear()
inUntranslatable = false
error = ""
}
/** Transform escaped codepoint into expected codepoint. */
fun handleEscape(
codePoint: Int,
textBuilder: StringBuilder,
codePoints: PrimitiveIterator.OfInt,
str: String
): Boolean {
when (codePoint) {
't'.toInt() -> textBuilder.append('\t')
'n'.toInt() -> textBuilder.append('\n')
'#'.toInt(), '@'.toInt(), '?'.toInt(), '"'.toInt(), '\''.toInt(), '\\'.toInt() ->
textBuilder.appendCodePoint(codePoint)
'u'.toInt() -> {
if (!appendUnicodeEscapeSequence(codePoints, textBuilder)) {
error = "Invalid unicode escape sequence in string\n\"$str\""
return false
}
}
// Can ignore the escape character and just include the code point.
else -> textBuilder.appendCodePoint(codePoint)
}
return true
}
/** Appends a chunk of text to the xml string. */
fun append(str : String) : XmlStringBuilder {
rawStringBuilder.append(str)
val codePoints = str.codePoints().iterator()
while (codePoints.hasNext()) {
var codePoint = codePoints.nextInt()
if (!Character.isValidCodePoint(codePoint)) {
error = "Invalid unicode code point in string\n\"$str\""
return this
}
if (Character.isWhitespace(codePoint) && !preserveSpaces && !inQuote) {
if (!lastCodepointWasSpace) {
// emit a space if it is the first
textBuilder.append(' ')
lastCodepointWasSpace = true
lastCodepointWasBackslash = false
}
continue
}
if (firstChar == -1) {
firstChar = textBuilder.length
}
lastCodepointWasSpace = false
when {
lastCodepointWasBackslash -> {
if (!handleEscape(codePoint, textBuilder, codePoints, str)) {
return this
}
lastChar = textBuilder.length
lastCodepointWasBackslash = false
}
codePoint == '\\'.toInt() -> {
if (codePoints.hasNext()) {
codePoint = codePoints.nextInt()
if (!handleEscape(codePoint, textBuilder, codePoints, str)) {
return this
}
lastChar = textBuilder.length
} else {
lastCodepointWasBackslash = true
}
}
codePoint == '\"'.toInt() && !preserveSpaces -> {
// only toggle quote when we are not preserving spaces.
inQuote = !inQuote
if (firstQuote == -1) {
firstQuote = textBuilder.length
}
lastQuote = textBuilder.length
lastChar = textBuilder.length
}
codePoint == '\''.toInt() && !preserveSpaces && !inQuote -> {
// this should be escaped when we are not preserving spaces
error = "Invalid unicode escape sequence in string\n\"{str}\""
return this
}
else -> {
textBuilder.appendCodePoint(codePoint)
lastChar = textBuilder.length
}
}
}
return this
}
/**
* Starts a Span (tag) with the given name.
* <p> The name is expected to be of the form:
* "tag_name;attr1=value;attr2=value"
* <p> Which is how Spans are encoded in the ResStringPool. A corresponding call to
* [endSpan] is needed to close the span.
*/
fun startSpan(name : String): XmlStringBuilder {
if (error.isNotEmpty()) {
return this
}
// When a span is started, all state associated with whitespace truncation and quotation is
// ended.
resetTextState()
val span = Span(name, textBuilder.length)
spans.add(span)
spanStack.add(span)
return this
}
/** Ends the last span (tag) that was started with a call to [startSpan]. */
fun endSpan(): XmlStringBuilder {
if (error.isNotEmpty()) {
return this
}
if (spanStack.isEmpty()) {
error = "Unmatched span end."
return this
}
// When a span is ended, all state associated with whitespace truncation and quotation is ended.
resetTextState()
val lastSpan = spanStack.removeAt(spanStack.lastIndex)
lastSpan.lastChar = textBuilder.length - 1
return this
}
/**
* Starts an Untranslatable section.
* <p> To end the section, call the [endUntranslatable] method. No more than one translatable
* section can be open at one time.
*/
fun startUntranslatable(): XmlStringBuilder {
if (error.isNotEmpty()) {
return this
}
if (inUntranslatable) {
error = "Attempt to nest untranslatable sections in flattened xml, which is not allowed."
return this
}
val section = UntranslatableSection(textBuilder.length)
untranslatableSections.add(section)
inUntranslatable = true
return this
}
/** Ends the current untranslatable section started with a call to [startUntranslatable] */
fun endUntranslatable(): XmlStringBuilder {
if (error.isNotEmpty()) {
return this
}
if (!inUntranslatable) {
error = "Attempting to end untranslatable section while not in an untranslatable sections."
return this
}
untranslatableSections.last().endIndex = textBuilder.length
inUntranslatable = false
return this
}
/**
* Returns the flattened XML string, with all spans and untranslatable sections encoded as
* parallel data structures.
*/
fun getFlattenedXml() : FlattenedXml {
if (inUntranslatable) {
error = "Attempting to flatten xml with unfinished untranslatable section."
}
if (spanStack.isNotEmpty()) {
error = "Attempting to flatten xml with unfinished spans."
}
var untranslatables: List<UntranslatableSection> = untranslatableSections
var text = textBuilder.toString()
if (spans.isEmpty()) {
var firstChar = getFirstChar()
if (untranslatableSections.isNotEmpty()) {
firstChar = min(firstChar, untranslatableSections.first().startIndex)
}
var lastChar = getLastChar(text)
if (untranslatableSections.isNotEmpty()) {
lastChar = max(lastChar, untranslatableSections.last().endIndex)
}
untranslatables = untranslatableSections.map {
it.shift(-firstChar)
}
// If we had a string made fully of whitespace, then firstChar > lastChar. Since we're
// removing leading and trailing whitespaces, handle this as an empty string.
text = if (firstChar >= lastChar) "" else textBuilder.substring(firstChar, lastChar)
}
val raw = rawStringBuilder.toString()
return FlattenedXml(
raw, StyleString(text, spans.toList()), untranslatables.toList(), error.isEmpty())
}
private fun getFirstChar(): Int {
// If there were no spans, we treat this string a little differently (according to AAPT).
// We must strip the leading and trailing whitespace.
if (firstQuote == -1) {
if (firstChar != -1) {
// If there are no quotes, remove all whitespace before first character.
return firstChar
}
// If there are no characters in the string, it's just whitespaces, the resulting string
// string should be empty.
return textBuilder.length
}
// If there are quotes we need to check if there are any non-whitespace characters before
// the quote. If there are any, the string should be preserved.
if (firstChar != -1 && firstChar < firstQuote)
return 0
return firstQuote
}
private fun getLastChar(text: String): Int {
if (lastQuote == -1) {
// If we had no quotes then just trim the whitespace at the end of the string.
if (lastChar != -1) {
return lastChar
}
// If there were no characters, it's all whitespace, and the resulting string should be empty.
return 0
}
// If we had quotes, only trim the string if there are exclusively whitespaces after the
// last quote.
if (lastChar != -1 && lastChar > lastQuote)
return text.length
return lastQuote
}
private fun appendUnicodeEscapeSequence(iter : Iterator<Int>, text: StringBuilder) : Boolean {
var code = 0
for (i in 1..4) {
if (!iter.hasNext()) {
break
}
val codePoint = iter.next()
val value = parseHex(codePoint)
if (value == -1) return false
code = (code shl 4) or value
}
if (!Character.isValidCodePoint(code)) {
return false
}
text.appendCodePoint(code)
return true
}
private fun resetTextState() {
inQuote = preserveSpaces
lastCodepointWasSpace = false
lastCodepointWasBackslash = false
}
}
@@ -0,0 +1,91 @@
package com.android.aaptcompiler
import com.android.SdkConstants
import javax.xml.stream.XMLEventReader
import javax.xml.stream.XMLInputFactory
import javax.xml.stream.events.StartElement
internal val xmlInputFactory = XMLInputFactory.newFactory()
const val SCHEMA_PUBLIC_PREFIX = SdkConstants.URI_PREFIX
const val SCHEMA_PRIVATE_PREFIX = "http://schemas.android.com/apk/prv/res/"
const val SCHEMA_AUTO = SdkConstants.AUTO_URI
const val AAPT_ATTR_URI = "http://schemas.android.com/aapt"
// Result of extracting a package name from a namespace URI declaration
data class ExtractedPackage(val packageName: String, val isPrivate: Boolean)
private val EMPTY_PACKAGE = ExtractedPackage("", false)
fun extractPackageFromUri(namespaceUri: String): ExtractedPackage? {
return when {
namespaceUri.startsWith(SCHEMA_PUBLIC_PREFIX) -> {
val packageName = namespaceUri.substring(SCHEMA_PUBLIC_PREFIX.length)
if (packageName.isEmpty()) {
return null
}
ExtractedPackage(packageName, false)
}
namespaceUri.startsWith(SCHEMA_PRIVATE_PREFIX) -> {
val packageName = namespaceUri.substring(SCHEMA_PRIVATE_PREFIX.length)
if (packageName.isEmpty()) {
return null
}
ExtractedPackage(packageName, true)
}
namespaceUri == SCHEMA_AUTO -> ExtractedPackage("", true)
else -> null
}
}
fun constructPackageUri(alias: String, isPrivate: Boolean) = if (isPrivate) {
SCHEMA_PRIVATE_PREFIX + alias
} else {
SCHEMA_PUBLIC_PREFIX + alias
}
fun transformPackageAlias(element: StartElement, alias: String): ExtractedPackage? {
if (alias.isEmpty()) {
return EMPTY_PACKAGE
}
val uri = element.getNamespaceURI(alias) ?: return null
return extractPackageFromUri(uri)
}
fun resolvePackage(element: StartElement, ref: Reference) {
if (ref.name != ResourceName.EMPTY) {
val transformedPackage = transformPackageAlias(element, ref.name.pck!!) ?: return
ref.name = ref.name.copy(pck = transformedPackage.packageName)
ref.isPrivate = ref.isPrivate || transformedPackage.isPrivate
}
}
/**
* Polls the {@code eventReader} till the reader is after the corresponding end element of
* {@code element}.
*
* <p> It is assumed that {@code element} is the last startElement read from
* the reader.
*
* @param element The start of the element to which we want to reach the end of.
* @param eventReader The eventReader to be moved to the end of the corresponding
* {@code EndElement}.
*/
internal fun walkToEndOfElement(element: StartElement, eventReader: XMLEventReader) {
var depth = 1
while (eventReader.hasNext()) {
val event = eventReader.nextEvent()
if (event.isStartElement) {
++depth
} else if (event.isEndElement) {
--depth
if (depth == 0) {
// Sanity check.
assert(event.asEndElement().name.localPart == element.name.localPart)
break
}
}
}
}
@@ -0,0 +1,444 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.aaptcompiler.android
object AConfiguration {
/** Orientation: not specified. */
const val ACONFIGURATION_ORIENTATION_ANY: Byte = 0x0000
/**
* Orientation: const value corresponding to the
* [port](@dacRoot/guide/topics/resources/providing-resources.html#OrientationQualifier)
* resource qualifier.
*/
const val ACONFIGURATION_ORIENTATION_PORT : Byte = 0x0001
/**
* Orientation: const value corresponding to the
* [land](@dacRoot/guide/topics/resources/providing-resources.html#OrientationQualifier)
* resource qualifier.
*/
const val ACONFIGURATION_ORIENTATION_LAND : Byte = 0x0002
@Deprecated("Not currently supported or used. ")
const val ACONFIGURATION_ORIENTATION_SQUARE : Byte = 0x0003
/** Touchscreen: not specified. */
const val ACONFIGURATION_TOUCHSCREEN_ANY : Byte = 0x0000
/**
* Touchscreen: const value corresponding to the
* [notouch](@dacRoot/guide/topics/resources/providing-resources.html#TouchscreenQualifier)
* resource qualifier.
*/
const val ACONFIGURATION_TOUCHSCREEN_NOTOUCH : Byte = 0x0001
@Deprecated("Not currently supported or used. ")
const val ACONFIGURATION_TOUCHSCREEN_STYLUS : Byte = 0x0002
/**
* Touchscreen: const value corresponding to the
* [finger](@dacRoot/guide/topics/resources/providing-resources.html#TouchscreenQualifier)
* resource qualifier.
*/
const val ACONFIGURATION_TOUCHSCREEN_FINGER : Byte = 0x0003
/** Density: default density. */
const val ACONFIGURATION_DENSITY_DEFAULT : Byte = 0
/**
* Density: const value corresponding to the
* [ldpi](@dacRoot/guide/topics/resources/providing-resources.html#DensityQualifier)
* resource qualifier.
*/
const val ACONFIGURATION_DENSITY_LOW : Byte = 120
/**
* Density: const value corresponding to the
* [mdpi](@dacRoot/guide/topics/resources/providing-resources.html#DensityQualifier)
* resource qualifier.
*/
const val ACONFIGURATION_DENSITY_MEDIUM : Int = 160
/**
* Density: const value corresponding to the
* [tvdpi](@dacRoot/guide/topics/resources/providing-resources.html#DensityQualifier)
* resource qualifier.
*/
const val ACONFIGURATION_DENSITY_TV : Int = 213
/**
* Density: const value corresponding to the
* [hdpi](@dacRoot/guide/topics/resources/providing-resources.html#DensityQualifier)
* resource qualifier.
*/
const val ACONFIGURATION_DENSITY_HIGH : Int = 240
/**
* Density: const value corresponding to the
* [xhdpi](@dacRoot/guide/topics/resources/providing-resources.html#DensityQualifier)
* resource qualifier.
*/
const val ACONFIGURATION_DENSITY_XHIGH : Int = 320
/**
* Density: const value corresponding to the
* [xxhdpi](@dacRoot/guide/topics/resources/providing-resources.html#DensityQualifier)
* resource qualifier.
*/
const val ACONFIGURATION_DENSITY_XXHIGH : Int = 480
/**
* Density: const value corresponding to the
* [xxxhdpi](@dacRoot/guide/topics/resources/providing-resources.html#DensityQualifier)
* resource qualifier.
*/
const val ACONFIGURATION_DENSITY_XXXHIGH : Int = 640
/** Density: any density. */
const val ACONFIGURATION_DENSITY_ANY : Int = 0xfffe
/** Density: no density specified. */
const val ACONFIGURATION_DENSITY_NONE : Int = 0xffff
/** Keyboard: not specified. */
const val ACONFIGURATION_KEYBOARD_ANY : Byte = 0x0000
/**
* Keyboard: const value corresponding to the
* [nokeys](@dacRoot/guide/topics/resources/providing-resources.html#ImeQualifier)
* resource qualifier.
*/
const val ACONFIGURATION_KEYBOARD_NOKEYS : Byte = 0x0001
/**
* Keyboard: const value corresponding to the
* [qwerty](@dacRoot/guide/topics/resources/providing-resources.html#ImeQualifier)
* resource qualifier.
*/
const val ACONFIGURATION_KEYBOARD_QWERTY : Byte = 0x0002
/**
* Keyboard: const value corresponding to the
* [12key](@dacRoot/guide/topics/resources/providing-resources.html#ImeQualifier)
* resource qualifier.
*/
const val ACONFIGURATION_KEYBOARD_12KEY : Byte = 0x0003
/** Navigation: not specified. */
const val ACONFIGURATION_NAVIGATION_ANY : Byte = 0x0000
/**
* Navigation: const value corresponding to the
* [nonav](@@dacRoot/guide/topics/resources/providing-resources.html#NavigationQualifier)
* resource qualifier.
*/
const val ACONFIGURATION_NAVIGATION_NONAV : Byte = 0x0001
/**
* Navigation: const value corresponding to the
* [dpad](@dacRoot/guide/topics/resources/providing-resources.html#NavigationQualifier)
* resource qualifier.
*/
const val ACONFIGURATION_NAVIGATION_DPAD : Byte = 0x0002
/**
* Navigation: const value corresponding to the
* [trackball](@dacRoot/guide/topics/resources/providing-resources.html#NavigationQualifier)
* resource qualifier.
*/
const val ACONFIGURATION_NAVIGATION_TRACKBALL : Byte = 0x0003
/**
* Navigation: const value corresponding to the
* [wheel](@dacRoot/guide/topics/resources/providing-resources.html#NavigationQualifier)
* resource qualifier.
*/
const val ACONFIGURATION_NAVIGATION_WHEEL : Byte = 0x0004
/** Keyboard availability: not specified. */
const val ACONFIGURATION_KEYSHIDDEN_ANY : Byte = 0x0000
/**
* Keyboard availability: const value corresponding to the
* [keysexposed](@dacRoot/guide/topics/resources/providing-resources.html#KeyboardAvailQualifier)
* resource qualifier.
*/
const val ACONFIGURATION_KEYSHIDDEN_NO : Byte = 0x0001
/**
* Keyboard availability: const value corresponding to the
* [keyshidden](@dacRoot/guide/topics/resources/providing-resources.html#KeyboardAvailQualifier)
* resource qualifier.
*/
const val ACONFIGURATION_KEYSHIDDEN_YES : Byte = 0x0002
/**
* Keyboard availability: const value corresponding to the
* [keyssoft](@dacRoot/guide/topics/resources/providing-resources.html#KeyboardAvailQualifier)
* resource qualifier.
*/
const val ACONFIGURATION_KEYSHIDDEN_SOFT : Byte = 0x0003
/** Navigation availability: not specified. */
const val ACONFIGURATION_NAVHIDDEN_ANY : Byte = 0x0000
/**
* Navigation availability: const value corresponding to the
* [navexposed](@dacRoot/guide/topics/resources/providing-resources.html#NavAvailQualifier)
* resource qualifier.
*/
const val ACONFIGURATION_NAVHIDDEN_NO : Byte = 0x0001
/**
* Navigation availability: const value corresponding to the
* [navhidden](@dacRoot/guide/topics/resources/providing-resources.html#NavAvailQualifier)
* resource qualifier.
*/
const val ACONFIGURATION_NAVHIDDEN_YES : Byte = 0x0002
/** Screen size: not specified. */
const val ACONFIGURATION_SCREENSIZE_ANY : Byte = 0x00
/**
* Screen size: const value indicating the screen is at least
* approximately 320x426 dp units, corresponding to the
* [small](@dacRoot/guide/topics/resources/providing-resources.html#ScreenSizeQualifier)
* resource qualifier.
*/
const val ACONFIGURATION_SCREENSIZE_SMALL : Byte = 0x01
/**
* Screen size: const value indicating the screen is at least
* approximately 320x470 dp units, corresponding to the
* [normal](@dacRoot/guide/topics/resources/providing-resources.html#ScreenSizeQualifier)
* resource qualifier.
*/
const val ACONFIGURATION_SCREENSIZE_NORMAL : Byte = 0x02
/**
* Screen size: const value indicating the screen is at least
* approximately 480x640 dp units, corresponding to the
* [large](@dacRoot/guide/topics/resources/providing-resources.html#ScreenSizeQualifier)
* resource qualifier.
*/
const val ACONFIGURATION_SCREENSIZE_LARGE : Byte = 0x03
/**
* Screen size: const value indicating the screen is at least
* approximately 720x960 dp units, corresponding to the
* [xlarge](@dacRoot/guide/topics/resources/providing-resources.html#ScreenSizeQualifier)
* resource qualifier.
*/
const val ACONFIGURATION_SCREENSIZE_XLARGE : Byte = 0x04
/** Screen layout: not specified. */
const val ACONFIGURATION_SCREENLONG_ANY : Byte = 0x00
/**
* Screen layout: const value that corresponds to the
* [notlong](@dacRoot/guide/topics/resources/providing-resources.html#ScreenAspectQualifier)
* resource qualifier.
*/
const val ACONFIGURATION_SCREENLONG_NO : Byte = 0x1
/**
* Screen layout: const value that corresponds to the
* [long](@dacRoot/guide/topics/resources/providing-resources.html#ScreenAspectQualifier)
* resource qualifier.
*/
const val ACONFIGURATION_SCREENLONG_YES : Byte = 0x2
const val ACONFIGURATION_SCREENROUND_ANY : Byte = 0x00
const val ACONFIGURATION_SCREENROUND_NO : Byte = 0x1
const val ACONFIGURATION_SCREENROUND_YES : Byte = 0x2
/** Wide color gamut: not specified. */
const val ACONFIGURATION_WIDE_COLOR_GAMUT_ANY : Byte = 0x00
/**
* Wide color gamut: const value that corresponds to
* [no
* nowidecg](@dacRoot/guide/topics/resources/providing-resources.html#WideColorGamutQualifier)
* resource qualifier specified.
*/
const val ACONFIGURATION_WIDE_COLOR_GAMUT_NO : Byte = 0x1
/**
* Wide color gamut: const value that corresponds to
* [
* widecg](@dacRoot/guide/topics/resources/providing-resources.html#WideColorGamutQualifier)
* resource qualifier specified.
*/
const val ACONFIGURATION_WIDE_COLOR_GAMUT_YES : Byte = 0x2
/** HDR: not specified. */
const val ACONFIGURATION_HDR_ANY : Byte = 0x00
/**
* HDR: const value that corresponds to
* [
* lowdr](@dacRoot/guide/topics/resources/providing-resources.html#HDRQualifier) resource
* qualifier specified.
*/
const val ACONFIGURATION_HDR_NO : Byte = 0x1
/**
* HDR: const value that corresponds to
* [
* highdr](@dacRoot/guide/topics/resources/providing-resources.html#HDRQualifier) resource
* qualifier specified.
*/
const val ACONFIGURATION_HDR_YES : Byte = 0x2
/** UI mode: not specified. */
const val ACONFIGURATION_UI_MODE_TYPE_ANY : Byte = 0x00
/**
* UI mode: const value that corresponds to
* [no
* UI mode type](@dacRoot/guide/topics/resources/providing-resources.html#UiModeQualifier)
* resource qualifier specified.
*/
const val ACONFIGURATION_UI_MODE_TYPE_NORMAL : Byte = 0x01
/**
* UI mode: const value that corresponds to
* [desk](@dacRoot/guide/topics/resources/providing-resources.html#UiModeQualifier) resource
* qualifier specified.
*/
const val ACONFIGURATION_UI_MODE_TYPE_DESK : Byte = 0x02
/**
* UI mode: const value that corresponds to
* [car](@dacRoot/guide/topics/resources/providing-resources.html#UiModeQualifier) resource
* qualifier specified.
*/
const val ACONFIGURATION_UI_MODE_TYPE_CAR : Byte = 0x03
/**
* UI mode: const value that corresponds to
* [television](@dacRoot/guide/topics/resources/providing-resources.html#UiModeQualifier) resource
* qualifier specified.
*/
const val ACONFIGURATION_UI_MODE_TYPE_TELEVISION : Byte = 0x04
/**
* UI mode: const value that corresponds to
* [appliance](@dacRoot/guide/topics/resources/providing-resources.html#UiModeQualifier) resource
* qualifier specified.
*/
const val ACONFIGURATION_UI_MODE_TYPE_APPLIANCE : Byte = 0x05
/**
* UI mode: const value that corresponds to
* [watch](@dacRoot/guide/topics/resources/providing-resources.html#UiModeQualifier) resource
* qualifier specified.
*/
const val ACONFIGURATION_UI_MODE_TYPE_WATCH : Byte = 0x06
/**
* UI mode: const value that corresponds to
* [vr](@dacRoot/guide/topics/resources/providing-resources.html#UiModeQualifier) resource
* qualifier specified.
*/
const val ACONFIGURATION_UI_MODE_TYPE_VR_HEADSET : Byte = 0x07
/** UI night mode: not specified. */
const val ACONFIGURATION_UI_MODE_NIGHT_ANY : Byte = 0x00
/**
* UI night mode: const value that corresponds to
* [notnight](@dacRoot/guide/topics/resources/providing-resources.html#NightQualifier) resource
* qualifier specified.
*/
const val ACONFIGURATION_UI_MODE_NIGHT_NO : Byte = 0x1
/**
* UI night mode: const value that corresponds to
* [night](@dacRoot/guide/topics/resources/providing-resources.html#NightQualifier) resource
* qualifier specified.
*/
const val ACONFIGURATION_UI_MODE_NIGHT_YES : Byte = 0x2
/** Screen width DPI: not specified. */
const val ACONFIGURATION_SCREEN_WIDTH_DP_ANY : Byte = 0x0000
/** Screen height DPI: not specified. */
const val ACONFIGURATION_SCREEN_HEIGHT_DP_ANY : Byte = 0x0000
/** Smallest screen width DPI: not specified. */
const val ACONFIGURATION_SMALLEST_SCREEN_WIDTH_DP_ANY : Byte = 0x0000
/** Layout direction: not specified. */
const val ACONFIGURATION_LAYOUTDIR_ANY : Byte = 0x00
/**
* Layout direction: const value that corresponds to
* [ldltr](@dacRoot/guide/topics/resources/providing-resources.html#LayoutDirectionQualifier)
* resource qualifier specified.
*/
const val ACONFIGURATION_LAYOUTDIR_LTR : Byte = 0x01
/**
* Layout direction: const value that corresponds to
* [ldrtl](@dacRoot/guide/topics/resources/providing-resources.html#LayoutDirectionQualifier)
* resource qualifier specified.
*/
const val ACONFIGURATION_LAYOUTDIR_RTL : Byte = 0x02
/**
* Bit mask for
* [mcc](@dacRoot/guide/topics/resources/providing-resources.html#MccQualifier)
* configuration.
*/
const val ACONFIGURATION_MCC : Byte = 0x0001
/**
* Bit mask for
* [mnc](@dacRoot/guide/topics/resources/providing-resources.html#MccQualifier)
* configuration.
*/
const val ACONFIGURATION_MNC : Byte = 0x0002
/**
* Bit mask for
* [locale]({@docRoot}guide/topics/resources/providing-resources.html#LocaleQualifier)
* configuration.
*/
const val ACONFIGURATION_LOCALE : Byte = 0x0004
/**
* Bit mask for
* [touchscreen](@dacRoot/guide/topics/resources/providing-resources.html#TouchscreenQualifier)
* configuration.
*/
const val ACONFIGURATION_TOUCHSCREEN : Byte = 0x0008
/**
* Bit mask for
* [keyboard](@dacRoot/guide/topics/resources/providing-resources.html#ImeQualifier)
* configuration.
*/
const val ACONFIGURATION_KEYBOARD : Byte = 0x0010
/**
* Bit mask for
* [keyboardHidden](@dacRoot/guide/topics/resources/providing-resources.html#KeyboardAvailQualifier)
* configuration.
*/
const val ACONFIGURATION_KEYBOARD_HIDDEN : Byte = 0x0020
/**
* Bit mask for
* [navigation](@dacRoot/guide/topics/resources/providing-resources.html#NavigationQualifier)
* configuration.
*/
const val ACONFIGURATION_NAVIGATION : Byte = 0x0040
/**
* Bit mask for
* [orientation](@dacRoot/guide/topics/resources/providing-resources.html#OrientationQualifier)
* configuration.
*/
const val ACONFIGURATION_ORIENTATION : Int = 0x0080
/**
* Bit mask for
* [density](@dacRoot/guide/topics/resources/providing-resources.html#DensityQualifier)
* configuration.
*/
const val ACONFIGURATION_DENSITY : Int = 0x0100
/**
* Bit mask for
* [screen size](@dacRoot/guide/topics/resources/providing-resources.html#ScreenSizeQualifier)
* configuration.
*/
const val ACONFIGURATION_SCREEN_SIZE : Int = 0x0200
/**
* Bit mask for
* [platform version](@dacRoot/guide/topics/resources/providing-resources.html#VersionQualifier)
* configuration.
*/
const val ACONFIGURATION_VERSION : Int = 0x0400
/**
* Bit mask for screen layout configuration.
*/
const val ACONFIGURATION_SCREEN_LAYOUT : Int = 0x0800
/**
* Bit mask for
* [ui mode](@dacRoot/guide/topics/resources/providing-resources.html#UiModeQualifier)
* configuration.
*/
const val ACONFIGURATION_UI_MODE : Int = 0x1000
/**
* Bit mask for
* [smallest screen width](@dacRoot/guide/topics/resources/providing-resources.html#SmallestScreenWidthQualifier)
* configuration.
*/
const val ACONFIGURATION_SMALLEST_SCREEN_SIZE : Int = 0x2000
/**
* Bit mask for
* [layout direction](@dacRoot/guide/topics/resources/providing-resources.html#LayoutDirectionQualifier)
* configuration.
*/
const val ACONFIGURATION_LAYOUTDIR : Int = 0x4000
const val ACONFIGURATION_SCREEN_ROUND : Int = 0x8000
/**
* Bit mask for
* [wide color gamut](@dacRoot/guide/topics/resources/providing-resources.html#WideColorGamutQualifier)
* and [HDR](@dacRoot/guide/topics/resources/providing-resources.html#HDRQualifier) configurations.
*/
const val ACONFIGURATION_COLOR_MODE : Int = 0x10000
/**
* Constant used to to represent MNC (Mobile Network Code) zero.
* 0 cannot be used, since it is used to represent an undefined MNC.
*/
const val ACONFIGURATION_MNC_ZERO : Int = 0xffff
}
@@ -0,0 +1,266 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.aaptcompiler.android;
import static com.android.aaptcompiler.android.LocaleDataTablesKt.*;
import java.util.Arrays;
import java.util.Map;
/**
* transliterated from:
* https://android.googlesource.com/platform/frameworks/base/+/android-9.0.0_r12/libs/androidfw/LocaleData.cpp
*/
public class LocaleData {
private static int packLocale(final byte[] language, final byte[] region) {
return ((language[0] & 0xff) << 24)
| ((language[1] & 0xff) << 16)
| ((region[0] & 0xff) << 8)
| (region[1] & 0xff);
}
private static int dropRegion(int packed_locale) {
return packed_locale & 0xFFFF0000;
}
private static boolean hasRegion(int packed_locale) {
return (packed_locale & 0x0000FFFF) != 0;
}
static final int SCRIPT_LENGTH = 4;
private static final int PACKED_ROOT = 0; // to represent the root locale
private static int findParent(int packed_locale, final String script) {
if (hasRegion(packed_locale)) {
for (Map.Entry<String, Map<Integer, Integer>> entry : getSCRIPT_PARENTS().entrySet()) {
if (script.equals(entry.getKey())) {
Map<Integer, Integer> map = entry.getValue();
Integer lookup_result = map.get(packed_locale);
if (lookup_result != null) {
return lookup_result;
}
break;
}
}
return dropRegion(packed_locale);
}
return PACKED_ROOT;
}
/**
* Find the ancestors of a locale, and fill 'out' with it (assumes out has enough
* space). If any of the members of stop_list was seen, write it in the
* output but stop afterwards.
*
* This also outputs the index of the last written ancestor in the stop_list
* to stop_list_index, which will be -1 if it is not found in the stop_list.
*
* Returns the number of ancestors written in the output, which is always
* at least one.
*
* (If 'out' is null, we do everything the same way but we simply don't write
* any results in 'out'.)
*/
static int findAncestors(
int[] out,
Ref<Long> stop_list_index,
int packed_locale,
final String script,
final int[] stop_list,
int stop_set_length) {
int ancestor = packed_locale;
int count = 0;
do {
if (out != null) {
out[count] = ancestor;
}
count++;
for (int i = 0; i < stop_set_length; i++) {
if (stop_list[i] == ancestor) {
stop_list_index.set((long) i);
return count;
}
}
ancestor = findParent(ancestor, script);
} while (ancestor != PACKED_ROOT);
stop_list_index.set((long) -1);
return count;
}
static int findDistance(
int supported,
final String script,
final int[] request_ancestors,
int request_ancestors_count) {
final Ref<Long> request_ancestors_indexRef = new Ref<>(null);
final int supported_ancestor_count =
findAncestors(
null,
request_ancestors_indexRef,
supported,
script,
request_ancestors,
request_ancestors_count);
// Since both locales share the same root, there will always be a shared
// ancestor, so the distance in the parent tree is the sum of the distance
// of 'supported' to the lowest common ancestor (number of ancestors
// written for 'supported' minus 1) plus the distance of 'request' to the
// lowest common ancestor (the index of the ancestor in request_ancestors).
return (int) (supported_ancestor_count + request_ancestors_indexRef.get() - 1);
}
static boolean isRepresentative(int language_and_region, final String script) {
final long packed_locale =
((((long) language_and_region) << 32)
| (((long) script.charAt(0) & 0xff) << 24)
| (((long) script.charAt(1) & 0xff) << 16)
| (((long) script.charAt(2) & 0xff) << 8)
| ((long) script.charAt(3) & 0xff));
return (getREPRESENTATIVE_LOCALES().contains(packed_locale));
}
private static final int US_SPANISH = 0x65735553; // es-US
private static final int MEXICAN_SPANISH = 0x65734D58; // es-MX
private static final int LATIN_AMERICAN_SPANISH = 0x6573A424; // es-419
/**
* The two locales es-US and es-MX are treated as special fallbacks for es-419.
* If there is no es-419, they are considered its equivalent.
*/
private static boolean isSpecialSpanish(int language_and_region) {
return (language_and_region == US_SPANISH || language_and_region == MEXICAN_SPANISH);
}
static int localeDataCompareRegions(
final byte[] left_region,
final byte[] right_region,
final byte[] requested_language,
final String requested_script,
final byte[] requested_region) {
if (left_region[0] == right_region[0] && left_region[1] == right_region[1]) {
return 0;
}
int left = packLocale(requested_language, left_region);
int right = packLocale(requested_language, right_region);
final int request = packLocale(requested_language, requested_region);
// If one and only one of the two locales is a special Spanish locale, we
// replace it with es-419. We don't do the replacement if the other locale
// is already es-419, or both locales are special Spanish locales (when
// es-US is being compared to es-MX).
final boolean leftIsSpecialSpanish = isSpecialSpanish(left);
final boolean rightIsSpecialSpanish = isSpecialSpanish(right);
if (leftIsSpecialSpanish && !rightIsSpecialSpanish && right != LATIN_AMERICAN_SPANISH) {
left = LATIN_AMERICAN_SPANISH;
} else if (rightIsSpecialSpanish
&& !leftIsSpecialSpanish
&& left != LATIN_AMERICAN_SPANISH) {
right = LATIN_AMERICAN_SPANISH;
}
int[] request_ancestors = new int[MAX_SCRIPT_PARENT_DEPTH + 1];
final Ref<Long> left_right_indexRef = new Ref<Long>(null);
// Find the parents of the request, but stop as soon as we saw left or right
final int left_and_right[] = {left, right};
final int ancestor_count =
findAncestors(
request_ancestors, left_right_indexRef,
request, requested_script,
left_and_right, sizeof(left_and_right));
if (left_right_indexRef.get() == 0) { // We saw left earlier
return 1;
}
if (left_right_indexRef.get() == 1) { // We saw right earlier
return -1;
}
// If we are here, neither left nor right are an ancestor of the
// request. This means that all the ancestors have been computed and
// the last ancestor is just the language by itself. We will use the
// distance in the parent tree for determining the better match.
final int left_distance =
findDistance(left, requested_script, request_ancestors, ancestor_count);
final int right_distance =
findDistance(right, requested_script, request_ancestors, ancestor_count);
if (left_distance != right_distance) {
return (int) right_distance - (int) left_distance; // smaller distance is better
}
// If we are here, left and right are equidistant from the request. We will
// try and see if any of them is a representative locale.
final boolean left_is_representative = isRepresentative(left, requested_script);
final boolean right_is_representative = isRepresentative(right, requested_script);
if (left_is_representative != right_is_representative) {
return (left_is_representative ? 1 : 0) - (right_is_representative ? 1 : 0);
}
// We have no way of figuring out which locale is a better match. For
// the sake of stability, we consider the locale with the lower region
// code (in dictionary order) better, with two-letter codes before
// three-digit codes (since two-letter codes are more specific).
return right - left;
}
static void localeDataComputeScript(byte[] out, final byte[] language, final byte[] region) {
if (language[0] == '\0') {
// memset(out, '\0', SCRIPT_LENGTH);
Arrays.fill(out, (byte) 0);
return;
}
int lookup_key = packLocale(language, region);
Byte lookup_result = getLIKELY_SCRIPTS().get(lookup_key);
if (lookup_result == null) {
// We couldn't find the locale. Let's try without the region
if (region[0] != '\0') {
lookup_key = dropRegion(lookup_key);
lookup_result = getLIKELY_SCRIPTS().get(lookup_key);
if (lookup_result != null) {
// memcpy(out, SCRIPT_CODES[lookup_result.second], SCRIPT_LENGTH);
System.arraycopy(getSCRIPT_CODES()[lookup_result], 0, out, 0, SCRIPT_LENGTH);
return;
}
}
// We don't know anything about the locale
// memset(out, '\0', SCRIPT_LENGTH);
Arrays.fill(out, (byte) 0);
return;
} else {
// We found the locale.
// memcpy(out, SCRIPT_CODES[lookup_result.second], SCRIPT_LENGTH);
System.arraycopy(getSCRIPT_CODES()[lookup_result], 0, out, 0, SCRIPT_LENGTH);
}
}
static final int[] ENGLISH_STOP_LIST = {
0x656E0000, // en
0x656E8400, // en-001
};
static final byte[] ENGLISH_CHARS = {'e', 'n'};
static final String LATIN_CHARS = "Latn";
static boolean localeDataIsCloseToUsEnglish(final byte[] region) {
final int locale = packLocale(ENGLISH_CHARS, region);
final Ref<Long> stop_list_indexRef = new Ref<>(null);
findAncestors(null, stop_list_indexRef, locale, LATIN_CHARS, ENGLISH_STOP_LIST, 2);
// A locale is like US English if we see "en" before "en-001" in its ancestor list.
return stop_list_indexRef.get() == 0; // 'en' is first in ENGLISH_STOP_LIST
}
private static int sizeof(int[] array) {
return array.length;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.aaptcompiler.android
class Ref<T>(private var t: T?) {
fun get(): T? = t
fun set(t: T) {
this.t = t
}
override fun toString(): String = "Ref<$t>"
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,659 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.aaptcompiler.android
import com.android.aaptcompiler.StringPool
import com.android.aaptcompiler.parseFloat
import java.nio.ByteBuffer
import java.nio.ByteOrder
import kotlin.text.Charsets
/**
* Definitions of resource data structures.
* <p>Transliterated from: *
* https://android.googlesource.com/platform/frameworks/base/+/android-9.0.0_r12/libs/androidfw/ResourceTypes.cpp
*/
/**
* Header that appears at the front of every data chunk in a resource.
*/
class ResChunkHeader(
// Type identifier for this chunk. The meaning of this value depends on the containing chunk.
val typeId: Short,
// Size of the chunk header (in bytes). Adding this value to the address of the chunk will be
// the address of the associated data if any.
val headerSize: Short) {
constructor(): this(0, 0)
// Total size of this chunk (in bytes). This is the chunkSize plus the size of any data
// associated with this chunk. Adding the this to the address of this chunk will completely skip
// its contents (including any child chunks).
var size: Int = 0
}
enum class ChunkType(val id: Short) {
NULL_TYPE(0x0000),
STRING_POOL_TYPE(0x0001),
TABLE_TYPE(0x0002),
XML_TYPE(0x0003),
// Marker for the beginning of XML types.
XML_FIRST_CHUNK_TYPE(0x0100),
// Child types of XML_TYPE
XML_START_NAMESPACE_TYPE(0x0100),
XML_END_NAMESPACE_TYPE(0x0101),
XML_START_ELEMENT_TYPE(0x0102),
XML_END_ELEMENT_TYPE(0x0103),
XML_CDATA_TYPE(0x0104),
XML_LAST_CHUNK_TYPE(0x017f),
// This contains a uint32_t array mapping strings in the string
// pool back to resource identifiers. It is optional.
XML_RESOURCE_MAP_TYPE(0x0180),
// Child types of TABLE_TYPE
TABLE_PACKAGE_TYPE(0x0200),
TABLE_TYPE_TYPE(0x0201),
TABLE_TYPE_SPEC_TYPE(0x0202),
TABLE_LIBRARY_TYPE(0x0203),
TABLE_OVERLAYABLE_TYPE(0x0204),
TABLE_OVERLAYABLE_POLICY_TYPE(0x0205),
}
/**
* Representation of a value in a resource, supplying type information.
*/
data class ResValue(val dataType: DataType, val data: Int, val size: Short = 0) {
constructor() : this(DataType.NULL, 0)
// type of the data value.
enum class DataType(val byteValue: Byte) {
// The 'data' is either 0 or 1, specifying this resource is undefined or empty, respectively.
NULL(0X00),
// The 'data' holds a ResTable reference, i.e. a reference to another resource.
REFERENCE(0x01),
// The 'data' holds an attribute resource identifier.
ATTRIBUTE(0x02),
// The 'data' holds an index into the containing resource table's string pool.
STRING(0x03),
// The 'data' holds a single-precision floating point number.
FLOAT(0x04),
// The 'data' holds a fixed point number encoding a dimension value, e.g. 100in.
DIMENSION(0x05),
// The 'data' holds a fixed point number encoding a fraction of a container.
FRACTION(0x06),
// The 'data' holds a dynamic ResTable reference, which needs to be resolved before it can be
// used as a REFERENCE.
DYNAMIC_REFERENCE(0x07),
// The 'data' holds an attribute resource identifier, which needs to be resolved before it can
// be used like a ATTRIBUTE.
DYNAMIC_ATTRIBUTE(0x08),
// The 'data' is a raw integer of the form n..n.
INT_DEC(0x10),
// The 'data' is a raw integer value of the form 0xn..n.
INT_HEX(0x11),
// The 'data' is either 0 or 1, for input 'false' or 'true' respectively.
INT_BOOLEAN(0x12),
// The 'data' is a raw integer value of the form #aarrggbb.
INT_COLOR_ARGB8(0x1c),
// The 'data' is a raw integer value of the form #rrggbb.
INT_COLOR_RGB8(0x1d),
// The 'data' is a raw integer value of the form #argb
INT_COLOR_ARGB4(0x1e),
// The 'data is a raw integer value of the form #rgb
INT_COLOR_RGB4(0x1f);
companion object {
val FIRST_INT = DataType.INT_DEC
val FIRST_COLOR_INT = DataType.INT_COLOR_ARGB8
val LAST_COLOR_INT = DataType.INT_COLOR_RGB4
val LAST_INT = LAST_COLOR_INT
}
}
// Structure of complex data values (TYPE_DIMENSION and TYPE_FRACTION)
object ComplexFormat {
// Where the unit type information is. This gives us 16 possible
// types, as defined below.
const val UNIT_SHIFT = 0
const val UNIT_MASK = 0xf
// TYPE_DIMENSION: Value is raw pixels.
const val UNIT_PX = 0
// TYPE_DIMENSION: Value is Device Independent Pixels.
const val UNIT_DIP = 1
// TYPE_DIMENSION: Value is a Scaled device independent Pixels.
const val UNIT_SP = 2
// TYPE_DIMENSION: Value is in points.
const val UNIT_PT = 3
// TYPE_DIMENSION: Value is in inches.
const val UNIT_IN = 4
// TYPE_DIMENSION: Value is in millimeters.
const val UNIT_MM = 5
// TYPE_FRACTION: A basic fraction of the overall size.
const val UNIT_FRACTION = 0
// TYPE_FRACTION: A fraction of the parent size.
const val UNIT_FRACTION_PARENT = 1
// Where the radix information is, telling where the decimal place
// appears in the mantissa. This give us 4 possible fixed point
// representations as defined below.
const val RADIX_SHIFT = 4
const val RADIX_MASK = 0x3
// The mantissa is an integral number -- i.e., 0xnnnnnn.0
const val RADIX_23p0 = 0
// The mantissa magnitude is 16 bits -- i.e, 0xnnnn.nn
const val RADIX_16p7 = 1
// The mantissa magnitude is 8 bits -- i.e, 0xnn.nnnn
const val RADIX_8p15 = 2
// The mantissa magnitude is 0 bits -- i.e, 0x0.nnnnnn
const val RADIX_0p23 = 3
// Where the actual value is. This gives us 23 bits of
// precision. The top bit is the sign.
const val MANTISSA_SHIFT = 8
const val MANTISSA_MASK = 0xffffff
}
// Possible Data values for DataType NULL.
object NullFormat {
const val UNDEFINED = 0
const val EMPTY = 1
}
}
/**
* Reference to a string in a string pool
*/
data class ResStringPoolRef(
// Index into the string pool table (unsigned 32 bit offset from the indices immediately after
// ResStringPoolHeader) at which to find the location of the string data in the pool
val index: Int)
/**
* Definition of a pool of strings.
*
* <p> The data of this chunk is an array of 32 bit unsigned integers providing indices into the
* pool, relative to the start of the string values. This start, {@code stringStart}, are of all the
* UTF-16 Strings concatenated together; each starts with a short representing the string's length
* and ends in a terminating 0x0000u character. If a string is > 32767 characters, the size will be
* stored as two 16 bit values, a high word and a low word, with the high bit set in the first value
* to indicate this format is being used.
*
* <p> If {@code styleCount} is not zero, then immediately following the array of indices into the
* string table is another array of indices into a style table starting at {@code stylesStart}.
* Each entry in the style table is an array of {@code ResStringPoolSpan} structures.
*/
data class ResStringPoolHeader(
val header: ResChunkHeader,
// Number of strings in this pool (number of 32 bit indices that follow in the data).
val stringCount: Int,
// Number of style span arrays in the pool (number of 32 bit indices that follow the string
// indices).
val styleCount: Int) {
constructor(): this(ResChunkHeader(), 0, 0)
var flags: Int = 0
// Index from header of the string data.
var stringsStart: Int = 0
// Index from header of the style data.
var stylesStart: Int = 0
companion object {
const val SORTED_FLAG = 1 shl 0
const val UTF8_FLAG = 1 shl 8
const val SIZE = 28.toShort()
}
}
/**
* This structure defines a span of style information associated with a string in the pool.
*/
data class ResStringPoolSpan(
// this is the name of the span -- that is, the name of the XML tag that defined it. The special
// value END indicates the end of an array of spans.
val name: ResStringPoolRef,
// The range of characters in the string that this span applies to.
val firstChar: Int,
val lastChar: Int
) {
companion object {
const val END = -1
const val SIZE = 12
}
}
/** Convenience class for accessing data in a String Pool Flattened Resource */
class ResStringPool private constructor(
val data: ByteBuffer,
val header: ResStringPoolHeader,
val stringPoolSize: Int,
val strings: List<String>,
val stylesPoolSize: Int,
val styles: List<List<ResStringPoolSpan>>) {
companion object {
fun get(buffer: ByteBuffer, length: Int): ResStringPool {
buffer.order(ByteOrder.nativeOrder())
if (length < ResStringPoolHeader.SIZE) {
error("Invalid StringPool: buffer too small to store a string pool.")
}
val typeId = buffer.getShort(0).deviceToHost()
val headerSize = buffer.getShort(2).deviceToHost()
val resourceSize = buffer.getInt(4).deviceToHost()
if (typeId != ChunkType.STRING_POOL_TYPE.id ||
headerSize != ResStringPoolHeader.SIZE ||
resourceSize < headerSize ||
resourceSize > length
) {
error("Invalid StringPool: Header has invalid format.")
}
// The size has been checked, so we can start reading the ResStringPoolHeader Fields.
val chunkHeader = ResChunkHeader(typeId, headerSize)
chunkHeader.size = resourceSize
val header =
ResStringPoolHeader(
chunkHeader, buffer.getInt(8).deviceToHost(), buffer.getInt(12).deviceToHost())
header.flags = buffer.getInt(16).deviceToHost()
header.stringsStart = buffer.getInt(20).deviceToHost()
header.stylesStart = buffer.getInt(24).deviceToHost()
var stringPoolSize = 0
val strings = mutableListOf<String>()
if (header.stringCount != 0) {
// we need to check overflow and ensure the string indexes can fit.
if (header.stringCount*4 < header.stringCount ||
(header.header.headerSize + (header.stringCount*4)) > resourceSize) {
error("Invalid StringPool: Buffer not large enough for string indices.")
}
val charSize = if ((header.flags and ResStringPoolHeader.UTF8_FLAG) != 0) 1 else 2
// There should at least be enough space for the smallest string.
// (2 bytes length, null terminator)
if (header.stringsStart+2 >= resourceSize) {
error("Invalid StringPool: Buffer not large enough for strings.")
}
if (header.styleCount == 0) {
stringPoolSize = (resourceSize - header.stringsStart) / charSize
} else {
// check invariant: styles starts before end of data
if (header.stylesStart >= resourceSize - 2) {
error("Invalid StringPool: Style start specified in header too large.")
}
// check invariant: styles follow strings
if (header.stylesStart <= header.stringsStart) {
error("Invalid StringPool: ")
}
stringPoolSize = (header.stylesStart - header.stringsStart) / charSize
}
if (stringPoolSize ==0) {
error("Invalid StringPool: Space for strings in header is too small.")
}
var currentStringIndex = headerSize.toInt()
for (i in 0.until(header.stringCount)) {
val stringLocation =
buffer.getInt(currentStringIndex).deviceToHost() + header.stringsStart
currentStringIndex += 4
val string = decodeString(buffer, stringLocation, charSize == 1)
strings.add(string)
}
}
var stylePoolSize = 0
val styles = mutableListOf<List<ResStringPoolSpan>>()
if (header.styleCount != 0) {
var currentStyleIndex = headerSize.toInt() + header.stringCount*4
// invariant: integer overflow in calculating styles
if (currentStyleIndex < headerSize.toInt()) {
error("Invalid StringPool: Integer overflow encountered while decoding styles.")
}
stylePoolSize = (resourceSize - header.stylesStart)/4
for (i in 0.until(header.styleCount)) {
val styleLocation = buffer.getInt(currentStyleIndex).deviceToHost() + header.stylesStart
currentStyleIndex += 4
val style = decodeStyle(buffer, styleLocation)
styles.add(style)
}
}
return ResStringPool(buffer, header, stringPoolSize, strings, stylePoolSize, styles)
}
private fun decodeString(buffer: ByteBuffer, location: Int, utf8: Boolean): String {
var stringPosition = location
if (utf8) {
// In UTF8 mode, the length of UTF16 comes first)
val firstByteUTF16 = buffer.get(stringPosition)
++stringPosition
val utf16Length = when {
(firstByteUTF16.toInt() and StringPool.TWO_BYTE_UTF8_LENGTH_SIGNIFIER) != 0 -> {
val secondByte = buffer.get(stringPosition).toInt() and 0xff
++stringPosition
((firstByteUTF16.toInt() shl 8) + secondByte) and StringPool.UTF8_ENCODE_LENGTH_MAX
}
else -> firstByteUTF16.toInt()
}
// In UTF8 mode, the length in UTF8 comes next
val firstByteUTF8 = buffer.get(stringPosition)
++stringPosition
val utf8Length = when {
(firstByteUTF8.toInt() and StringPool.TWO_BYTE_UTF8_LENGTH_SIGNIFIER) != 0 -> {
val secondByte = buffer.get(stringPosition).toInt() and 0xff
++stringPosition
((firstByteUTF8.toInt() shl 8) + secondByte) and StringPool.UTF8_ENCODE_LENGTH_MAX
}
else -> firstByteUTF8.toInt()
}
// pull the bytes out of the buffer.
val array = ByteArray(utf8Length)
for (i in 0.until(utf8Length)) {
array[i] = buffer.get(stringPosition)
++stringPosition
}
// ensure the string is null terminated
if (buffer.get(stringPosition) != 0.toByte()) {
error("Invalid StringPool: UTF8 string value is not null-terminated.")
}
val utf16String = String(array, Charsets.UTF_8)
// ensure the utf16 size is correct
if (utf16String.length != utf16Length) {
error("Invalid StringPool: specified UTF16 does not match actual string length.")
}
return utf16String
} else {
// first get the utf16 length, as that is the first component regardless of mode.
val firstShort = buffer.getShort(stringPosition).deviceToHost()
stringPosition += 2
val utf16Length = when {
(firstShort.toInt() and StringPool.TWO_CHAR_UTF16_LENGTH_SIGNIFIER) != 0 -> {
val secondShort = buffer.getShort(stringPosition).deviceToHost().toInt() and 0xffff
stringPosition += 2
((firstShort.toInt() shl 16) + secondShort) and StringPool.UTF16_ENCODE_LENGTH_MAX
}
else -> firstShort.toInt()
}
// pull the chars out of the buffer
val array = CharArray(utf16Length)
for (i in 0.until(utf16Length)) {
array[i] = buffer.getChar(stringPosition).deviceToHost()
stringPosition += 2
}
// ensure the string is null terminated
if (buffer.getChar(stringPosition) != 0.toChar()) {
error("Invalid StringPool: UTF16 string value is not null-terminated.")
}
return String(array)
}
}
private fun decodeStyle(buffer: ByteBuffer, location: Int): List<ResStringPoolSpan> {
var currentIndex = location
val result = mutableListOf<ResStringPoolSpan>()
while (true) {
val refIndex = buffer.getInt(currentIndex).deviceToHost()
if (refIndex == ResStringPoolSpan.END) {
break
}
val firstChar = buffer.getInt(currentIndex + 4).deviceToHost()
val lastChar = buffer.getInt(currentIndex + 8).deviceToHost()
currentIndex += 12
result.add(ResStringPoolSpan(ResStringPoolRef(refIndex), firstChar, lastChar))
}
return result
}
}
}
fun parseHex(codePoint: Int) =
when(codePoint) {
in '0'.toInt()..'9'.toInt() -> codePoint - '0'.toInt()
in 'A'.toInt()..'F'.toInt() -> codePoint - 'A'.toInt() + 10
in 'a'.toInt()..'f'.toInt() -> codePoint - 'a'.toInt() + 10
else -> -1
}
fun stringToInt(string: String): ResValue? {
val trimmedString = string.trimStart()
if (trimmedString.isEmpty()) {
return null
}
val codePointCount = trimmedString.codePointCount(0, trimmedString.length)
var index = 0
var value = 0L
var isNegative = false
if (trimmedString.codePointAt(0) == '-'.toInt()) {
++index
isNegative = true
}
if (codePointCount == index
|| trimmedString.codePointAt(index) !in '0'.toInt()..'9'.toInt()) {
return null
}
val isHex: Boolean
if (codePointCount >= index + 2
&& trimmedString.codePointAt(index) == '0'.toInt()
&& trimmedString.codePointAt(index+1) == 'x'.toInt()) {
isHex = true
index += 2
if (isNegative) {
// Hex values are not allowed to be negative.
return null
}
if (index == codePointCount) {
// Just "0x", not a valid format.
return null
}
while (index < codePointCount) {
val hexValue = parseHex(trimmedString.codePointAt(index))
if (hexValue == -1) {
return null
}
++index
value = (value shl 4) + hexValue
if (value > 0xffffffffL) {
return null
}
}
} else {
isHex = false
while (index < codePointCount) {
val codePoint = trimmedString.codePointAt(index)
if (codePoint !in '0'.toInt()..'9'.toInt()) {
return null
}
++index
val decValue = codePoint - '0'.toInt()
value = (value*10) + decValue
val outOfBounds =
if (isNegative) -value < Int.MIN_VALUE.toLong() else value > Int.MAX_VALUE.toLong()
if (outOfBounds) {
return null
}
}
}
if (isNegative) {
value = -value
}
return ResValue(
if(isHex) ResValue.DataType.INT_HEX else ResValue.DataType.INT_DEC,
value.toInt())
}
private data class UnitEntry(
val name: String, val dataType: ResValue.DataType, val unitValue: Int, val scale: Float = 1.0f)
private val unitSuffixes = listOf(
UnitEntry("px", ResValue.DataType.DIMENSION, ResValue.ComplexFormat.UNIT_PX),
UnitEntry("dip", ResValue.DataType.DIMENSION, ResValue.ComplexFormat.UNIT_DIP),
UnitEntry("dp", ResValue.DataType.DIMENSION, ResValue.ComplexFormat.UNIT_DIP),
UnitEntry("sp", ResValue.DataType.DIMENSION, ResValue.ComplexFormat.UNIT_SP),
UnitEntry("pt", ResValue.DataType.DIMENSION, ResValue.ComplexFormat.UNIT_PT),
UnitEntry("in", ResValue.DataType.DIMENSION, ResValue.ComplexFormat.UNIT_IN),
UnitEntry("mm", ResValue.DataType.DIMENSION, ResValue.ComplexFormat.UNIT_MM),
UnitEntry("%", ResValue.DataType.FRACTION, ResValue.ComplexFormat.UNIT_FRACTION, 1.0f/100),
UnitEntry(
"%p", ResValue.DataType.FRACTION, ResValue.ComplexFormat.UNIT_FRACTION_PARENT, 1.0f/100))
private fun parseUnitType(string: String) : UnitEntry? {
for (entry in unitSuffixes) {
if (string.endsWith(entry.name)) {
return entry
}
}
return null
}
fun stringToFloat(string: String): ResValue? {
val trimmedString = string.trim()
if (trimmedString.isEmpty() || trimmedString.length > 128) {
return null
}
// need to find the unit type suffix first, so we can trim it and interpret the rest as a float
val entry = parseUnitType(trimmedString)
val suffixIndex =
if (entry != null) trimmedString.lastIndexOf(entry.name) else trimmedString.length
// no spaces allowed between suffix and floating point value
if (suffixIndex == 0 || Character.isWhitespace(trimmedString.codePointAt(suffixIndex-1))) {
return null
}
var stringToParse = trimmedString.substring(0, suffixIndex)
// We have an issue where the suffix could be considered part of a hex floating point number
// and would result in a valid dimension value which is normally considered invalid.
// i.e "0x1d.dp" should be considered: "0x1d.d p" not "0x1d. dp"
if (parseHex(entry?.name?.codePointAt(0) ?: 0) != -1) {
// try to parse the float value with the beginning of the suffix considered.
if (trimmedString.contains("0x", true) &&
!stringToParse.contains('p')) {
// ambiguous value string, resulting in a parse failure.
return null
}
}
// attempt fast floating point parsing first
var parsedValue = parseFloat(stringToParse)
// Resort to back up parsing if the fast parsing fails to create a floating point value.
if (parsedValue == null) {
// Kotlin does not parse hex floating point numbers that do not have exponent identifier. I.e.
// "0x23.b" does not parse. "0x23.bp0" will. So we append "p0" where we need it, to enforce
// consistency with C++ floating point parsing.
if (stringToParse.contains("0x", true) && !stringToParse.contains('p', true)) {
stringToParse += "p0"
}
parsedValue = stringToParse.toFloatOrNull()
parsedValue ?: return null
}
if (entry != null) {
// Treat as a Unit.
parsedValue *= entry.scale
val negative = parsedValue < 0
if (negative) {
parsedValue = -parsedValue
}
// Transform the float to (rounded) integer with the lower 23 bits representing the fraction
// and bits 45 to 23 representing the whole number part.
val bits = (parsedValue*(1 shl 23) + .5f).toLong()
val (radix, shift) = when {
bits and ((1L shl 23) - 1) == 0L ->
// Always use 23p0 if there is no fraction, as it is easier to read.
Pair(ResValue.ComplexFormat.RADIX_23p0, 23)
bits and ((1L shl 23) - 1).inv() == 0L ->
// Whole number part is zero -- can fit into 0 leading bits of precision.
Pair(ResValue.ComplexFormat.RADIX_0p23, 0)
bits and ((1L shl 31) -1).inv() == 0L ->
// Magnitude can fit in 8 leading bits of precision.
Pair(ResValue.ComplexFormat.RADIX_8p15, 8)
bits and ((1L shl 39) -1).inv() == 0L ->
// Magnitude can fit in 16 leading bits of precision.
Pair(ResValue.ComplexFormat.RADIX_16p7, 16)
else ->
// Need whole range, so no fractional part.
Pair(ResValue.ComplexFormat.RADIX_23p0, 23)
}
var mantissa = (bits ushr shift).toInt() and ResValue.ComplexFormat.MANTISSA_MASK
if (negative) {
mantissa = (-mantissa) and ResValue.ComplexFormat.MANTISSA_MASK
}
val dataValue = (entry.unitValue shl ResValue.ComplexFormat.UNIT_SHIFT) or
(radix shl ResValue.ComplexFormat.RADIX_SHIFT) or
(mantissa shl ResValue.ComplexFormat.MANTISSA_SHIFT)
return ResValue(entry.dataType, dataValue)
}
return ResValue(ResValue.DataType.FLOAT, parsedValue.toRawBits())
}
@@ -0,0 +1,73 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.aaptcompiler.android
import java.nio.ByteOrder
private val littleEndian = ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN
private val DEBUG = false
/**
* Changes the byte order of the short (on device) to the byte order of the host.
*/
fun Short.deviceToHost() = if (littleEndian) this else java.lang.Short.reverseBytes(this)
/**
* Changes the byte order of the char on device to the byte order of the host.
*/
fun Char.deviceToHost() = if (littleEndian) this else java.lang.Character.reverseBytes(this)
/**
* Changes the byte order of the int on device to the byte order of the host.
*/
fun Int.deviceToHost() = if (littleEndian) this else java.lang.Integer.reverseBytes(this)
/**
* Changes the byte order of the short on host to the byte order of the device.
*/
fun Short.hostToDevice() = if (littleEndian) this else java.lang.Short.reverseBytes(this)
/**
* Changes the byte order of the char on host to the byte order of the device.
*/
fun Char.hostToDevice() = if (littleEndian) this else java.lang.Character.reverseBytes(this)
/**
* Changes the byte order of the int on host to the byte order of the device.
*/
fun Int.hostToDevice() = if (littleEndian) this else java.lang.Integer.reverseBytes(this)
/**
* Returns true if the int reflects a "truthy" value in C++ for use in conditionals.
*/
fun Int.isTruthy() = this != 0
/**
* Returns true if the short reflects a "truthy" value in C++ for use in conditionals.
*/
fun Short.isTruthy() = this != 0.toShort()
/**
* Returns true if the byte reflects a "truthy" value in C++ for use in conditionals.
*/
fun Byte.isTruthy() = this != 0.toByte()
/**
* Returns true if the "pointer" reflects a "truthy" value in C++ for use in conditionals.
*/
fun Any?.isTruthy() = this != null
@@ -0,0 +1,188 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.aaptcompiler.buffer
import java.nio.ByteBuffer
import java.nio.ByteOrder
import kotlin.IndexOutOfBoundsException
import kotlin.math.max
/**
* Inspired by protobuf's ZeroCopyOutputStream, offers blocks of memory in which to write without
* knowing the full size of the entire payload. At the core of BigBuffer, it is a list of memory
* blocks. As one fills up, another block is allocated and appended to the end of the list.
*
* <p> This class is primarily used to flatten StringPool blobs which do not their size, until after
* it has been written.
*
* @property blockSize the minimum block size per block. If an entry to the BigBuffer through a
* call to [nextBlock] would be larger than this, a block will be created large enough to fit the
* entry.
*/
class BigBuffer(val blockSize: Int = 1024) {
var size: Int = 0
private set
internal val blocks = mutableListOf<Block>()
internal class Block(
internal var size: Int, internal val blockSize: Int, internal val data: ByteBuffer)
data class BlockRef internal constructor(
internal val start: Int, val size: Int, internal val block: Block) {
fun writeByte(value: Byte, location: Int) {
if (location + 1 > size) {
throw IndexOutOfBoundsException()
}
val actualLocation = start + location
block.data.put(actualLocation, value)
}
fun writeShort(value: Short, location: Int) {
if (location + 2 > size) {
throw IndexOutOfBoundsException()
}
val actualLocation = start + location
block.data.putShort(actualLocation, value)
}
fun writeInt(value: Int, location: Int) {
if (location + 4 > size) {
throw IndexOutOfBoundsException()
}
val actualLocation = start + location
block.data.putInt(actualLocation, value)
}
fun readByte(location: Int): Byte {
if (location + 1 > size) {
throw IndexOutOfBoundsException()
}
val actualLocation = start + location
return block.data.get(actualLocation)
}
fun readShort(location: Int): Short {
if (location + 2 > size) {
throw IndexOutOfBoundsException()
}
val actualLocation = start + location
return block.data.getShort(actualLocation)
}
fun readInt(location: Int): Int {
if (location + 4 > size) {
throw IndexOutOfBoundsException()
}
val actualLocation = start + location
return block.data.getInt(actualLocation)
}
}
/**
* Retrieves a block ref large enough to contain an entry with element size. Allocated a new block
* if necessary.
*
* @param elementSize the size of the entry that needs to be created.
* @return a reference to a portion of a block of the required size. Writing to this reference
* will modify the underlying [BigBuffer].
*/
fun nextBlock(elementSize: Int): BlockRef {
if (blocks.isNotEmpty()) {
val back = blocks.last()
// We still have room in the last block so we can use it.
if (back.blockSize - back.size >= elementSize) {
val start = back.size
back.size += elementSize
size += elementSize
return BlockRef(start, elementSize, back)
}
}
val actualSize = max(this.blockSize, elementSize)
val block = Block(
elementSize,
actualSize,
ByteBuffer.wrap(ByteArray(actualSize)).order(ByteOrder.nativeOrder()))
blocks.add(block)
size += elementSize
return BlockRef(0, elementSize, block)
}
/**
* Creates an empty entry of the given size.
*
* @param bytes the number of bytes to be padded.
*/
fun pad(bytes: Int) {
nextBlock(bytes)
}
/**
* Pads the [BigBuffer] so that the next created entry is aligned to a 4 byte chunk.
*/
fun align4() {
val unaligned = size % 4
if (unaligned != 0) {
pad(4-unaligned)
}
}
/**
* Retrieves a reference to the block on the given index.
*
* @param index the index of the block to be retrieved.
* @return a reference to this block.
*
* <p> It is not guaranteed, nor is it expected that the {@code n}th block will be equivalent to
* the {@code n}th entry. This is because multiple entries, if small enough, will be collected
* to the same block.
*/
fun block(index: Int): BlockRef {
return BlockRef(0, blocks[index].size, blocks[index])
}
/**
* Appends the given BigBuffer to the current blocks. The given buffer will be empty after this
* operation is complete.
*
* @param other the [BigBuffer] to be appended to the current buffer. This buffer will be empty,
* after this method is complete.
*/
fun append(other: BigBuffer) {
blocks.addAll(other.blocks)
size += other.size
other.blocks.clear()
other.size = 0
}
/**
* Returns the contents of the [BigBuffer] as one cohesive array of bytes.
*/
fun toBytes(): ByteArray {
val array = ByteArray(size)
var currentLocation = 0
for (block in blocks) {
val data = block.data.position(0) as ByteBuffer
data.get(array, currentLocation, block.size)
currentLocation += block.size
}
return array
}
}
@@ -0,0 +1,806 @@
package com.android.aaptcompiler.proto
import com.android.aapt.ConfigurationOuterClass
import com.android.aapt.Resources
import com.android.aaptcompiler.AllowNew
import com.android.aaptcompiler.ArrayResource
import com.android.aaptcompiler.AttributeResource
import com.android.aaptcompiler.BasicString
import com.android.aaptcompiler.BinaryPrimitive
import com.android.aaptcompiler.ConfigDescription
import com.android.aaptcompiler.FileReference
import com.android.aaptcompiler.Id
import com.android.aaptcompiler.Item
import com.android.aaptcompiler.LocaleValue
import com.android.aaptcompiler.Macro
import com.android.aaptcompiler.Overlayable
import com.android.aaptcompiler.OverlayableItem
import com.android.aaptcompiler.Plural
import com.android.aaptcompiler.RawString
import com.android.aaptcompiler.Reference
import com.android.aaptcompiler.ResourceFile
import com.android.aaptcompiler.ResourceName
import com.android.aaptcompiler.ResourceTable
import com.android.aaptcompiler.Source
import com.android.aaptcompiler.Span
import com.android.aaptcompiler.StringPool
import com.android.aaptcompiler.Style
import com.android.aaptcompiler.StyleString
import com.android.aaptcompiler.Styleable
import com.android.aaptcompiler.StyledString
import com.android.aaptcompiler.UntranslatableSection
import com.android.aaptcompiler.Value
import com.android.aaptcompiler.Visibility
import com.android.aaptcompiler.android.ResStringPool
import com.android.aaptcompiler.android.ResTableConfig
import com.android.aaptcompiler.android.ResValue
import com.android.aaptcompiler.android.deviceToHost
import com.android.aaptcompiler.android.isTruthy
import com.android.aaptcompiler.isValidId
import com.android.aaptcompiler.parseResourceName
import com.android.aaptcompiler.resourceIdFromParts
import com.android.aaptcompiler.resourceTypeFromTag
import com.itsaky.androidide.layoutlib.resources.ResourceVisibility
import com.android.utils.ILogger
fun deserializeConfigFromPb(
config: ConfigurationOuterClass.Configuration, logger: ILogger?): ConfigDescription? {
val mcc = config.getMcc().toShort()
val mnc = config.getMnc().toShort()
val localeString = config.getLocale()
val localeValue = LocaleValue()
if (localeString.isNotEmpty()) {
if (!localeValue.initFromBcp47Tag(localeString)) {
logger?.error(null, "Failed to initialized from Bcp47 '%s'", localeString)
return null
}
}
val screenLayoutDir = when (config.getLayoutDirection()) {
ConfigurationOuterClass.Configuration.LayoutDirection.LAYOUT_DIRECTION_LTR ->
ResTableConfig.SCREEN_LAYOUT.DIR_LTR
ConfigurationOuterClass.Configuration.LayoutDirection.LAYOUT_DIRECTION_RTL ->
ResTableConfig.SCREEN_LAYOUT.DIR_RTL
else -> ResTableConfig.SCREEN_LAYOUT.DIR_ANY
}
val smallestScreenWidthDp = config.getSmallestScreenWidthDp()
val screenWidthDp = config.getScreenWidthDp()
val screenHeightDp = config.getScreenHeightDp()
val screenLayoutSize = when (config.getScreenLayoutSize()) {
ConfigurationOuterClass.Configuration.ScreenLayoutSize.SCREEN_LAYOUT_SIZE_SMALL ->
ResTableConfig.SCREEN_LAYOUT.SIZE_SMALL
ConfigurationOuterClass.Configuration.ScreenLayoutSize.SCREEN_LAYOUT_SIZE_NORMAL ->
ResTableConfig.SCREEN_LAYOUT.SIZE_NORMAL
ConfigurationOuterClass.Configuration.ScreenLayoutSize.SCREEN_LAYOUT_SIZE_LARGE ->
ResTableConfig.SCREEN_LAYOUT.SIZE_LARGE
ConfigurationOuterClass.Configuration.ScreenLayoutSize.SCREEN_LAYOUT_SIZE_XLARGE ->
ResTableConfig.SCREEN_LAYOUT.SIZE_XLARGE
else -> ResTableConfig.SCREEN_LAYOUT.SIZE_ANY
}
val screenLayoutLong = when (config.getScreenLayoutLong()) {
ConfigurationOuterClass.Configuration.ScreenLayoutLong.SCREEN_LAYOUT_LONG_LONG ->
ResTableConfig.SCREEN_LAYOUT.SCREENLONG_YES
ConfigurationOuterClass.Configuration.ScreenLayoutLong.SCREEN_LAYOUT_LONG_NOTLONG ->
ResTableConfig.SCREEN_LAYOUT.SCREENLONG_NO
else -> ResTableConfig.SCREEN_LAYOUT.SCREENLONG_ANY
}
val screenRound = when (config.getScreenRound()) {
ConfigurationOuterClass.Configuration.ScreenRound.SCREEN_ROUND_ROUND ->
ResTableConfig.SCREEN_LAYOUT2.SCREENROUND_YES
ConfigurationOuterClass.Configuration.ScreenRound.SCREEN_ROUND_NOTROUND ->
ResTableConfig.SCREEN_LAYOUT2.SCREENROUND_NO
else -> ResTableConfig.SCREEN_LAYOUT2.SCREENROUND_ANY
}
val wideGamutColor = when (config.getWideColorGamut()) {
ConfigurationOuterClass.Configuration.WideColorGamut.WIDE_COLOR_GAMUT_WIDECG ->
ResTableConfig.COLOR_MODE.WIDE_GAMUT_YES
ConfigurationOuterClass.Configuration.WideColorGamut.WIDE_COLOR_GAMUT_NOWIDECG ->
ResTableConfig.COLOR_MODE.WIDE_GAMUT_NO
else -> ResTableConfig.COLOR_MODE.WIDE_GAMUT_ANY
}
val hdr = when (config.getHdr()) {
ConfigurationOuterClass.Configuration.Hdr.HDR_HIGHDR -> ResTableConfig.COLOR_MODE.HDR_YES
ConfigurationOuterClass.Configuration.Hdr.HDR_LOWDR -> ResTableConfig.COLOR_MODE.HDR_NO
else -> ResTableConfig.COLOR_MODE.HDR_ANY
}
val orientation = when (config.getOrientation()) {
ConfigurationOuterClass.Configuration.Orientation.ORIENTATION_PORT ->
ResTableConfig.ORIENTATION.PORT
ConfigurationOuterClass.Configuration.Orientation.ORIENTATION_LAND ->
ResTableConfig.ORIENTATION.LAND
ConfigurationOuterClass.Configuration.Orientation.ORIENTATION_SQUARE ->
ResTableConfig.ORIENTATION.SQUARE
else -> ResTableConfig.ORIENTATION.ANY
}
val uiModeType = when (config.getUiModeType()) {
ConfigurationOuterClass.Configuration.UiModeType.UI_MODE_TYPE_NORMAL ->
ResTableConfig.UI_MODE.TYPE_NORMAL
ConfigurationOuterClass.Configuration.UiModeType.UI_MODE_TYPE_DESK ->
ResTableConfig.UI_MODE.TYPE_DESK
ConfigurationOuterClass.Configuration.UiModeType.UI_MODE_TYPE_CAR ->
ResTableConfig.UI_MODE.TYPE_CAR
ConfigurationOuterClass.Configuration.UiModeType.UI_MODE_TYPE_TELEVISION ->
ResTableConfig.UI_MODE.TYPE_TELEVISION
ConfigurationOuterClass.Configuration.UiModeType.UI_MODE_TYPE_APPLIANCE ->
ResTableConfig.UI_MODE.TYPE_APPLIANCE
ConfigurationOuterClass.Configuration.UiModeType.UI_MODE_TYPE_WATCH ->
ResTableConfig.UI_MODE.TYPE_WATCH
ConfigurationOuterClass.Configuration.UiModeType.UI_MODE_TYPE_VRHEADSET ->
ResTableConfig.UI_MODE.TYPE_VR_HEADSET
else -> ResTableConfig.UI_MODE.TYPE_ANY
}
val uiModeNight = when (config.getUiModeNight()) {
ConfigurationOuterClass.Configuration.UiModeNight.UI_MODE_NIGHT_NIGHT ->
ResTableConfig.UI_MODE.NIGHT_YES
ConfigurationOuterClass.Configuration.UiModeNight.UI_MODE_NIGHT_NOTNIGHT ->
ResTableConfig.UI_MODE.NIGHT_NO
else -> ResTableConfig.UI_MODE.NIGHT_ANY
}
val density = config.getDensity()
val touchscreen = when (config.getTouchscreen()) {
ConfigurationOuterClass.Configuration.Touchscreen.TOUCHSCREEN_NOTOUCH ->
ResTableConfig.TOUCHSCREEN.NOTOUCH
ConfigurationOuterClass.Configuration.Touchscreen.TOUCHSCREEN_STYLUS ->
ResTableConfig.TOUCHSCREEN.STYLUS
ConfigurationOuterClass.Configuration.Touchscreen.TOUCHSCREEN_FINGER ->
ResTableConfig.TOUCHSCREEN.FINGER
else -> ResTableConfig.TOUCHSCREEN.ANY
}
val keysHidden = when (config.getKeysHidden()) {
ConfigurationOuterClass.Configuration.KeysHidden.KEYS_HIDDEN_KEYSEXPOSED ->
ResTableConfig.INPUT_FLAGS.KEYSHIDDEN_NO
ConfigurationOuterClass.Configuration.KeysHidden.KEYS_HIDDEN_KEYSHIDDEN ->
ResTableConfig.INPUT_FLAGS.KEYSHIDDEN_YES
ConfigurationOuterClass.Configuration.KeysHidden.KEYS_HIDDEN_KEYSSOFT ->
ResTableConfig.INPUT_FLAGS.KEYSHIDDEN_SOFT
else -> ResTableConfig.INPUT_FLAGS.KEYSHIDDEN_ANY
}
val keyboard = when (config.getKeyboard()) {
ConfigurationOuterClass.Configuration.Keyboard.KEYBOARD_NOKEYS ->
ResTableConfig.KEYBOARD.NOKEYS
ConfigurationOuterClass.Configuration.Keyboard.KEYBOARD_QWERTY ->
ResTableConfig.KEYBOARD.QWERTY
ConfigurationOuterClass.Configuration.Keyboard.KEYBOARD_TWELVEKEY ->
ResTableConfig.KEYBOARD.TWELVEKEY
else -> ResTableConfig.KEYBOARD.ANY
}
val navHidden = when (config.getNavHidden()) {
ConfigurationOuterClass.Configuration.NavHidden.NAV_HIDDEN_NAVHIDDEN ->
ResTableConfig.INPUT_FLAGS.NAVHIDDEN_YES
ConfigurationOuterClass.Configuration.NavHidden.NAV_HIDDEN_NAVEXPOSED ->
ResTableConfig.INPUT_FLAGS.NAVHIDDEN_NO
else -> ResTableConfig.INPUT_FLAGS.NAVHIDDEN_ANY
}
val navigation = when (config.getNavigation()) {
ConfigurationOuterClass.Configuration.Navigation.NAVIGATION_NONAV ->
ResTableConfig.NAVIGATION.NONAV
ConfigurationOuterClass.Configuration.Navigation.NAVIGATION_DPAD ->
ResTableConfig.NAVIGATION.DPAD
ConfigurationOuterClass.Configuration.Navigation.NAVIGATION_TRACKBALL ->
ResTableConfig.NAVIGATION.TRACKBALL
ConfigurationOuterClass.Configuration.Navigation.NAVIGATION_WHEEL ->
ResTableConfig.NAVIGATION.WHEEL
else -> ResTableConfig.NAVIGATION.ANY
}
val screenWidth = config.getScreenWidth()
val screenHeight = config.getScreenHeight()
val sdkVersion = config.getSdkVersion().toShort()
val configDescription = ConfigDescription(
ResTableConfig(
mcc = mcc,
mnc = mnc,
orientation = orientation,
touchscreen = touchscreen,
density = density,
keyboard = keyboard,
navigation = navigation,
inputFlags = (keysHidden.toInt() or navHidden.toInt()).toByte(),
screenWidth = screenWidth,
screenHeight = screenHeight,
sdkVersion = sdkVersion,
screenLayout = (screenLayoutDir.toInt() or
screenLayoutLong.toInt() or
screenLayoutSize.toInt()).toByte(),
uiMode = (uiModeType.toInt() or uiModeNight.toInt()).toByte(),
smallestScreenWidthDp = smallestScreenWidthDp,
screenWidthDp = screenWidthDp,
screenHeightDp = screenHeightDp,
screenLayout2 = screenRound,
colorMode = (wideGamutColor.toInt() or hdr.toInt()).toByte()))
localeValue.writeTo(configDescription)
return configDescription
}
fun deserializeFileTypeFromPb(type: Resources.FileReference.Type) = when (type) {
Resources.FileReference.Type.BINARY_XML -> ResourceFile.Type.BinaryXml
Resources.FileReference.Type.PROTO_XML -> ResourceFile.Type.ProtoXml
Resources.FileReference.Type.PNG -> ResourceFile.Type.Png
else -> ResourceFile.Type.Unknown
}
fun deserializeReferenceTypeFromPb(type: Resources.Reference.Type) = when (type) {
Resources.Reference.Type.ATTRIBUTE -> Reference.Type.ATTRIBUTE
else -> Reference.Type.RESOURCE
}
fun deserializeSourceFromPb(source: Resources.Source, sourcePool: ResStringPool): Source {
val path = sourcePool.strings[source.getPathIdx()]
val line = source.getPosition().getLineNumber()
return Source(path, line)
}
fun deserializeVisibilityFromPb(level: Resources.Visibility.Level) = when(level) {
Resources.Visibility.Level.PRIVATE -> ResourceVisibility.PRIVATE
Resources.Visibility.Level.PUBLIC -> ResourceVisibility.PUBLIC
else -> ResourceVisibility.UNDEFINED
}
fun deserializeOverlayableFromPb(
item: Resources.OverlayableItem,
overlayable: Overlayable,
sourcePool: ResStringPool,
logger: ILogger?): OverlayableItem? {
var policies = 0
for (policy in item.getPolicyList()) {
policies = policies or when (policy) {
Resources.OverlayableItem.Policy.PUBLIC -> OverlayableItem.Policy.PUBLIC
Resources.OverlayableItem.Policy.SYSTEM -> OverlayableItem.Policy.SYSTEM
Resources.OverlayableItem.Policy.VENDOR -> OverlayableItem.Policy.VENDOR
Resources.OverlayableItem.Policy.PRODUCT -> OverlayableItem.Policy.PRODUCT
Resources.OverlayableItem.Policy.SIGNATURE -> OverlayableItem.Policy.SIGNATURE
Resources.OverlayableItem.Policy.ODM -> OverlayableItem.Policy.ODM
Resources.OverlayableItem.Policy.OEM -> OverlayableItem.Policy.OEM
else -> {
logger?.error(null, "Unrecognized policy: %s.", policy)
return null
}
}
}
val source = if (item.hasSource()) {
deserializeSourceFromPb(item.getSource(), sourcePool)
} else {
Source.EMPTY
}
val comment = item.getComment()
return OverlayableItem(overlayable, policies, comment, source)
}
fun deserializeBinPrimitiveFromPb(
primitive: Resources.Primitive, logger: ILogger?): BinaryPrimitive? {
val (type, deviceValue) = when(primitive.getOneofValueCase()) {
Resources.Primitive.OneofValueCase.NULL_VALUE ->
Pair(ResValue.DataType.NULL, ResValue.NullFormat.UNDEFINED)
Resources.Primitive.OneofValueCase.EMPTY_VALUE ->
Pair(ResValue.DataType.NULL, ResValue.NullFormat.EMPTY)
Resources.Primitive.OneofValueCase.FLOAT_VALUE -> {
val float = primitive.getFloatValue()
Pair(ResValue.DataType.FLOAT, float.toRawBits())
}
Resources.Primitive.OneofValueCase.DIMENSION_VALUE ->
Pair(ResValue.DataType.DIMENSION, primitive.getDimensionValue())
Resources.Primitive.OneofValueCase.FRACTION_VALUE ->
Pair(ResValue.DataType.FRACTION, primitive.getFractionValue())
Resources.Primitive.OneofValueCase.INT_DECIMAL_VALUE ->
Pair(ResValue.DataType.INT_DEC, primitive.getIntDecimalValue())
Resources.Primitive.OneofValueCase.INT_HEXADECIMAL_VALUE ->
Pair(ResValue.DataType.INT_HEX, primitive.getIntHexadecimalValue())
Resources.Primitive.OneofValueCase.BOOLEAN_VALUE -> {
val boolValue = if (primitive.getBooleanValue()) 0xffffffff.toInt() else 0
Pair(ResValue.DataType.INT_BOOLEAN, boolValue)
}
Resources.Primitive.OneofValueCase.COLOR_ARGB8_VALUE ->
Pair(ResValue.DataType.INT_COLOR_ARGB8, primitive.getColorArgb8Value())
Resources.Primitive.OneofValueCase.COLOR_RGB8_VALUE ->
Pair(ResValue.DataType.INT_COLOR_RGB8, primitive.getColorRgb8Value())
Resources.Primitive.OneofValueCase.COLOR_ARGB4_VALUE ->
Pair(ResValue.DataType.INT_COLOR_ARGB4, primitive.getColorArgb4Value())
Resources.Primitive.OneofValueCase.COLOR_RGB4_VALUE ->
Pair(ResValue.DataType.INT_COLOR_RGB4, primitive.getColorRgb4Value())
Resources.Primitive.OneofValueCase.DIMENSION_VALUE_DEPRECATED -> {
val float = primitive.getDimensionValueDeprecated()
Pair(ResValue.DataType.DIMENSION, float.toRawBits())
}
Resources.Primitive.OneofValueCase.FRACTION_VALUE_DEPRECATED -> {
val float = primitive.getFractionValueDeprecated()
Pair(ResValue.DataType.FRACTION, float.toRawBits())
}
else -> {
val errorMsg = "Value case unrecognized for Primitive proto: %s"
logger?.error(null, errorMsg, primitive.oneofValueCase)
return null
}
}
return BinaryPrimitive(ResValue(type, deviceValue.deviceToHost()))
}
fun deserializeStringFromPb(string: Resources.String, valuePool: StringPool) =
BasicString(valuePool.makeRef(string.getValue()))
fun deserializeRawFromPb(string: Resources.RawString, valuePool: StringPool) =
RawString(valuePool.makeRef(string.getValue()))
fun deserializeStyledStrFromPb(
string: Resources.StyledString, valuePool: StringPool): StyledString {
val spans = mutableListOf<Span>()
for (span in string.getSpanList()) {
spans.add(Span(span.getTag(), span.getFirstChar(), span.getLastChar()))
}
return StyledString(valuePool.makeRef(StyleString(string.getValue(), spans)), listOf())
}
fun deserializeReferenceFromPb(ref: Resources.Reference, logger: ILogger?): Reference? {
val reference = if (ref.getName().isNotEmpty()) {
val refName = parseResourceName(ref.getName())
if (refName == null) {
val errorMsg = "%s cannot be parsed as a Resource Name."
logger?.error(null, errorMsg, ref.name)
return null
}
Reference(refName.resourceName)
} else {
Reference()
}
reference.referenceType = deserializeReferenceTypeFromPb(ref.getType())
reference.isPrivate = ref.getPrivate()
if (ref.getId() != 0) {
reference.id = ref.getId()
}
return reference
}
fun deserializeFileRefFromPb(
file: Resources.FileReference, valuePool: StringPool, config: ConfigDescription): FileReference {
val fileRef = FileReference(
valuePool.makeRef(
file.getPath(),
StringPool.Context(StringPool.Context.Priority.HIGH.priority, config)))
fileRef.type = deserializeFileTypeFromPb(file.getType())
return fileRef
}
fun deserializeAttrFromPb(
attr: Resources.Attribute, sourcePool: ResStringPool, logger: ILogger?): AttributeResource? {
val attrResource = AttributeResource(attr.getFormatFlags())
attrResource.minInt = attr.getMinInt()
attrResource.maxInt = attr.getMaxInt()
for (symbol in attr.getSymbolList()) {
val reference = deserializeReferenceFromPb(symbol.getName(), logger)
reference ?: return null
if (symbol.hasSource()) {
reference.source = deserializeSourceFromPb(symbol.getSource(), sourcePool)
}
reference.comment = symbol.getComment()
attrResource.symbols.add(
AttributeResource.Symbol(reference, symbol.getValue(), symbol.getType().toByte()))
}
return attrResource
}
fun deserializeStyleFromPb(
style: Resources.Style,
valuePool: StringPool,
config: ConfigDescription,
sourcePool: ResStringPool,
logger: ILogger?): Style? {
val styleResource = Style()
if (style.hasParent()) {
val parentRef = deserializeReferenceFromPb(style.getParent(), logger)
parentRef ?: return null
parentRef.source = if (style.hasParentSource()) {
deserializeSourceFromPb(style.getParentSource(), sourcePool)
} else {
Source.EMPTY
}
styleResource.parent = parentRef
}
for (entry in style.getEntryList()) {
val key = deserializeReferenceFromPb(entry.getKey(), logger)
key ?: return null
val value = deserializeItemFromPb(entry.getItem(), valuePool, config, logger)
value ?: return null
if (entry.hasSource()) {
key.source = deserializeSourceFromPb(entry.getSource(), sourcePool)
value.source = deserializeSourceFromPb(entry.getSource(), sourcePool)
}
key.comment = entry.getComment()
value.comment = entry.getComment()
styleResource.entries.add(Style.Entry(key, value))
}
return styleResource
}
fun deserializeStyleableFromPb(
styleable: Resources.Styleable, sourcePool: ResStringPool, logger: ILogger?): Styleable? {
val styleableResource = Styleable()
for (entry in styleable.getEntryList()) {
val reference = deserializeReferenceFromPb(entry.getAttr(), logger)
reference?: return null
if (entry.hasSource()) {
reference.source = deserializeSourceFromPb(entry.getSource(), sourcePool)
}
reference.comment = entry.getComment()
styleableResource.entries.add(reference)
}
return styleableResource
}
fun deserializeArrayFromPb(
array: Resources.Array,
valuePool: StringPool,
config: ConfigDescription,
sourcePool: ResStringPool,
logger: ILogger?): ArrayResource? {
val arrayResource = ArrayResource()
for (entry in array.getElementList()) {
val item = deserializeItemFromPb(entry.getItem(), valuePool, config, logger)
item ?: return null
if (entry.hasSource()) {
item.source = deserializeSourceFromPb(entry.getSource(), sourcePool)
}
item.comment = entry.getComment()
arrayResource.elements.add(item)
}
return arrayResource
}
fun deserializePluralTypeFromPb(type: Resources.Plural.Arity) = when (type) {
Resources.Plural.Arity.ZERO -> Plural.Type.ZERO
Resources.Plural.Arity.ONE -> Plural.Type.ONE
Resources.Plural.Arity.TWO -> Plural.Type.TWO
Resources.Plural.Arity.FEW -> Plural.Type.FEW
Resources.Plural.Arity.MANY -> Plural.Type.MANY
else -> Plural.Type.OTHER
}
fun deserializePluralFromPb(
plural: Resources.Plural,
valuePool: StringPool,
config: ConfigDescription,
sourcePool: ResStringPool,
logger: ILogger?): Plural? {
val pluralResource = Plural()
for (entry in plural.getEntryList()) {
val type = deserializePluralTypeFromPb(entry.getArity())
val item = deserializeItemFromPb(entry.getItem(), valuePool, config, logger)
item ?: return null
item.source = if (entry.hasSource()) {
deserializeSourceFromPb(entry.getSource(), sourcePool)
} else {
Source.EMPTY
}
item.comment = entry.getComment()
pluralResource.setValue(type, item)
}
return pluralResource
}
fun deserializeMacroFromPb(
pbMacro: Resources.MacroBody,
valuePool: StringPool,
config: ConfigDescription,
sourcePool: ResStringPool,
logger: ILogger?): Macro {
val macro = Macro()
macro.rawValue = pbMacro.rawString
if (pbMacro.hasStyleString()) {
val spans = ArrayList<Span>()
for (span in pbMacro.styleString.spansList) {
spans.add(Span(span.name, span.startIndex, span.endIndex))
}
macro.styleString = StyleString(pbMacro.styleString.str, spans)
}
val untranslatableList = ArrayList<UntranslatableSection>()
for (untranslatable in pbMacro.untranslatableSectionsList) {
untranslatableList.add(
UntranslatableSection(
untranslatable.startIndex.toInt(), untranslatable.endIndex.toInt())
)
}
macro.untranslatables = untranslatableList
val aliasNamespaces = ArrayList<Macro.Namespace>()
for (namespace in pbMacro.namespaceStackList) {
aliasNamespaces.add(
Macro.Namespace(namespace.prefix, namespace.packageName, namespace.isPrivate)
)
}
macro.aliasNamespaces = aliasNamespaces
return macro
}
fun deserializeItemFromPb(
item: Resources.Item,
valuePool: StringPool,
config: ConfigDescription,
logger: ILogger?): Item? = when (item.getValueCase()) {
Resources.Item.ValueCase.REF -> deserializeReferenceFromPb(item.getRef(), logger)
Resources.Item.ValueCase.STR -> deserializeStringFromPb(item.getStr(), valuePool)
Resources.Item.ValueCase.RAW_STR -> deserializeRawFromPb(item.getRawStr(), valuePool)
Resources.Item.ValueCase.STYLED_STR ->
deserializeStyledStrFromPb(item.getStyledStr(), valuePool)
Resources.Item.ValueCase.FILE -> deserializeFileRefFromPb(item.getFile(), valuePool, config)
Resources.Item.ValueCase.ID -> Id()
Resources.Item.ValueCase.PRIM -> deserializeBinPrimitiveFromPb(item.getPrim(), logger)
else -> {
val errorMsg = "Unrecognized value case for Item: %s."
logger?.error(null, errorMsg, item.valueCase)
null
}
}
fun deserializeValueFromPb(
value: Resources.Value,
valuePool: StringPool,
config: ConfigDescription,
sourcePool: ResStringPool,
logger: ILogger?): Value? {
val valueResource = when {
value.hasItem() -> deserializeItemFromPb(value.getItem(), valuePool, config, logger)
value.hasCompoundValue() -> {
val compoundValue = value.getCompoundValue()
when (compoundValue.getValueCase()) {
Resources.CompoundValue.ValueCase.ATTR ->
deserializeAttrFromPb(compoundValue.getAttr(), sourcePool, logger)
Resources.CompoundValue.ValueCase.STYLE ->
deserializeStyleFromPb(compoundValue.getStyle(), valuePool, config, sourcePool, logger)
Resources.CompoundValue.ValueCase.STYLEABLE ->
deserializeStyleableFromPb(compoundValue.getStyleable(), sourcePool, logger)
Resources.CompoundValue.ValueCase.ARRAY ->
deserializeArrayFromPb(compoundValue.getArray(), valuePool, config, sourcePool, logger)
Resources.CompoundValue.ValueCase.PLURAL ->
deserializePluralFromPb(compoundValue.getPlural(), valuePool, config, sourcePool, logger)
Resources.CompoundValue.ValueCase.MACRO ->
deserializeMacroFromPb(compoundValue.getMacro(), valuePool, config, sourcePool, logger)
else -> {
val errorMsg = "Unrecognized compoundValue value case %s"
logger?.error(null, errorMsg, compoundValue.valueCase)
null
}
}
}
else -> {
val errorMsg = "Unrecognized case for serialized value %s"
logger?.error(null, errorMsg, value.valueCase)
null
}
}
valueResource ?: return null
valueResource.weak = value.getWeak()
if (value.hasSource()) {
valueResource.source = deserializeSourceFromPb(value.getSource(), sourcePool)
}
valueResource.comment = value.getComment()
return valueResource
}
fun deserializePackageFromPb(
original: Resources.Package,
sourcePool: ResStringPool,
overlayables: List<Overlayable>,
logger: ILogger?,
table: ResourceTable): Boolean {
val packageId =
if (original.hasPackageId()) original.getPackageId().getId().toByte() else 0.toByte()
val idIndex = mutableMapOf<Int, ResourceName>()
val resourcePackage =
table.createPackageAllowingDuplicateNames(original.getPackageName(), packageId)
for (group in original.getTypeList()) {
val resType = resourceTypeFromTag(group.getName())
if (resType == null) {
val errorMsg = "Unrecognized Resource Type: %s."
logger?.error(null, errorMsg, group.name)
return false
}
val resourceGroup = resourcePackage.findOrCreateGroup(resType)
if (group.hasTypeId()) {
resourceGroup.id = group.getTypeId().getId().toByte()
}
for (entry in group.getEntryList()) {
val resourceEntry = resourceGroup.findOrCreateEntry(entry.getName())
if (entry.hasEntryId()) {
resourceEntry.id = entry.getEntryId().getId().toShort()
}
// Deserialize the symbol status (public/private with metadata)
if (entry.hasVisibility()) {
val visibility = entry.getVisibility()
val visibilityLevel = deserializeVisibilityFromPb(visibility.getLevel())
when (visibilityLevel) {
// Propagate the visibility up to the type group.
ResourceVisibility.PUBLIC -> resourceGroup.visibility = ResourceVisibility.PUBLIC
// Propagate private, only if previously undefined.
ResourceVisibility.PRIVATE-> {
if (resourceGroup.visibility == ResourceVisibility.UNDEFINED) {
resourceGroup.visibility = ResourceVisibility.PRIVATE
}
}
else -> {}
}
val visibilitySource = if (visibility.hasSource()) {
deserializeSourceFromPb(visibility.getSource(), sourcePool)
} else {
Source.EMPTY
}
val visibilityComment = visibility.getComment()
resourceEntry.visibility = Visibility(visibilitySource, visibilityComment, visibilityLevel)
}
if (entry.hasAllowNew()) {
val allowNew = entry.getAllowNew()
val allowNewSource = if (allowNew.hasSource()) {
deserializeSourceFromPb(allowNew.getSource(), sourcePool)
} else {
Source.EMPTY
}
val allowNewComment = allowNew.getComment()
resourceEntry.allowNew = AllowNew(allowNewSource, allowNewComment)
}
if (entry.hasOverlayableItem()) {
// Find the overlayable to which this item belongs.
val overlayableItem = entry.getOverlayableItem()
if (overlayableItem.getOverlayableIdx() >= overlayables.size) {
val errorMsg = "Illegal value for overlayable id '%s'. Because only '%s' overlayables" +
"exist in proto."
logger?.error(null, errorMsg, overlayableItem.overlayableIdx, overlayables.size)
return false
}
val resourceOverlayable =
deserializeOverlayableFromPb(
overlayableItem,
overlayables[overlayableItem.getOverlayableIdx()],
sourcePool,
logger)
resourceOverlayable ?: return false
resourceEntry.overlayable = resourceOverlayable
}
val resourceId = resourceIdFromParts(
original.getPackageId().getId().toByte(),
group.getTypeId().getId().toByte(),
entry.getEntryId().getId().toShort())
if (resourceId.isValidId()) {
idIndex[resourceId] =
ResourceName(resourcePackage.name, resourceGroup.type, resourceEntry.name)
}
for (configValue in entry.getConfigValueList()) {
val config = configValue.getConfig()
val resourceConfig = deserializeConfigFromPb(config, logger)
resourceConfig ?: return false
val resourceConfigValue =
resourceEntry.findOrCreateValue(resourceConfig, config.getProduct())
if (resourceConfigValue.value.isTruthy()) {
val errorMsg = "Value already exists for name %s, config %s, and product %s."
logger?.error(
null,
errorMsg,
ResourceName(resourcePackage.name, resourceGroup.type, resourceEntry.name),
resourceConfig,
config.product)
return false
}
resourceConfigValue.value =
deserializeValueFromPb(
configValue.getValue(), table.stringPool, resourceConfig, sourcePool, logger)
resourceConfigValue.value ?: return false
}
}
}
// Assign all references with ids the names of the resource to which they are referencing.
for (group in resourcePackage.groups) {
for (idToEntries in group.entries.values) {
for (entry in idToEntries.values) {
for (configValue in entry.values) {
val value = configValue.value
if (value is Reference) {
val referenceId = value.id
if (referenceId == null || !referenceId.isValidId()) {
continue
}
val result = idIndex[referenceId]
result ?: continue
value.name = result
}
}
}
}
}
return true
}
fun deserializeTableFromPb(
table: Resources.ResourceTable, outTable: ResourceTable, logger: ILogger?): Boolean {
val sourcePool = ResStringPool.get(
table.getSourcePool().getData().asReadOnlyByteBuffer(),
table.getSourcePool().getSerializedSize())
// Deserialize the overlayable groups of a table
val overlayables = mutableListOf<Overlayable>()
for (overlayable in table.getOverlayableList()) {
val overlayableSource = if (overlayable.hasSource()) {
deserializeSourceFromPb(overlayable.getSource(), sourcePool)
} else {
Source.EMPTY
}
overlayables.add(Overlayable(overlayable.getName(), overlayable.getActor(), overlayableSource))
}
for (resourcePackage in table.getPackageList()) {
if (!deserializePackageFromPb(resourcePackage, sourcePool, overlayables, logger, outTable)) {
return false
}
}
return true
}
@@ -0,0 +1,719 @@
package com.android.aaptcompiler.proto
import android.aapt.pb.internal.ResourcesInternal
import com.android.aapt.ConfigurationOuterClass
import com.android.aapt.Resources
import com.android.aaptcompiler.ArrayResource
import com.android.aaptcompiler.AttributeResource
import com.android.aaptcompiler.BasicString
import com.android.aaptcompiler.BinaryPrimitive
import com.android.aaptcompiler.ConfigDescription
import com.android.aaptcompiler.FileReference
import com.android.aaptcompiler.Id
import com.android.aaptcompiler.Item
import com.android.aaptcompiler.Macro
import com.android.aaptcompiler.Overlayable
import com.android.aaptcompiler.OverlayableItem
import com.android.aaptcompiler.Plural
import com.android.aaptcompiler.RawString
import com.android.aaptcompiler.Reference
import com.android.aaptcompiler.ResourceFile
import com.android.aaptcompiler.ResourceName
import com.android.aaptcompiler.ResourceTable
import com.android.aaptcompiler.Source
import com.android.aaptcompiler.StringPool
import com.android.aaptcompiler.Style
import com.android.aaptcompiler.Styleable
import com.android.aaptcompiler.StyledString
import com.android.aaptcompiler.ToolFingerprint
import com.android.aaptcompiler.Value
import com.android.aaptcompiler.android.ResTableConfig
import com.android.aaptcompiler.android.ResValue
import com.android.aaptcompiler.blameSource
import com.android.aaptcompiler.buffer.BigBuffer
import com.itsaky.androidide.layoutlib.resources.ResourceVisibility
import com.android.utils.ILogger
import com.google.protobuf.ByteString
internal fun serializePoolToPb(pool: StringPool, logger: ILogger?): Resources.StringPool {
val buffer = BigBuffer(1024)
pool.flattenUtf8(buffer, logger)
val builder = Resources.StringPool.newBuilder()
builder.setData(ByteString.copyFrom(buffer.toBytes()))
return builder.build()
}
internal fun serializeSourceToPb(source: Source, sourcePool: StringPool): Resources.Source {
val ref = sourcePool.makeRef(source.path)
val sourceBuilder = Resources.Source.newBuilder()
sourceBuilder.setPathIdx(ref.index())
if (source.line != null) {
sourceBuilder.setPosition(
Resources.SourcePosition.newBuilder().setLineNumber(source.line).build())
}
return sourceBuilder.build()
}
fun serializeTableToPb(table: ResourceTable, logger: ILogger? = null): Resources.ResourceTable {
val tableBuilder = Resources.ResourceTable.newBuilder()
val sourcePool = StringPool()
tableBuilder.addToolFingerprint(Resources.ToolFingerprint.newBuilder()
.setTool(ToolFingerprint.TOOL_NAME)
.setVersion(ToolFingerprint.FINGERPRINT)
.build())
val overlayables = mutableListOf<Overlayable>()
for (resourcePackage in table.packages) {
val packageBuilder = Resources.Package.newBuilder()
val packageId = resourcePackage.id
if (packageId != null) {
packageBuilder.setPackageId(
Resources.PackageId.newBuilder()
.setId(packageId.toInt())
.build())
}
packageBuilder.setPackageName(resourcePackage.name)
for (resourceGroup in resourcePackage.groups) {
val groupBuilder = Resources.Type.newBuilder()
val groupId = resourceGroup.id
if (groupId != null) {
groupBuilder.setTypeId(
Resources.TypeId.newBuilder()
.setId(groupId.toInt())
.build())
}
groupBuilder.setName(resourceGroup.type.tagName)
for (idToEntries in resourceGroup.entries.values) {
for (entry in idToEntries.values) {
val entryBuilder = Resources.Entry.newBuilder()
val entryId = entry.id
if (entryId != null) {
entryBuilder.setEntryId(
Resources.EntryId.newBuilder()
.setId(entryId.toInt())
.build())
}
entryBuilder.setName(entry.name)
val visibilityBuilder = Resources.Visibility.newBuilder()
if (entry.visibility.source.isNotEmpty()) {
visibilityBuilder.setSource(serializeSourceToPb(entry.visibility.source, sourcePool))
}
entryBuilder.setVisibility(
visibilityBuilder
.setLevel(serializeVisibilityToPb(entry.visibility.level))
.setComment(entry.visibility.comment)
.build())
val entryAllowNew = entry.allowNew
if (entryAllowNew != null) {
val allowNewBuilder = Resources.AllowNew.newBuilder()
if (entryAllowNew.source.isNotEmpty()) {
allowNewBuilder.setSource(serializeSourceToPb(entryAllowNew.source, sourcePool))
}
entryBuilder.setAllowNew(
allowNewBuilder
.setComment(entryAllowNew.comment)
.build())
}
val entryOverlayableItem = entry.overlayable
if (entryOverlayableItem != null) {
entryBuilder.setOverlayableItem(
serializeOverlayableToPb(
entryOverlayableItem,
overlayables,
tableBuilder,
sourcePool))
}
for (configValue in entry.values) {
entryBuilder.addConfigValue(
Resources.ConfigValue.newBuilder()
.setConfig(serializeConfigToPb(configValue.config, configValue.product, logger))
.setValue(serializeValueToPb(configValue.value!!, sourcePool, logger))
.build()
)
}
groupBuilder.addEntry(entryBuilder.build())
}
}
packageBuilder.addType(groupBuilder.build())
}
tableBuilder.addPackage(packageBuilder.build())
}
return tableBuilder.setSourcePool(serializePoolToPb(sourcePool, logger))
.build()
}
fun serializeVisibilityToPb(resourceVisibility: ResourceVisibility) =
when(resourceVisibility) {
ResourceVisibility.PRIVATE,
ResourceVisibility.PRIVATE_XML_ONLY -> Resources.Visibility.Level.PRIVATE
ResourceVisibility.PUBLIC -> Resources.Visibility.Level.PUBLIC
else -> Resources.Visibility.Level.UNKNOWN
}
fun serializeCompiledFileToPb(file: ResourceFile): ResourcesInternal.CompiledFile {
val compiledFile = ResourcesInternal.CompiledFile.newBuilder()
.setResourceName(file.name.toString())
.setSourcePath(file.source.path)
.setType(serializeFileTypeToPb(file.type))
.setConfig(serializeConfigToPb(file.configuration, null, null))
for (exportedResourceName in file.exportedSymbols) {
compiledFile.addExportedSymbol(
ResourcesInternal.CompiledFile.Symbol.newBuilder()
.setResourceName(exportedResourceName.name.toString())
.setSource(
Resources.SourcePosition.newBuilder()
.setLineNumber(exportedResourceName.line)))
}
return compiledFile.build()
}
fun serializeConfigToPb(
config: ConfigDescription,
product: String?,
logger: ILogger?): ConfigurationOuterClass.Configuration {
val configBuilder = ConfigurationOuterClass.Configuration.newBuilder()
configBuilder.setMcc(config.mcc.toInt() and 0xffff)
configBuilder.setMnc(config.mnc.toInt() and 0xffff)
configBuilder.setLocale(config.getBcp47Locale())
configBuilder.setLayoutDirection(
when ((config.screenLayout.toInt() and ResTableConfig.SCREEN_LAYOUT.DIR_MASK).toByte()) {
ResTableConfig.SCREEN_LAYOUT.DIR_LTR ->
ConfigurationOuterClass.Configuration.LayoutDirection.LAYOUT_DIRECTION_LTR
ResTableConfig.SCREEN_LAYOUT.DIR_RTL ->
ConfigurationOuterClass.Configuration.LayoutDirection.LAYOUT_DIRECTION_RTL
else -> ConfigurationOuterClass.Configuration.LayoutDirection.LAYOUT_DIRECTION_UNSET
})
configBuilder.setScreenWidth(config.screenWidth)
configBuilder.setScreenHeight(config.screenHeight)
configBuilder.setScreenWidthDp(config.screenWidthDp)
configBuilder.setScreenHeightDp(config.screenHeightDp)
configBuilder.setSmallestScreenWidthDp(config.smallestScreenWidthDp)
configBuilder.setScreenLayoutSize(
when ((config.screenLayout.toInt() and ResTableConfig.SCREEN_LAYOUT.SIZE_MASK).toByte()) {
ResTableConfig.SCREEN_LAYOUT.SIZE_SMALL ->
ConfigurationOuterClass.Configuration.ScreenLayoutSize.SCREEN_LAYOUT_SIZE_SMALL
ResTableConfig.SCREEN_LAYOUT.SIZE_NORMAL ->
ConfigurationOuterClass.Configuration.ScreenLayoutSize.SCREEN_LAYOUT_SIZE_NORMAL
ResTableConfig.SCREEN_LAYOUT.SIZE_LARGE ->
ConfigurationOuterClass.Configuration.ScreenLayoutSize.SCREEN_LAYOUT_SIZE_LARGE
ResTableConfig.SCREEN_LAYOUT.SIZE_XLARGE ->
ConfigurationOuterClass.Configuration.ScreenLayoutSize.SCREEN_LAYOUT_SIZE_XLARGE
else -> ConfigurationOuterClass.Configuration.ScreenLayoutSize.SCREEN_LAYOUT_SIZE_UNSET
})
configBuilder.setScreenLayoutLong(
when (
(config.screenLayout.toInt() and ResTableConfig.SCREEN_LAYOUT.SCREENLONG_MASK).toByte()) {
ResTableConfig.SCREEN_LAYOUT.SCREENLONG_YES ->
ConfigurationOuterClass.Configuration.ScreenLayoutLong.SCREEN_LAYOUT_LONG_LONG
ResTableConfig.SCREEN_LAYOUT.SCREENLONG_NO ->
ConfigurationOuterClass.Configuration.ScreenLayoutLong.SCREEN_LAYOUT_LONG_NOTLONG
else -> ConfigurationOuterClass.Configuration.ScreenLayoutLong.SCREEN_LAYOUT_LONG_UNSET
})
configBuilder.setScreenRound(
when (
(config.screenLayout2.toInt() and ResTableConfig.SCREEN_LAYOUT2.SCREENROUND_MASK).toByte()) {
ResTableConfig.SCREEN_LAYOUT2.SCREENROUND_YES ->
ConfigurationOuterClass.Configuration.ScreenRound.SCREEN_ROUND_ROUND
ResTableConfig.SCREEN_LAYOUT2.SCREENROUND_NO ->
ConfigurationOuterClass.Configuration.ScreenRound.SCREEN_ROUND_NOTROUND
else -> ConfigurationOuterClass.Configuration.ScreenRound.SCREEN_ROUND_UNSET
})
configBuilder.setWideColorGamut(
when ((config.colorMode.toInt() and ResTableConfig.COLOR_MODE.WIDE_GAMUT_MASK).toByte()) {
ResTableConfig.COLOR_MODE.WIDE_GAMUT_YES ->
ConfigurationOuterClass.Configuration.WideColorGamut.WIDE_COLOR_GAMUT_WIDECG
ResTableConfig.COLOR_MODE.WIDE_GAMUT_NO ->
ConfigurationOuterClass.Configuration.WideColorGamut.WIDE_COLOR_GAMUT_NOWIDECG
else -> ConfigurationOuterClass.Configuration.WideColorGamut.WIDE_COLOR_GAMUT_UNSET
})
configBuilder.setHdr(
when ((config.colorMode.toInt() and ResTableConfig.COLOR_MODE.HDR_MASK).toByte()) {
ResTableConfig.COLOR_MODE.HDR_YES -> ConfigurationOuterClass.Configuration.Hdr.HDR_HIGHDR
ResTableConfig.COLOR_MODE.HDR_NO -> ConfigurationOuterClass.Configuration.Hdr.HDR_LOWDR
else -> ConfigurationOuterClass.Configuration.Hdr.HDR_UNSET
})
configBuilder.setOrientation(
when (config.orientation) {
ResTableConfig.ORIENTATION.PORT ->
ConfigurationOuterClass.Configuration.Orientation.ORIENTATION_PORT
ResTableConfig.ORIENTATION.LAND ->
ConfigurationOuterClass.Configuration.Orientation.ORIENTATION_LAND
ResTableConfig.ORIENTATION.SQUARE ->
ConfigurationOuterClass.Configuration.Orientation.ORIENTATION_SQUARE
else -> ConfigurationOuterClass.Configuration.Orientation.ORIENTATION_UNSET
})
configBuilder.setUiModeType(
when ((config.uiMode.toInt() and ResTableConfig.UI_MODE.TYPE_MASK).toByte()) {
ResTableConfig.UI_MODE.TYPE_NORMAL ->
ConfigurationOuterClass.Configuration.UiModeType.UI_MODE_TYPE_NORMAL
ResTableConfig.UI_MODE.TYPE_DESK ->
ConfigurationOuterClass.Configuration.UiModeType.UI_MODE_TYPE_DESK
ResTableConfig.UI_MODE.TYPE_CAR ->
ConfigurationOuterClass.Configuration.UiModeType.UI_MODE_TYPE_CAR
ResTableConfig.UI_MODE.TYPE_TELEVISION ->
ConfigurationOuterClass.Configuration.UiModeType.UI_MODE_TYPE_TELEVISION
ResTableConfig.UI_MODE.TYPE_APPLIANCE ->
ConfigurationOuterClass.Configuration.UiModeType.UI_MODE_TYPE_APPLIANCE
ResTableConfig.UI_MODE.TYPE_WATCH ->
ConfigurationOuterClass.Configuration.UiModeType.UI_MODE_TYPE_WATCH
ResTableConfig.UI_MODE.TYPE_VR_HEADSET ->
ConfigurationOuterClass.Configuration.UiModeType.UI_MODE_TYPE_VRHEADSET
else -> ConfigurationOuterClass.Configuration.UiModeType.UI_MODE_TYPE_UNSET
})
configBuilder.setUiModeNight(
when ((config.uiMode.toInt() and ResTableConfig.UI_MODE.NIGHT_MASK).toByte()) {
ResTableConfig.UI_MODE.NIGHT_YES ->
ConfigurationOuterClass.Configuration.UiModeNight.UI_MODE_NIGHT_NIGHT
ResTableConfig.UI_MODE.NIGHT_NO ->
ConfigurationOuterClass.Configuration.UiModeNight.UI_MODE_NIGHT_NOTNIGHT
else -> ConfigurationOuterClass.Configuration.UiModeNight.UI_MODE_NIGHT_UNSET
})
configBuilder.setDensity(config.density)
configBuilder.setTouchscreen(
when (config.touchscreen) {
ResTableConfig.TOUCHSCREEN.NOTOUCH ->
ConfigurationOuterClass.Configuration.Touchscreen.TOUCHSCREEN_NOTOUCH
ResTableConfig.TOUCHSCREEN.STYLUS ->
ConfigurationOuterClass.Configuration.Touchscreen.TOUCHSCREEN_STYLUS
ResTableConfig.TOUCHSCREEN.FINGER ->
ConfigurationOuterClass.Configuration.Touchscreen.TOUCHSCREEN_FINGER
else -> ConfigurationOuterClass.Configuration.Touchscreen.TOUCHSCREEN_UNSET
})
configBuilder.setKeysHidden(
when ((config.inputFlags.toInt() and ResTableConfig.INPUT_FLAGS.KEYSHIDDEN_MASK).toByte()) {
ResTableConfig.INPUT_FLAGS.KEYSHIDDEN_NO ->
ConfigurationOuterClass.Configuration.KeysHidden.KEYS_HIDDEN_KEYSEXPOSED
ResTableConfig.INPUT_FLAGS.KEYSHIDDEN_YES ->
ConfigurationOuterClass.Configuration.KeysHidden.KEYS_HIDDEN_KEYSHIDDEN
ResTableConfig.INPUT_FLAGS.KEYSHIDDEN_SOFT ->
ConfigurationOuterClass.Configuration.KeysHidden.KEYS_HIDDEN_KEYSSOFT
else -> ConfigurationOuterClass.Configuration.KeysHidden.KEYS_HIDDEN_UNSET
})
configBuilder.setKeyboard(
when (config.keyboard) {
ResTableConfig.KEYBOARD.NOKEYS ->
ConfigurationOuterClass.Configuration.Keyboard.KEYBOARD_NOKEYS
ResTableConfig.KEYBOARD.QWERTY ->
ConfigurationOuterClass.Configuration.Keyboard.KEYBOARD_QWERTY
ResTableConfig.KEYBOARD.TWELVEKEY ->
ConfigurationOuterClass.Configuration.Keyboard.KEYBOARD_TWELVEKEY
else -> ConfigurationOuterClass.Configuration.Keyboard.KEYBOARD_UNSET
})
configBuilder.setNavHidden(
when ((config.inputFlags.toInt() and ResTableConfig.INPUT_FLAGS.NAVHIDDEN_MASK).toByte()) {
ResTableConfig.INPUT_FLAGS.NAVHIDDEN_YES ->
ConfigurationOuterClass.Configuration.NavHidden.NAV_HIDDEN_NAVHIDDEN
ResTableConfig.INPUT_FLAGS.NAVHIDDEN_NO ->
ConfigurationOuterClass.Configuration.NavHidden.NAV_HIDDEN_NAVEXPOSED
else -> ConfigurationOuterClass.Configuration.NavHidden.NAV_HIDDEN_UNSET
})
configBuilder.setNavigation(
when (config.navigation) {
ResTableConfig.NAVIGATION.NONAV ->
ConfigurationOuterClass.Configuration.Navigation.NAVIGATION_NONAV
ResTableConfig.NAVIGATION.DPAD ->
ConfigurationOuterClass.Configuration.Navigation.NAVIGATION_DPAD
ResTableConfig.NAVIGATION.TRACKBALL ->
ConfigurationOuterClass.Configuration.Navigation.NAVIGATION_TRACKBALL
ResTableConfig.NAVIGATION.WHEEL ->
ConfigurationOuterClass.Configuration.Navigation.NAVIGATION_WHEEL
else -> ConfigurationOuterClass.Configuration.Navigation.NAVIGATION_UNSET
})
configBuilder.setSdkVersion(config.sdkVersion.toInt() and 0xffff)
if (product != null) {
configBuilder.setProduct(product)
}
return configBuilder.build()
}
fun serializeOverlayableToPb(
item: OverlayableItem,
overlayables: MutableList<Overlayable>,
table: Resources.ResourceTable.Builder,
sourcePool: StringPool): Resources.OverlayableItem {
// Retrieve the index of the overlayable in the list of groups that have already been serialized.
val foundIndex = overlayables.indexOf(item.overlayable)
// Serialize the overlayable if it has not been serialized already.
if (foundIndex == -1) {
overlayables.add(item.overlayable)
val overlayableBuilder = Resources.Overlayable.newBuilder()
.setName(item.overlayable.name)
.setActor(item.overlayable.actor)
if (item.overlayable.source.isNotEmpty()) {
overlayableBuilder.setSource(serializeSourceToPb(item.overlayable.source, sourcePool))
}
table.addOverlayable(overlayableBuilder.build())
}
val overlayableIndex = if (foundIndex == -1) overlayables.lastIndex else foundIndex
val itemBuilder = Resources.OverlayableItem.newBuilder()
itemBuilder.setOverlayableIdx(overlayableIndex)
if ((item.policies and OverlayableItem.Policy.PUBLIC) != 0) {
itemBuilder.addPolicy(Resources.OverlayableItem.Policy.PUBLIC)
}
if ((item.policies and OverlayableItem.Policy.PRODUCT) != 0) {
itemBuilder.addPolicy(Resources.OverlayableItem.Policy.PRODUCT)
}
if ((item.policies and OverlayableItem.Policy.SYSTEM) != 0) {
itemBuilder.addPolicy(Resources.OverlayableItem.Policy.SYSTEM)
}
if ((item.policies and OverlayableItem.Policy.VENDOR) != 0) {
itemBuilder.addPolicy(Resources.OverlayableItem.Policy.VENDOR)
}
if ((item.policies and OverlayableItem.Policy.SIGNATURE) != 0) {
itemBuilder.addPolicy(Resources.OverlayableItem.Policy.SIGNATURE)
}
if ((item.policies and OverlayableItem.Policy.SYSTEM) != 0) {
itemBuilder.addPolicy(Resources.OverlayableItem.Policy.SYSTEM)
}
if ((item.policies and OverlayableItem.Policy.ODM) != 0) {
itemBuilder.addPolicy(Resources.OverlayableItem.Policy.ODM)
}
if ((item.policies and OverlayableItem.Policy.OEM) != 0) {
itemBuilder.addPolicy(Resources.OverlayableItem.Policy.OEM)
}
if (item.source.isNotEmpty()) {
itemBuilder.setSource(serializeSourceToPb(item.source, sourcePool))
}
itemBuilder.setComment(item.comment)
return itemBuilder.build()
}
fun serializeReferenceTypeToPb(type: Reference.Type) = when (type) {
Reference.Type.RESOURCE ->
Resources.Reference.Type.REFERENCE
Reference.Type.ATTRIBUTE ->
Resources.Reference.Type.ATTRIBUTE
}
fun serializeReferenceToPb(ref: Reference): Resources.Reference {
val refBuilder = Resources.Reference.newBuilder()
refBuilder.setId(ref.id ?: 0)
if (ref.name != ResourceName.EMPTY) {
refBuilder.setName(ref.name.toString())
}
refBuilder.setPrivate(ref.isPrivate)
refBuilder.setType(serializeReferenceTypeToPb(ref.referenceType))
refBuilder.setAllowRaw(ref.allowRaw)
refBuilder.setTypeFlags(ref.typeFlags!!)
return refBuilder.build()
}
fun serializePluralTypeToPb(type: Plural.Type) = when (type) {
Plural.Type.ZERO -> Resources.Plural.Arity.ZERO
Plural.Type.ONE -> Resources.Plural.Arity.ONE
Plural.Type.TWO -> Resources.Plural.Arity.TWO
Plural.Type.FEW -> Resources.Plural.Arity.FEW
Plural.Type.MANY -> Resources.Plural.Arity.MANY
else -> Resources.Plural.Arity.OTHER
}
fun serializeFileTypeToPb(type: ResourceFile.Type) = when (type) {
ResourceFile.Type.BinaryXml -> Resources.FileReference.Type.BINARY_XML
ResourceFile.Type.ProtoXml -> Resources.FileReference.Type.PROTO_XML
ResourceFile.Type.Png -> Resources.FileReference.Type.PNG
else -> Resources.FileReference.Type.UNKNOWN
}
fun serializeStringToPb(string: BasicString) =
Resources.String.newBuilder().setValue(string.ref.value()).build()
fun serializeRawToPb(string: RawString) =
Resources.RawString.newBuilder().setValue(string.value.value()).build()
fun serializeStyledStrToPb(string: StyledString): Resources.StyledString {
val styleBuilder = Resources.StyledString.newBuilder()
styleBuilder.setValue(string.ref.value())
for (span in string.ref.spans()) {
val spanBuilder = Resources.StyledString.Span.newBuilder()
spanBuilder.setTag(span.name.value())
.setFirstChar(span.firstChar)
.setLastChar(span.lastChar)
styleBuilder.addSpan(spanBuilder.build())
}
return styleBuilder.build()
}
fun serializeFileRefToPb(file: FileReference) =
Resources.FileReference.newBuilder()
.setPath(file.path.value())
.setType(serializeFileTypeToPb(file.type))
.build()
fun serializeBinPrimitiveToPb(primitive: BinaryPrimitive, logger: ILogger?): Resources.Primitive {
val resVal = primitive.flatten()
val primitiveBuilder = Resources.Primitive.newBuilder()
if (resVal == null) {
logger?.error(
null, "%s, Failed to serialize primitive %s.", blameSource(primitive.source), primitive)
return primitiveBuilder.build()
}
when (resVal.dataType) {
ResValue.DataType.NULL -> {
when (resVal.data) {
ResValue.NullFormat.UNDEFINED ->
primitiveBuilder.setNullValue(Resources.Primitive.NullType.newBuilder().build())
ResValue.NullFormat.EMPTY ->
primitiveBuilder.setEmptyValue(Resources.Primitive.EmptyType.newBuilder().build())
else -> {
val errorMsg = "%s, Invalid null format value '%s' for primitive %s."
logger?.error(null, errorMsg, blameSource(primitive.source), resVal.data, primitive)
}
}
}
ResValue.DataType.FLOAT -> primitiveBuilder.setFloatValue(Float.fromBits(resVal.data))
ResValue.DataType.DIMENSION -> primitiveBuilder.setDimensionValue(resVal.data)
ResValue.DataType.FRACTION -> primitiveBuilder.setFractionValue(resVal.data)
ResValue.DataType.INT_DEC -> primitiveBuilder.setIntDecimalValue(resVal.data)
ResValue.DataType.INT_HEX -> primitiveBuilder.setIntHexadecimalValue(resVal.data)
ResValue.DataType.INT_BOOLEAN -> primitiveBuilder.setBooleanValue(resVal.data != 0)
ResValue.DataType.INT_COLOR_ARGB8 -> primitiveBuilder.setColorArgb8Value(resVal.data)
ResValue.DataType.INT_COLOR_RGB8 -> primitiveBuilder.setColorRgb8Value(resVal.data)
ResValue.DataType.INT_COLOR_ARGB4 -> primitiveBuilder.setColorArgb4Value(resVal.data)
ResValue.DataType.INT_COLOR_RGB4 -> primitiveBuilder.setColorRgb4Value(resVal.data)
else -> {
val errorMsg = "%s, Invalid data type '%s' for primitive %s."
logger?.error(null, errorMsg, blameSource(primitive.source), resVal.dataType, primitive)
}
}
return primitiveBuilder.build()
}
fun serializeAttrToPb(attribute: AttributeResource, sourcePool: StringPool): Resources.Attribute {
val attrBuilder = Resources.Attribute.newBuilder()
.setFormatFlags(attribute.typeMask)
.setMinInt(attribute.minInt)
.setMaxInt(attribute.maxInt)
for (symbol in attribute.symbols) {
val symbolBuilder = Resources.Attribute.Symbol.newBuilder()
if (symbol.symbol.source.isNotEmpty()) {
symbolBuilder.setSource(serializeSourceToPb(symbol.symbol.source, sourcePool))
}
attrBuilder.addSymbol(
symbolBuilder
.setComment(symbol.symbol.comment)
.setName(serializeReferenceToPb(symbol.symbol))
.setValue(symbol.value)
.setType(symbol.type.toInt())
.build())
}
return attrBuilder.build()
}
fun serializeStyleToPb(style: Style, sourcePool: StringPool, logger: ILogger?): Resources.Style {
val styleBuilder = Resources.Style.newBuilder()
val parent = style.parent
if (parent != null) {
styleBuilder.setParent(serializeReferenceToPb(parent))
if (parent.source.isNotEmpty()) {
styleBuilder.setParentSource(serializeSourceToPb(parent.source, sourcePool))
}
}
for (entry in style.entries) {
val entryBuilder = Resources.Style.Entry.newBuilder()
if (entry.key.source.isNotEmpty()) {
entryBuilder.setSource(serializeSourceToPb(entry.key.source, sourcePool))
}
styleBuilder.addEntry(
entryBuilder
.setKey(serializeReferenceToPb(entry.key))
.setComment(entry.key.comment)
.setItem(serializeItemToPb(entry.value!!, logger))
.build())
}
return styleBuilder.build()
}
fun serializeStyleableToPb(styleable: Styleable, sourcePool: StringPool): Resources.Styleable {
val styleableBuilder = Resources.Styleable.newBuilder()
for (entry in styleable.entries) {
val entryBuilder = Resources.Styleable.Entry.newBuilder()
if (entry.source.isNotEmpty()) {
entryBuilder.setSource(serializeSourceToPb(entry.source, sourcePool))
}
styleableBuilder.addEntry(
entryBuilder
.setComment(entry.comment)
.setAttr(serializeReferenceToPb(entry))
.build())
}
return styleableBuilder.build()
}
fun serializeArrayToPb(
array: ArrayResource, sourcePool: StringPool, logger: ILogger?): Resources.Array {
val arrayBuilder = Resources.Array.newBuilder()
for (element in array.elements) {
val entryBuilder = Resources.Array.Element.newBuilder()
if (element.source.isNotEmpty()) {
entryBuilder.setSource(serializeSourceToPb(element.source, sourcePool))
}
arrayBuilder.addElement(
entryBuilder
.setComment(element.comment)
.setItem(serializeItemToPb(element, logger))
.build())
}
return arrayBuilder.build()
}
fun serializePluralToPb(
plural: Plural, sourcePool: StringPool, logger: ILogger?): Resources.Plural {
val pluralBuilder = Resources.Plural.newBuilder()
for (type in Plural.Type.TYPES) {
val entry = plural.values[type.ordinal]
entry ?: continue
val entryBuilder = Resources.Plural.Entry.newBuilder()
if (entry.source.isNotEmpty()) {
entryBuilder.setSource(serializeSourceToPb(entry.source, sourcePool))
}
pluralBuilder.addEntry(
entryBuilder
.setComment(entry.comment)
.setArity(serializePluralTypeToPb(type))
.setItem(serializeItemToPb(entry, logger))
.build())
}
return pluralBuilder.build()
}
fun serializeMacroToPb(macro: Macro): Resources.MacroBody {
val macroBuilder = Resources.MacroBody.newBuilder()
macroBuilder.rawString = macro.rawValue!!
val styleString = macroBuilder.styleStringBuilder
styleString.str = macro.styleString!!.str
for (span in macro.styleString!!.spans) {
val spansBuilder = styleString.addSpansBuilder()
spansBuilder.name = span.name
spansBuilder.startIndex = span.firstChar
spansBuilder.endIndex = span.lastChar
styleString.addSpans(spansBuilder.build())
}
macroBuilder.styleString = styleString.build()
for (untranslatables in macro.untranslatables) {
val section = macroBuilder.addUntranslatableSectionsBuilder()
section.startIndex = untranslatables.startIndex.toLong()
section.endIndex = untranslatables.endIndex.toLong()
macroBuilder.addUntranslatableSections(section.build())
}
for (aliasNamespace in macro.aliasNamespaces) {
val namespace = macroBuilder.addNamespaceStackBuilder()
namespace.prefix = aliasNamespace.alias
namespace.packageName = aliasNamespace.packageName
namespace.isPrivate = aliasNamespace.isPrivate
macroBuilder.addNamespaceStack(namespace.build())
}
return macroBuilder.build()
}
fun serializeItemToPb(item: Item, logger: ILogger?): Resources.Item {
val itemBuilder = Resources.Item.newBuilder()
when (item) {
is Reference -> itemBuilder.setRef(serializeReferenceToPb(item))
is BasicString -> itemBuilder.setStr(serializeStringToPb(item))
is RawString -> itemBuilder.setRawStr(serializeRawToPb(item))
is StyledString -> itemBuilder.setStyledStr(serializeStyledStrToPb(item))
is FileReference -> itemBuilder.setFile(serializeFileRefToPb(item))
is Id -> itemBuilder.setId(Resources.Id.newBuilder().build())
is BinaryPrimitive -> itemBuilder.setPrim(serializeBinPrimitiveToPb(item, logger))
else -> {
val errorMsg = "%s, Unrecognized item type %s, for value %s."
logger?.error(null, errorMsg, blameSource(item.source), item.javaClass, item)
}
}
return itemBuilder.build()
}
fun serializeValueToPb(value: Value, sourcePool: StringPool, logger: ILogger?): Resources.Value {
val valueBuilder = Resources.Value.newBuilder()
if (value is Item) {
valueBuilder.setItem(serializeItemToPb(value, logger))
} else {
val compoundBuilder = Resources.CompoundValue.newBuilder()
when (value) {
is AttributeResource -> compoundBuilder.setAttr(serializeAttrToPb(value, sourcePool))
is Style -> compoundBuilder.setStyle(serializeStyleToPb(value, sourcePool, logger))
is Styleable -> compoundBuilder.setStyleable(serializeStyleableToPb(value, sourcePool))
is ArrayResource -> compoundBuilder.setArray(serializeArrayToPb(value, sourcePool, logger))
is Plural -> compoundBuilder.setPlural(serializePluralToPb(value, sourcePool, logger))
is Macro -> compoundBuilder.setMacro(serializeMacroToPb(value))
else -> {
val errorMsg = "%s, Unrecognized value type %s, for value %s."
logger?.error(null, errorMsg, blameSource(value.source), value.javaClass, value)
}
}
valueBuilder.setCompoundValue(compoundBuilder.build())
}
if (value.source.isNotEmpty()){
valueBuilder.setSource(serializeSourceToPb(value.source, sourcePool))
}
return valueBuilder
.setComment(value.comment)
.setWeak(value.weak)
.build()
}
@@ -0,0 +1,47 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.aaptcompiler
import com.android.aaptcompiler.BlameLogger
import com.android.aaptcompiler.ResourceCompilerOptions
import com.android.aaptcompiler.compileResource
import com.android.utils.StdLogger
import com.android.utils.StdLogger.Level.VERBOSE
import java.nio.file.Paths
import kotlin.io.path.absolute
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
/** @author Akash Yadav */
@RunWith(JUnit4::class)
class CompilerTest {
@Test
fun `test simple compilatation`() {
val input =
Paths.get("../../app/src/main/res/layout/activity_editor.xml").absolute().normalize().toFile()
val output = Paths.get("./build").absolute().normalize().toFile()
println(input)
println(output)
val start = System.nanoTime()
compileResource(input, output, ResourceCompilerOptions(), BlameLogger(StdLogger(VERBOSE)))
println("Compiled in ${System.nanoTime() - start} nano seconds")
}
}
+1
View File
@@ -0,0 +1 @@
/build
+50
View File
@@ -0,0 +1,50 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
}
java {
sourceCompatibility = rootProject.ext.javaSourceVersion
targetCompatibility = rootProject.ext.javaTargetVersion
}
compileKotlin {
kotlinOptions {
jvmTarget = "11"
}
}
compileTestKotlin {
kotlinOptions {
jvmTarget = "11"
}
}
dependencies {
implementation common.jkotlin
implementation 'com.android.tools:annotations:30.2.2'
implementation 'net.sf.kxml:kxml2:2.3.0'
implementation 'org.jetbrains:annotations:13.0'
implementation google.guava_jre
testImplementation tests.junit
testImplementation tests.google_truth
}
@@ -0,0 +1 @@
#androidx.enableJetifier=false
@@ -0,0 +1,71 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.itsaky.androidide.layoutlib.resources;
public class AndroidConstants {
public static final String NULL_RESOURCE = "@null";
public static final String ANDROID_NS_NAME = "android";
public static final String ANDROID_URI = "http://schemas.android.com/apk/res/android";
public static final String TOOLS_NS_NAME = "tools";
public static final String TOOLS_URI = "http://schemas.android.com/tools";
public static final String URI_PREFIX = "http://schemas.android.com/apk/res/";
public static final String AAPT_URI = "http://schemas.android.com/aapt";
public static final String AUTO_URI = "http://schemas.android.com/apk/res-auto";
public static final String URI_DOMAIN_PREFIX = "http://schemas.android.com/";
public static final String TAG_DECLARE_STYLEABLE = "declare-styleable";
public static final String TAG_PUBLIC = "public";
public static final String TAG_EAT_COMMENT = "eat-comment";
public static final String TAG_ITEM = "item";
public static final String ATTR_TYPE = "type";
public static final String PREFIX_THEME_REF = "?";
public static final String PREFIX_RESOURCE_REF = "@";
public static final String REFERENCE_STYLE = "style/";
public static final String APP_PREFIX = "app";
public static final String ANDROID_PKG = "android";
public static final String ATTR_VISIBILITY = "visibility";
public static final String ATTR_LAYOUT_WIDTH = "layout_width";
public static final String ATTR_LAYOUT_HEIGHT = "layout_height";
public static final String VALUE_MATCH_PARENT = "match_parent";
public static final String DOT_XML = ".xml";
public static final String REFERENCE_NULL = AndroidConstants.NULL_RESOURCE;
public static final String REFERENCE_EMPTY = "@empty";
public static final String REFERENCE_UNDEFINED = "@undefined";
}
@@ -0,0 +1,58 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.itsaky.androidide.layoutlib.resources;
import com.android.annotations.NonNull;
import org.jetbrains.annotations.Nullable;
/**
* Represents Android quantities.
*
* @see <a href="https://developer.android.com/guide/topics/resources/string-resource#Plurals">Arity
* strings (plurals)</a>
*/
public enum Arity {
ZERO("zero"),
ONE("one"),
TWO("two"),
FEW("few"),
MANY("many"),
OTHER("other");
public static final Arity[] EMPTY_ARRAY = {};
@NonNull private final String name;
Arity(@NonNull String name) {
this.name = name;
}
@NonNull
public String getName() {
return name;
}
@Nullable
public static Arity getEnum(@NonNull String name) {
for (Arity value : values()) {
if (value.name.equals(name)) {
return value;
}
}
return null;
}
}
@@ -0,0 +1,197 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.itsaky.androidide.layoutlib.resources;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.google.common.collect.Maps;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;
/**
* Allowed values of screen density.
*
* <p>This enum is used in the manifest in the uses-configuration node and in the resource folder
* names as well as in other places that need to know the density values.
*/
public enum Density implements ResourceEnum {
XXXHIGH("xxxhdpi", "XXX-High Density", 640, 18), //$NON-NLS-1$
DPI_560("560dpi", "560 DPI Density", 560, 1), //$NON-NLS-1$
XXHIGH( "xxhdpi", "XX-High Density", 480, 16), //$NON-NLS-1$
DPI_440("440dpi", "440 DPI Density", 440, 28),
DPI_420("420dpi", "420 DPI Density", 420, 23), //$NON-NLS-1$
DPI_400("400dpi", "400 DPI Density", 400, 1), //$NON-NLS-1$
DPI_360("360dpi", "360 DPI Density", 360, 23), //$NON-NLS-1$
XHIGH( "xhdpi", "X-High Density", 320, 8), //$NON-NLS-1$
DPI_260("260dpi", "260 DPI Density", 260, 25), //$NON-NLS-1$
DPI_280("280dpi", "280 DPI Density", 280, 22), //$NON-NLS-1$
DPI_300("300dpi", "300 DPI Density", 300, 25), //$NON-NLS-1$
DPI_340("340dpi", "340 DPI Density", 340, 25), //$NON-NLS-1$
HIGH( "hdpi", "High Density", 240, 4), //$NON-NLS-1$
DPI_220("220dpi", "220 DPI Density", 220, 29),
TV( "tvdpi", "TV Density", 213, 13), //$NON-NLS-1$
DPI_200("200dpi", "200 DPI Density", 200, 29),
DPI_180("180dpi", "180 DPI Density", 180, 29),
MEDIUM( "mdpi", "Medium Density", 160, 4), //$NON-NLS-1$
DPI_140("140dpi", "140 DPI Density", 140, 29),
LOW( "ldpi", "Low Density", 120, 4), //$NON-NLS-1$
ANYDPI( "anydpi", "Any Density", 0xFFFE, 21), // 0xFFFE is the value used by the framework.
NODPI( "nodpi", "No Density", 0xFFFF, 4); // 0xFFFF is the value used by the framework.
public static final int DEFAULT_DENSITY = MEDIUM.getDpiValue();
private static final Map<String, Density> densityByValue =
Maps.newHashMapWithExpectedSize(values().length);
static {
for (Density density : values()) {
densityByValue.put(density.mValue, density);
}
}
@NonNull private final String mValue;
@NonNull private final String mDisplayValue;
private final int mDpi;
private final int mSince;
Density(@NonNull String value, @NonNull String displayValue, int density, int since) {
mValue = value;
mDisplayValue = displayValue;
mDpi = density;
mSince = since;
}
/**
* Returns the enum matching the provided qualifier value.
*
* @param value The qualifier value.
* @return the enum for the qualifier value or null if no match was found.
*/
@Nullable
public static Density getEnum(@Nullable String value) {
return densityByValue.get(value);
}
/**
* Returns the enum matching the given DPI value.
*
* @param dpiValue The density value.
* @return the enum for the density value or null if no match was found.
*/
@Nullable
public static Density getEnum(int dpiValue) {
Density[] densities = values();
for (Density density : densities) {
if (density.mDpi == dpiValue) {
return density;
}
}
return null;
}
@Override
@NonNull
public String getResourceValue() {
return mValue;
}
public int getDpiValue() {
return mDpi;
}
public int since() {
return mSince;
}
@Override
@NonNull
public String getShortDisplayValue() {
return mDisplayValue;
}
@Override
@NonNull
public String getLongDisplayValue() {
return mDisplayValue;
}
public static int getIndex(@Nullable Density value) {
return value == null ? -1 : value.ordinal();
}
@Nullable
public static Density getByIndex(int index) {
try {
return values()[index];
} catch (ArrayIndexOutOfBoundsException e) {
return null;
}
}
/**
* Returns all densities which are recommended and valid for a device.
*
* @see #isRecommended()
* @see #isValidValueForDevice()
*/
@NonNull
public static Set<Density> getRecommendedValuesForDevice() {
EnumSet<Density> result = EnumSet.noneOf(Density.class);
for (Density value : values()) {
if (value.isRecommended() && value.isValidValueForDevice()) {
result.add(value);
}
}
return result;
}
/**
* Returns true if this density is relevant for app developers (e.g.
* a density you should consider providing resources for)
*/
public boolean isRecommended() {
switch (this) {
case TV:
case DPI_140:
case DPI_180:
case DPI_200:
case DPI_220:
case DPI_260:
case DPI_280:
case DPI_300:
case DPI_340:
case DPI_360:
case DPI_400:
case DPI_420:
case DPI_440:
case DPI_560:
return false;
default:
return true;
}
}
@Override
public boolean isFakeValue() {
return false;
}
@Override
public boolean isValidValueForDevice() {
return this != NODPI && this != ANYDPI; // nodpi/anydpi is not a valid config for devices.
}
}
@@ -0,0 +1,273 @@
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.itsaky.androidide.layoutlib.resources;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
/**
* This class gives access to the bidirectional relationship between {@link ResourceType} and
* {@link ResourceFolderType}.
*/
public final class FolderTypeRelationship {
private static final Map<ResourceType, List<ResourceFolderType>> mTypeToFolderMap =
new EnumMap<>(ResourceType.class);
private static final Map<ResourceFolderType, List<ResourceType>> mFolderToTypeMap =
new EnumMap<>(ResourceFolderType.class);
static {
// generate the relationships in a temporary map
// Non-id resources which appear in non-value folders.
add(ResourceType.ANIM, ResourceFolderType.ANIM);
add(ResourceType.ANIMATOR, ResourceFolderType.ANIMATOR);
add(ResourceType.COLOR, ResourceFolderType.COLOR);
add(ResourceType.DRAWABLE, ResourceFolderType.DRAWABLE);
add(ResourceType.FONT, ResourceFolderType.FONT);
add(ResourceType.INTERPOLATOR, ResourceFolderType.INTERPOLATOR);
add(ResourceType.LAYOUT, ResourceFolderType.LAYOUT);
add(ResourceType.MENU, ResourceFolderType.MENU);
add(ResourceType.MIPMAP, ResourceFolderType.MIPMAP);
add(ResourceType.RAW, ResourceFolderType.RAW);
add(ResourceType.TRANSITION, ResourceFolderType.TRANSITION);
add(ResourceType.XML, ResourceFolderType.XML);
add(ResourceType.NAVIGATION, ResourceFolderType.NAVIGATION);
// Resource types which can appear in values/
add(ResourceType.ARRAY, ResourceFolderType.VALUES);
add(ResourceType.ATTR, ResourceFolderType.VALUES);
add(ResourceType.BOOL, ResourceFolderType.VALUES);
add(ResourceType.COLOR, ResourceFolderType.VALUES);
add(ResourceType.DIMEN, ResourceFolderType.VALUES);
add(ResourceType.DRAWABLE, ResourceFolderType.VALUES);
add(ResourceType.FRACTION, ResourceFolderType.VALUES);
add(ResourceType.ID, ResourceFolderType.VALUES);
add(ResourceType.INTEGER, ResourceFolderType.VALUES);
add(ResourceType.MACRO, ResourceFolderType.VALUES);
add(ResourceType.PLURALS, ResourceFolderType.VALUES);
add(ResourceType.PUBLIC, ResourceFolderType.VALUES);
add(ResourceType.STRING, ResourceFolderType.VALUES);
add(ResourceType.STYLE, ResourceFolderType.VALUES);
add(ResourceType.STYLE_ITEM, ResourceFolderType.VALUES);
add(ResourceType.STYLEABLE, ResourceFolderType.VALUES);
// Folders which can contain id declarations
// These are added to the map last to ensure that the first
// resource type we get out of a folder map is the primary
// resource type, not the id.
add(ResourceType.ID, ResourceFolderType.DRAWABLE);
add(ResourceType.ID, ResourceFolderType.LAYOUT);
add(ResourceType.ID, ResourceFolderType.MENU);
add(ResourceType.ID, ResourceFolderType.NAVIGATION);
add(ResourceType.ID, ResourceFolderType.TRANSITION);
add(ResourceType.ID, ResourceFolderType.XML);
makeSafe();
}
/**
* The ID-providing relationship is also encoded in the above maps by having a folder map to two
* ResourceTypes, with the second ResourceType being ID.
*/
private static final EnumSet<ResourceFolderType> ID_PROVIDING_RESOURCE_TYPES =
EnumSet.of(
ResourceFolderType.LAYOUT,
ResourceFolderType.MENU,
ResourceFolderType.DRAWABLE,
ResourceFolderType.XML,
ResourceFolderType.TRANSITION,
ResourceFolderType.NAVIGATION);
/**
* Returns a list of {@link ResourceType}s that can be generated from files inside a folder of
* the specified type.
*
* @param folderType The folder type.
* @return a list of {@link ResourceType}, possibly empty but never null.
*/
@NonNull
public static List<ResourceType> getRelatedResourceTypes(
@NonNull ResourceFolderType folderType) {
List<ResourceType> list = mFolderToTypeMap.get(folderType);
if (list != null) {
return list;
}
return Collections.emptyList();
}
/**
* Returns a single ResourceType that can be generated from files inside a folder of the
* specified type and which is not {@link ResourceType#ID}.
*
* @param folderType The folder type.
* @return a single non-ID {@link ResourceType}.
*/
@NonNull
public static ResourceType getNonIdRelatedResourceType(@NonNull ResourceFolderType folderType) {
List<ResourceType> resourceTypes = getRelatedResourceTypes(folderType);
if (resourceTypes.isEmpty()) {
throw new IllegalArgumentException(
// {@link FolderTypeRelationshipTest#testResourceFolderType} guarantees this
// should never happen
String.format(
"No resource types defined for given folder type: %s",
folderType.getName()));
}
ResourceType resourceType = resourceTypes.get(0);
if (resourceTypes.size() > 1 && resourceType == ResourceType.ID) {
resourceType = resourceTypes.get(1);
}
return resourceType;
}
/**
* Returns a list of {@link ResourceFolderType} that can contain files generating resources of
* the specified type.
*
* @param resType the type of resource.
* @return a list of {@link ResourceFolderType}, possibly empty but never null.
*/
@NonNull
public static List<ResourceFolderType> getRelatedFolders(@NonNull ResourceType resType) {
List<ResourceFolderType> list = mTypeToFolderMap.get(resType);
if (list != null) {
return list;
}
return Collections.emptyList();
}
/**
* Returns the {@link ResourceFolderType} corresponding to a given {@link ResourceType}, that is
* a folder that can contain files generating resources of the specified type and is not {@link
* ResourceFolderType#VALUES}, if one exists.
*
* <p>Returns null for {@link ResourceType#ID}, since there's not a single {@link
* ResourceFolderType} that contains only ids.
*/
@Nullable
public static ResourceFolderType getNonValuesRelatedFolder(@NonNull ResourceType resType) {
if (resType == ResourceType.ID) {
return null;
}
List<ResourceFolderType> list = mTypeToFolderMap.get(resType);
if (list != null) {
for (ResourceFolderType type : list) {
if (type != ResourceFolderType.VALUES) {
return type;
}
}
}
return null;
}
/**
* Check if a folder may contain ID generating types (via android:id="@+id/xyz").
* @param folderType The folder type.
* @return true if folder may contain ID generating types.
*/
public static boolean isIdGeneratingFolderType(@NonNull ResourceFolderType folderType) {
return ID_PROVIDING_RESOURCE_TYPES.contains(folderType);
}
@NonNull
public static Collection<ResourceFolderType> getIdGeneratingFolderTypes() {
return ID_PROVIDING_RESOURCE_TYPES;
}
/**
* Returns true if the {@link ResourceType} and the {@link ResourceFolderType} values match.
* @param resType the resource type.
* @param folderType the folder type.
* @return true if files inside the folder of the specified {@link ResourceFolderType}
* could generate a resource of the specified {@link ResourceType}
*/
public static boolean match(@NonNull ResourceType resType, @NonNull ResourceFolderType folderType) {
List<ResourceFolderType> list = mTypeToFolderMap.get(resType);
if (list != null) {
return list.contains(folderType);
}
return false;
}
/**
* Adds a {@link ResourceType} - {@link ResourceFolderType} relationship. this indicates that
* a file in the folder can generate a resource of the specified type.
* @param type The resourceType
* @param folder The {@link ResourceFolderType}
*/
private static void add(ResourceType type, ResourceFolderType folder) {
// first we add the folder to the list associated with the type.
List<ResourceFolderType> folderList = mTypeToFolderMap.get(type);
if (folderList == null) {
folderList = new ArrayList<>();
mTypeToFolderMap.put(type, folderList);
}
if (folderList.indexOf(folder) == -1) {
folderList.add(folder);
}
// now we add the type to the list associated with the folder.
List<ResourceType> typeList = mFolderToTypeMap.get(folder);
if (typeList == null) {
typeList = new ArrayList<>();
mFolderToTypeMap.put(folder, typeList);
}
if (typeList.indexOf(type) == -1) {
typeList.add(type);
}
}
/**
* Makes the maps safe by replacing the current list values with unmodifiable lists.
*/
private static void makeSafe() {
for (ResourceType type : ResourceType.values()) {
List<ResourceFolderType> list = mTypeToFolderMap.get(type);
if (list != null) {
// getNonValuesRelatedFolder above assumes every resource belongs in only one non-values folder.
assert type == ResourceType.ID || list.size() <= 2;
// replace with a unmodifiable list wrapper around the current list.
mTypeToFolderMap.put(type, Collections.unmodifiableList(list));
}
}
for (ResourceFolderType folder : ResourceFolderType.values()) {
List<ResourceType> list = mFolderToTypeMap.get(folder);
if (list != null) {
// replace with a unmodifiable list wrapper around the current list.
mFolderToTypeMap.put(folder, Collections.unmodifiableList(list));
}
}
}
}
@@ -0,0 +1,90 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.itsaky.androidide.layoutlib.resources;
/**
* High Dynamic Range
*
* <p>This is used in the resource folder names.
*/
public enum HighDynamicRange implements ResourceEnum {
HIGHDR("highdr", "High Dynamic Range", "High Dynamic Rage"),
LOWDR("lowdr", "Low Dynamic Range", "Low Dynamic Range");
private final String mValue;
private final String mShortDisplayValue;
private final String mLongDisplayValue;
HighDynamicRange(String value, String displayValue, String longDisplayValue) {
mValue = value;
mShortDisplayValue = displayValue;
mLongDisplayValue = longDisplayValue;
}
/**
* Returns the enum for matching the provided qualifier value.
*
* @param value The qualifier value.
* @return the enum for the qualifier value or null if no matching was found.
*/
public static HighDynamicRange getEnum(String value) {
for (HighDynamicRange orient : values()) {
if (orient.mValue.equals(value)) {
return orient;
}
}
return null;
}
@Override
public String getResourceValue() {
return mValue;
}
@Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
@Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
public static int getIndex(HighDynamicRange value) {
return value == null ? -1 : value.ordinal();
}
public static HighDynamicRange getByIndex(int index) {
HighDynamicRange[] values = values();
if (index >= 0 && index < values.length) {
return values[index];
}
return null;
}
@Override
public boolean isFakeValue() {
return false;
}
@Override
public boolean isValidValueForDevice() {
return true;
}
}
@@ -0,0 +1,92 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.itsaky.androidide.layoutlib.resources;
/**
* Keyboard enum.
* <p>This is used in the manifest in the uses-configuration node and in the resource folder names.
*/
public enum Keyboard implements ResourceEnum {
NOKEY("nokeys", null, "No Keys", "No keyboard"), //$NON-NLS-1$
QWERTY("qwerty", null, "Qwerty", "Qwerty keyboard"), //$NON-NLS-1$
TWELVEKEY("12key", "twelvekey", "12 Key", "12 key keyboard"); //$NON-NLS-1$ //$NON-NLS-2$
private final String mValue, mValue2;
private final String mShortDisplayValue;
private final String mLongDisplayValue;
Keyboard(String value, String value2, String shortDisplayValue,
String longDisplayValue) {
mValue = value;
mValue2 = value2;
mShortDisplayValue = shortDisplayValue;
mLongDisplayValue = longDisplayValue;
}
/**
* Returns the enum for matching the provided qualifier value.
* @param value The qualifier value.
* @return the enum for the qualifier value or null if no matching was found.
*/
public static Keyboard getEnum(String value) {
for (Keyboard kbrd : values()) {
if (kbrd.mValue.equals(value) ||
(kbrd.mValue2 != null && kbrd.mValue2.equals(value))) {
return kbrd;
}
}
return null;
}
@Override
public String getResourceValue() {
return mValue;
}
@Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
@Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
public static int getIndex(Keyboard value) {
return value == null ? -1 : value.ordinal();
}
public static Keyboard getByIndex(int index) {
Keyboard[] values = values();
if (index >= 0 && index < values.length) {
return values[index];
}
return null;
}
@Override
public boolean isFakeValue() {
return false;
}
@Override
public boolean isValidValueForDevice() {
return true;
}
}
@@ -0,0 +1,90 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.itsaky.androidide.layoutlib.resources;
/**
* Keyboard state enum.
* <p>This is used in the manifest in the uses-configuration node and in the resource folder names.
*/
public enum KeyboardState implements ResourceEnum {
EXPOSED("keysexposed", "Exposed", "Exposed keyboard"), //$NON-NLS-1$
HIDDEN("keyshidden", "Hidden", "Hidden keyboard"), //$NON-NLS-1$
SOFT("keyssoft", "Soft", "Soft keyboard"); //$NON-NLS-1$
private final String mValue;
private final String mShortDisplayValue;
private final String mLongDisplayValue;
KeyboardState(String value, String shortDisplayValue, String longDisplayValue) {
mValue = value;
mShortDisplayValue = shortDisplayValue;
mLongDisplayValue = longDisplayValue;
}
/**
* Returns the enum for matching the provided qualifier value.
* @param value The qualifier value.
* @return the enum for the qualifier value or null if no matching was found.
*/
public static KeyboardState getEnum(String value) {
for (KeyboardState state : values()) {
if (state.mValue.equals(value)) {
return state;
}
}
return null;
}
@Override
public String getResourceValue() {
return mValue;
}
@Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
@Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
public static int getIndex(KeyboardState value) {
return value == null ? -1 : value.ordinal();
}
public static KeyboardState getByIndex(int index) {
KeyboardState[] values = values();
if (index >= 0 && index < values.length) {
return values[index];
}
return null;
}
@Override
public boolean isFakeValue() {
return false;
}
@Override
public boolean isValidValueForDevice() {
return true;
}
}
@@ -0,0 +1,87 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.itsaky.androidide.layoutlib.resources;
/**
* Layout Direction enum.
*/
public enum LayoutDirection implements ResourceEnum {
LTR("ldltr", "LTR", "Left To Right"), //$NON-NLS-1$
RTL("ldrtl", "RTL", "Right To Left"); //$NON-NLS-1$
private final String mValue;
private final String mShortDisplayValue;
private final String mLongDisplayValue;
LayoutDirection(String value, String shortDisplayValue, String longDisplayValue) {
mValue = value;
mShortDisplayValue = shortDisplayValue;
mLongDisplayValue = longDisplayValue;
}
/**
* Returns the enum for matching the provided qualifier value.
* @param value The qualifier value.
* @return the enum for the qualifier value or null if no matching was found.
*/
public static LayoutDirection getEnum(String value) {
for (LayoutDirection orient : values()) {
if (orient.mValue.equals(value)) {
return orient;
}
}
return null;
}
@Override
public String getResourceValue() {
return mValue;
}
@Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
@Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
public static int getIndex(LayoutDirection orientation) {
return orientation == null ? -1 : orientation.ordinal();
}
public static LayoutDirection getByIndex(int index) {
LayoutDirection[] values = values();
if (index >= 0 && index < values.length) {
return values[index];
}
return null;
}
@Override
public boolean isFakeValue() {
return false;
}
@Override
public boolean isValidValueForDevice() {
return true;
}
}
@@ -0,0 +1,91 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.itsaky.androidide.layoutlib.resources;
/**
* Navigation enum.
* <p>This is used in the manifest in the uses-configuration node and in the resource folder names.
*/
public enum Navigation implements ResourceEnum {
NONAV("nonav", "None", "No navigation"), //$NON-NLS-1$
DPAD("dpad", "D-pad", "D-pad navigation"), //$NON-NLS-1$
TRACKBALL("trackball", "Trackball", "Trackball navigation"), //$NON-NLS-1$
WHEEL("wheel", "Wheel", "Wheel navigation"); //$NON-NLS-1$
private final String mValue;
private final String mShortDisplayValue;
private final String mLongDisplayValue;
Navigation(String value, String shortDisplayValue, String longDisplayValue) {
mValue = value;
mShortDisplayValue = shortDisplayValue;
mLongDisplayValue = longDisplayValue;
}
/**
* Returns the enum for matching the provided qualifier value.
* @param value The qualifier value.
* @return the enum for the qualifier value or null if no matching was found.
*/
public static Navigation getEnum(String value) {
for (Navigation nav : values()) {
if (nav.mValue.equals(value)) {
return nav;
}
}
return null;
}
@Override
public String getResourceValue() {
return mValue;
}
@Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
@Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
public static int getIndex(Navigation value) {
return value == null ? -1 : value.ordinal();
}
public static Navigation getByIndex(int index) {
Navigation[] values = values();
if (index >= 0 && index < values.length) {
return values[index];
}
return null;
}
@Override
public boolean isFakeValue() {
return false;
}
@Override
public boolean isValidValueForDevice() {
return true;
}
}
@@ -0,0 +1,89 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.itsaky.androidide.layoutlib.resources;
/**
* Navigation state enum.
* <p>This is used in the resource folder names.
*/
public enum NavigationState implements ResourceEnum {
EXPOSED("navexposed", "Exposed", "Exposed navigation"), //$NON-NLS-1$
HIDDEN("navhidden", "Hidden", "Hidden navigation"); //$NON-NLS-1$
private final String mValue;
private final String mShortDisplayValue;
private final String mLongDisplayValue;
NavigationState(String value, String shortDisplayValue, String longDisplayValue) {
mValue = value;
mShortDisplayValue = shortDisplayValue;
mLongDisplayValue = longDisplayValue;
}
/**
* Returns the enum for matching the provided qualifier value.
* @param value The qualifier value.
* @return the enum for the qualifier value or null if no matching was found.
*/
public static NavigationState getEnum(String value) {
for (NavigationState state : values()) {
if (state.mValue.equals(value)) {
return state;
}
}
return null;
}
@Override
public String getResourceValue() {
return mValue;
}
@Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
@Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
public static int getIndex(NavigationState value) {
return value == null ? -1 : value.ordinal();
}
public static NavigationState getByIndex(int index) {
NavigationState[] values = values();
if (index >= 0 && index < values.length) {
return values[index];
}
return null;
}
@Override
public boolean isFakeValue() {
return false;
}
@Override
public boolean isValidValueForDevice() {
return true;
}
}
@@ -0,0 +1,89 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.itsaky.androidide.layoutlib.resources;
/**
* Night enum.
* <p>This is used in the resource folder names.
*/
public enum NightMode implements ResourceEnum {
NOTNIGHT("notnight", "Not Night", "Day time"),
NIGHT("night", "Night", "Night time");
private final String mValue;
private final String mShortDisplayValue;
private final String mLongDisplayValue;
NightMode(String value, String shortDisplayValue, String longDisplayValue) {
mValue = value;
mShortDisplayValue = shortDisplayValue;
mLongDisplayValue = longDisplayValue;
}
/**
* Returns the enum for matching the provided qualifier value.
* @param value The qualifier value.
* @return the enum for the qualifier value or null if no matching was found.
*/
public static NightMode getEnum(String value) {
for (NightMode mode : values()) {
if (mode.mValue.equals(value)) {
return mode;
}
}
return null;
}
@Override
public String getResourceValue() {
return mValue;
}
@Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
@Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
public static int getIndex(NightMode value) {
return value == null ? -1 : value.ordinal();
}
public static NightMode getByIndex(int index) {
NightMode[] values = values();
if (index >= 0 && index < values.length) {
return values[index];
}
return null;
}
@Override
public boolean isFakeValue() {
return false;
}
@Override
public boolean isValidValueForDevice() {
return true;
}
}
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.itsaky.androidide.layoutlib.resources;
/**
* Resource constants.
*/
public class ResourceConstants {
/** Default anim resource folder name, i.e. "anim". */
public static final String FD_RES_ANIM = "anim";
/** Default animator resource folder name, i.e. "animator". */
public static final String FD_RES_ANIMATOR = "animator";
/** Default color resource folder name, i.e. "color". */
public static final String FD_RES_COLOR = "color";
/** Default drawable resource folder name, i.e. "drawable". */
public static final String FD_RES_DRAWABLE = "drawable";
/** Default font resource folder name, i.e. "font". */
public static final String FD_RES_FONT = "font";
/** Default interpolator resource folder name, i.e. "interpolator". */
public static final String FD_RES_INTERPOLATOR = "interpolator";
/** Default layout resource folder name, i.e. "layout". */
public static final String FD_RES_LAYOUT = "layout";
/** Default menu resource folder name, i.e. "menu". */
public static final String FD_RES_MENU = "menu";
/** Default menu resource folder name, i.e. "mipmap". */
public static final String FD_RES_MIPMAP = "mipmap";
/** Default values resource folder name, i.e. "values". */
public static final String FD_RES_VALUES = "values";
/** Default xml resource folder name, i.e. "xml". */
public static final String FD_RES_XML = "xml";
/** Default raw resource folder name, i.e. "raw". */
public static final String FD_RES_RAW = "raw";
/** Default transition resource folder name, i.e. "transition". */
public static final String FD_RES_TRANSITION = "transition";
/** Default navigation resource folder name, i.e. "nav". */
public static final String FD_NAVIGATION = "navigation";
/** Separator between the resource folder qualifiers. */
public static final char RES_QUALIFIER_SEP = '-';
}
@@ -0,0 +1,60 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.itsaky.androidide.layoutlib.resources;
/**
* An enum representing a resource qualifier value.
*/
public interface ResourceEnum {
/**
* Returns the resource string. This is to be used in resource folder names.
*/
String getResourceValue();
/**
* Whether the value actually used on device. This returns true only if a device can report
* this value, false if it's just used to qualify resources.
*/
boolean isValidValueForDevice();
/**
* Whether the value is neither used for device nor resources. This returns false when
* the value is only used for internal usage in the custom editors.
*/
boolean isFakeValue();
/**
* Returns a short string for display value. The string does not need to show the context.
* <p>For instance "exposed", which can be the value for the keyboard state or the navigation
* state, would be valid since something else in the UI is expected to show if this is about the
* keyboard or the navigation state.
*
* @see #getLongDisplayValue()
*/
String getShortDisplayValue();
/**
* Returns a long string for display value. This must not only include the enum value but
* context (qualifier) about what the value represents.
* <p>For instance "Exposed keyboard", and "Export navigation", as "exposed" would not be
* enough to know what qualifier the value is about.
*
* @see #getShortDisplayValue()
*/
String getLongDisplayValue();
}
@@ -0,0 +1,94 @@
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.itsaky.androidide.layoutlib.resources;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
/**
* Enum representing a type of resource folder.
*/
public enum ResourceFolderType {
ANIM(ResourceConstants.FD_RES_ANIM),
ANIMATOR(ResourceConstants.FD_RES_ANIMATOR),
COLOR(ResourceConstants.FD_RES_COLOR),
DRAWABLE(ResourceConstants.FD_RES_DRAWABLE),
FONT(ResourceConstants.FD_RES_FONT),
INTERPOLATOR(ResourceConstants.FD_RES_INTERPOLATOR),
LAYOUT(ResourceConstants.FD_RES_LAYOUT),
MENU(ResourceConstants.FD_RES_MENU),
MIPMAP(ResourceConstants.FD_RES_MIPMAP),
NAVIGATION(ResourceConstants.FD_NAVIGATION),
RAW(ResourceConstants.FD_RES_RAW),
TRANSITION(ResourceConstants.FD_RES_TRANSITION),
VALUES(ResourceConstants.FD_RES_VALUES),
XML(ResourceConstants.FD_RES_XML);
private final String name;
private static final Map<String, ResourceFolderType> nameToType;
static {
ResourceFolderType[] values = ResourceFolderType.values();
nameToType = new HashMap<>(2 * values.length);
for (ResourceFolderType type : values) {
nameToType.put(type.getName(), type);
}
}
ResourceFolderType(String name) {
this.name = name;
}
/**
* Returns the folder name for this resource folder type.
*/
@NonNull
public String getName() {
return name;
}
/**
* Returns the enum by name.
* @param name The enum string value.
* @return the enum or null if not found.
*/
@Nullable
public static ResourceFolderType getTypeByName(@NonNull String name) {
assert name.indexOf('-') == -1 : name; // use #getFolderType instead
return nameToType.get(name);
}
/**
* Returns the {@link ResourceFolderType} from the folder name
* @param folderName The name of the folder. This must be a valid folder name in the format
* <code>resType[-resqualifiers[-resqualifiers[...]]</code>
* @return the <code>ResourceFolderType</code> representing the type of the folder, or
* <code>null</code> if no matching type was found.
*/
@Nullable
public static ResourceFolderType getFolderType(@NonNull String folderName) {
int index = folderName.indexOf(ResourceConstants.RES_QUALIFIER_SEP);
if (index != -1) {
folderName = folderName.substring(0, index);
}
return getTypeByName(folderName);
}
}
@@ -0,0 +1,302 @@
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.itsaky.androidide.layoutlib.resources;
import static com.google.common.base.MoreObjects.firstNonNull;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import java.util.Arrays;
import java.util.Collection;
import java.util.function.BiFunction;
import java.util.function.Function;
/**
* Enum representing a type of compiled resource.
*
* <p>See {@code ResourceType} in aapt2/Resource.h.
*/
public enum ResourceType {
ANIM("anim", "Animation"),
ANIMATOR("animator", "Animator"),
ARRAY("array", "Array", "string-array", "integer-array"),
ATTR("attr", "Attr"),
BOOL("bool", "Boolean"),
COLOR("color", "Color"),
DIMEN("dimen", "Dimension"),
DRAWABLE("drawable", "Drawable"),
FONT("font", "Font"),
FRACTION("fraction", "Fraction"),
ID("id", "ID"),
INTEGER("integer", "Integer"),
INTERPOLATOR("interpolator", "Interpolator"),
LAYOUT("layout", "Layout"),
MENU("menu", "Menu"),
MIPMAP("mipmap", "Mip Map"),
NAVIGATION("navigation", "Navigation"),
PLURALS("plurals", "Plurals"),
RAW("raw", "Raw"),
STRING("string", "String"),
STYLE("style", "Style"),
STYLEABLE("styleable", "Styleable", Kind.STYLEABLE),
TRANSITION("transition", "Transition"),
XML("xml", "XML"),
/**
* This is not actually used. Only there because they get parsed and since we want to detect new
* resource type, we need to have this one exist.
*/
PUBLIC("public", "Public visibility modifier", Kind.SYNTHETIC),
/**
* This type is used for elements dynamically generated by the parsing of aapt:attr nodes. The
* "aapt:attr" allow to inline resources as part of a different resource, for example, a drawable
* as part of a layout. When the parser, encounters one of this nodes, it will generate a
* synthetic _aapt attr reference.
*/
AAPT("_aapt", "Aapt Attribute", Kind.SYNTHETIC),
/**
* This tag is used for marking a resource overlayable, i.e. that it can be overlaid at runtime by
* RROs (Runtime Resource Overlays). This is a new feature supported starting Android 10. This tag
* (and the content following it in that node) does not define a resource.
*/
OVERLAYABLE("overlayable", "Overlayable tag", Kind.SYNTHETIC),
/** Represents item tags inside a style definition. */
STYLE_ITEM("item", "Style Item", Kind.SYNTHETIC),
/**
* Not an actual resource type from AAPT. Used to provide sample data values in the tools
* namespace
*/
SAMPLE_DATA("sample", "Sample data", Kind.SYNTHETIC),
/**
* Not a real resource, but a way of defining a resource reference that will be replaced with its
* actual value during linking. Does not exist at runtime, nor does it appear in the R class. Only
* present in raw and flat resources.
*/
MACRO("macro", "Macro resource replacement", Kind.SYNTHETIC),
;
/** The set of all types of resources that can be referenced by other resources. */
public static final ImmutableSet<ResourceType> REFERENCEABLE_TYPES;
private static final ImmutableMap<String, ResourceType> TAG_NAMES;
private static final ImmutableMap<String, ResourceType> CLASS_NAMES;
static {
ImmutableMap.Builder<String, ResourceType> tagNames = ImmutableMap.builder();
tagNames.put(AndroidConstants.TAG_DECLARE_STYLEABLE, STYLEABLE);
tagNames.put(AndroidConstants.TAG_PUBLIC, PUBLIC);
tagNames.put(OVERLAYABLE.getName(), OVERLAYABLE);
tagNames.put(MACRO.getName(), MACRO);
ImmutableMap.Builder<String, ResourceType> classNames = ImmutableMap.builder();
classNames.put(STYLEABLE.mName, STYLEABLE);
for (ResourceType type : ResourceType.values()) {
if (type.mKind != Kind.REAL || type == STYLEABLE) {
continue;
}
classNames.put(type.getName(), type);
tagNames.put(type.getName(), type);
for (String alternateName : type.mAlternateXmlNames) {
tagNames.put(alternateName, type);
}
}
TAG_NAMES = tagNames.build();
CLASS_NAMES = classNames.build();
REFERENCEABLE_TYPES =
Arrays.stream(values())
.filter(ResourceType::getCanBeReferenced)
.collect(Sets.toImmutableEnumSet());
}
@NonNull private final String mName;
@NonNull private final Kind mKind;
@NonNull private final String mDisplayName;
@NonNull private final String[] mAlternateXmlNames;
ResourceType(
@NonNull String name, @NonNull String displayName, @NonNull String... alternateXmlNames) {
mName = name;
mKind = Kind.REAL;
mDisplayName = displayName;
mAlternateXmlNames = alternateXmlNames;
}
ResourceType(@NonNull String name, @NonNull String displayName, @NonNull Kind kind) {
mName = name;
mKind = kind;
mDisplayName = displayName;
mAlternateXmlNames = new String[0];
}
/**
* Returns the enum by its name as it appears as a folder name under {@code res/}.
*
* @param folderName name of the inner class of the R class, e.g. "drawable" or "color".
*/
@Nullable
public static ResourceType fromFolderName(@NonNull String folderName) {
return CLASS_NAMES.get(folderName);
}
@Nullable
public static ResourceType fromXmlValue(@NonNull String xmlValue) {
if (xmlValue.equals(AndroidConstants.TAG_DECLARE_STYLEABLE)
|| xmlValue.equals(STYLEABLE.mName)) {
return null;
}
if (xmlValue.equals(SAMPLE_DATA.mName)) {
return SAMPLE_DATA;
}
if (xmlValue.equals(AAPT.mName)) {
return AAPT;
}
if (xmlValue.equals(OVERLAYABLE.mName)) {
return OVERLAYABLE;
}
if (xmlValue.equals(MACRO.mName)) {
return MACRO;
}
return CLASS_NAMES.get(xmlValue);
}
@Nullable
public static ResourceType fromXmlTag(@NonNull Node domNode) {
if (!(domNode instanceof Element)) {
return null;
}
Element tag = (Element) domNode;
return fromXmlTag(
tag,
element -> firstNonNull(element.getLocalName(), element.getTagName()),
Element::getAttribute);
}
@Nullable
public static <T> ResourceType fromXmlTag(
@NonNull T tag,
@NonNull Function<T, String> nameFunction,
@NonNull BiFunction<? super T, ? super String, String> attributeFunction) {
String tagName = nameFunction.apply(tag);
switch (tagName) {
case AndroidConstants.TAG_EAT_COMMENT:
return null;
case AndroidConstants.TAG_ITEM:
String typeAttribute = attributeFunction.apply(tag, AndroidConstants.ATTR_TYPE);
if (!Strings.isNullOrEmpty(typeAttribute)) {
return fromClassName(typeAttribute);
} else {
return null;
}
default:
return fromXmlTagName(tagName);
}
}
/**
* Returns the enum by its name as it appears in the R class.
*
* @param className name of the inner class of the R class, e.g. "string" or "styleable".
*/
@Nullable
public static ResourceType fromClassName(@NonNull String className) {
return CLASS_NAMES.get(className);
}
/**
* Returns the enum by its name as it appears in XML as a tag name.
*
* @param tagName name of the XML tag, e.g. "string" or "declare-styleable".
*/
@Nullable
public static ResourceType fromXmlTagName(@NonNull String tagName) {
return TAG_NAMES.get(tagName);
}
public static Collection<String> getClassNames() {
return CLASS_NAMES.keySet();
}
/** Returns a translated display name for the resource type. */
@NonNull
public String getDisplayName() {
return mDisplayName;
}
/**
* Returns true if the generated R class contains an inner class for this {@link ResourceType}.
*/
public boolean getHasInnerClass() {
return mKind != Kind.SYNTHETIC;
}
public boolean getCanBeReferenced() {
return (mKind == Kind.REAL && this != ATTR) || this == MACRO;
}
/** Returns true if this type is a synthetic type, such as {@link #PUBLIC} */
public boolean isSynthetic() {
return mKind == Kind.SYNTHETIC;
}
@Override
@NonNull
public String toString() {
// Unfortunately we still have code that relies on toString() returning the aapt name.
return getName();
}
/** Returns the resource type name, as used by XML files. */
@NonNull
public String getName() {
return mName;
}
private enum Kind {
/** These types are used both in the R and as XML tag names. */
REAL,
/**
* Styleables are handled by aapt but don't end up in the resource table. They have an R inner
* class (called {@code styleable}), are declared in XML (using {@code declare-styleable}) but
* cannot be referenced from XML.
*/
STYLEABLE,
/**
* Other types that are not known to aapt, but are used by tools to represent some information
* in the resources system.
*/
SYNTHETIC,
}
}
@@ -0,0 +1,92 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.itsaky.androidide.layoutlib.resources;
import com.android.annotations.NonNull;
/**
* An enum representing visibility of an android resource.
*
* <p>The below is the description of how AAPT2 understands resource visibility:
*
* <p>Public visibility.
*
* <p>A resource can be marked as public by adding the {@code public} element to a values XML file:
*
* <p>{@code <public name="my_public_string" type="string"/>}.
*
* <p>These elements are usually defined in the {@code public.xml} file inside the {@code
* res/values} directory.
*
* <p>Private visibility.
*
* <p>Sometimes a libraries can have a great number of resources and it might be confusing or
* tiresome to find the correct one when writing the Java or Kotlin code. In order to restrict the
* resources visible from the source code, one can list the only resources they want to be visible
* by using the {@code java-symbol} element:
*
* <p>{@code <java-symbol name="my_visible_string" type="string/>}.
*
* <p>These elements are usually defined in the {@code symbols.xml} file inside the {@code
* res/values} directory. The name {@code private} comes from these resources being present in the
* {@code private R.java} along with {@code public} resources.
*
* <p>Private XML only visibility.
*
* <p>All resources that were not marked as {@code public} or {@code private} have the {@code
* default} visibility. They are not placed in either {@code public} nor {@code private} R classes
* and are only accessible from other XML resources within that library/module.
*
* <p>Without the package for the private R.java specified, only the public R.java will be generated
* and it will contain all resources (ones marked as public, java-symbol and those not marked as
* either).
*
* <p>Additionally in the Gradle plugin we have the last enum value, {@code UNDEFINED}. It
* represents a case where the visibility was not defined at all. This should be only used in the
* case when we are not generating the public and private R class.
*/
public enum ResourceVisibility {
PRIVATE_XML_ONLY("default"),
PRIVATE("private"),
PUBLIC("public"),
UNDEFINED("undefined");
private final String qualifier;
ResourceVisibility(String qualifier) {
this.qualifier = qualifier;
}
public String getName() {
return qualifier;
}
public static ResourceVisibility getEnum(@NonNull String qualifier) {
for (ResourceVisibility accessibility : values()) {
if (accessibility.qualifier.equals(qualifier)) {
return accessibility;
}
}
return null;
}
public static ResourceVisibility max(
@NonNull ResourceVisibility v1, @NonNull ResourceVisibility v2) {
return v1.compareTo(v2) <= 0 ? v2 : v1;
}
}
@@ -0,0 +1,99 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.itsaky.androidide.layoutlib.resources;
/**
* Screen Orientation enum.
* <p>This is used in the manifest in the uses-configuration node and in the resource folder names.
*/
public enum ScreenOrientation implements ResourceEnum {
PORTRAIT("port", "Portrait", "Portrait Orientation"), //$NON-NLS-1$
LANDSCAPE("land", "Landscape", "Landscape Orientation"), //$NON-NLS-1$
SQUARE("square", "Square", "Square Orientation"); //$NON-NLS-1$
private final String mValue;
private final String mShortDisplayValue;
private final String mLongDisplayValue;
ScreenOrientation(String value, String shortDisplayValue, String longDisplayValue) {
mValue = value;
mShortDisplayValue = shortDisplayValue;
mLongDisplayValue = longDisplayValue;
}
/**
* Returns the enum for matching the provided qualifier value.
* @param value The qualifier value.
* @return the enum for the qualifier value or null if no matching was found.
*/
public static ScreenOrientation getEnum(String value) {
for (ScreenOrientation orient : values()) {
if (orient.mValue.equals(value)) {
return orient;
}
}
return null;
}
@Override
public String getResourceValue() {
return mValue;
}
@Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
@Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
public static int getIndex(ScreenOrientation orientation) {
return orientation == null ? -1 : orientation.ordinal();
}
public static ScreenOrientation getByIndex(int index) {
ScreenOrientation[] values = values();
if (index >=0 && index < values.length) {
return values[index];
}
return null;
}
public static ScreenOrientation getByShortDisplayName(String name) {
for (ScreenOrientation orientation : values()) {
if (orientation.getShortDisplayValue().equalsIgnoreCase(name)) {
return orientation;
}
}
return null;
}
@Override
public boolean isFakeValue() {
return false;
}
@Override
public boolean isValidValueForDevice() {
return true;
}
}
@@ -0,0 +1,90 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.itsaky.androidide.layoutlib.resources;
/**
* Screen Ratio enum.
* <p>This is used in the manifest in the uses-configuration node and in the resource folder names.
*/
public enum ScreenRatio implements ResourceEnum {
NOTLONG("notlong", "Not Long", "Short screen aspect ratio"), //$NON-NLS-1$
LONG("long", "Long", "Long screen aspect ratio"); //$NON-NLS-1$
private final String mValue;
private final String mShortDisplayValue;
private final String mLongDisplayValue;
ScreenRatio(String value, String displayValue, String longDisplayValue) {
mValue = value;
mShortDisplayValue = displayValue;
mLongDisplayValue = longDisplayValue;
}
/**
* Returns the enum for matching the provided qualifier value.
* @param value The qualifier value.
* @return the enum for the qualifier value or null if no matching was found.
*/
public static ScreenRatio getEnum(String value) {
for (ScreenRatio orient : values()) {
if (orient.mValue.equals(value)) {
return orient;
}
}
return null;
}
@Override
public String getResourceValue() {
return mValue;
}
@Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
@Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
public static int getIndex(ScreenRatio value) {
return value == null ? -1 : value.ordinal();
}
public static ScreenRatio getByIndex(int index) {
ScreenRatio[] values = values();
if (index >= 0 && index < values.length) {
return values[index];
}
return null;
}
@Override
public boolean isFakeValue() {
return false;
}
@Override
public boolean isValidValueForDevice() {
return true;
}
}
@@ -0,0 +1,90 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.itsaky.androidide.layoutlib.resources;
/**
* Screen Round enum.
* <p>This is used in the resource folder names.
*/
public enum ScreenRound implements ResourceEnum {
NOTROUND("notround", "Not Round", "Not Round screen"), //$NON-NLS-1$
ROUND( "round", "Round", "Round screen"); //$NON-NLS-1$
private final String mValue;
private final String mShortDisplayValue;
private final String mLongDisplayValue;
ScreenRound(String value, String displayValue, String longDisplayValue) {
mValue = value;
mShortDisplayValue = displayValue;
mLongDisplayValue = longDisplayValue;
}
/**
* Returns the enum for matching the provided qualifier value.
* @param value The qualifier value.
* @return the enum for the qualifier value or null if no matching was found.
*/
public static ScreenRound getEnum(String value) {
for (ScreenRound orient : values()) {
if (orient.mValue.equals(value)) {
return orient;
}
}
return null;
}
@Override
public String getResourceValue() {
return mValue;
}
@Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
@Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
public static int getIndex(ScreenRound value) {
return value == null ? -1 : value.ordinal();
}
public static ScreenRound getByIndex(int index) {
ScreenRound[] values = values();
if (index >= 0 && index < values.length) {
return values[index];
}
return null;
}
@Override
public boolean isFakeValue() {
return false;
}
@Override
public boolean isValidValueForDevice() {
return true;
}
}
@@ -0,0 +1,121 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.itsaky.androidide.layoutlib.resources;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
/**
* Screen size enum.
* <p>This is used in the manifest in the uses-configuration node and in the resource folder names.
*/
public enum ScreenSize implements ResourceEnum {
SMALL("small", "Small", "Small Screen"), //$NON-NLS-1$
NORMAL("normal", "Normal", "Normal Screen"), //$NON-NLS-1$
LARGE("large", "Large", "Large Screen"), //$NON-NLS-1$
XLARGE("xlarge", "X-Large", "Extra Large Screen"); //$NON-NLS-1$
private final String mValue;
private final String mShortDisplayValue;
private final String mLongDisplayValue;
ScreenSize(String value, String shortDisplayValue, String longDisplayValue) {
mValue = value;
mShortDisplayValue = shortDisplayValue;
mLongDisplayValue = longDisplayValue;
}
/**
* Returns the enum for matching the provided qualifier value.
* @param value The qualifier value.
* @return the enum for the qualifier value or null if no matching was found.
*/
public static ScreenSize getEnum(String value) {
for (ScreenSize orient : values()) {
if (orient.mValue.equals(value)) {
return orient;
}
}
return null;
}
@Override
public String getResourceValue() {
return mValue;
}
@Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
@Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
public static int getIndex(@Nullable ScreenSize value) {
return value == null ? -1 : value.ordinal();
}
public static ScreenSize getByIndex(int index) {
ScreenSize[] values = values();
if (index >= 0 && index < values.length) {
return values[index];
}
return null;
}
/**
* Get the resource bucket value that corresponds to the given size in inches.
*
* @param diagonalSize Diagonal Screen size in inches.
* If null, a default diagonal size is used
*/
@NonNull
public static ScreenSize getScreenSize(@Nullable Double diagonalSize) {
if (diagonalSize == null) {
return ScreenSize.NORMAL;
}
// Density-independent pixel (dp) : The density-independent pixel is
// equivalent to one physical pixel on a 160 dpi screen,
// which is the baseline density assumed by the system for a
// "medium" density screen.
// Android 8.1 Compatibility Definition, section 7.1
double diagonalDp = 160.0 * diagonalSize;
// Set the Screen Size
if (diagonalDp >= 1200) return XLARGE;
if (diagonalDp >= 800) return LARGE;
if (diagonalDp >= 568) return NORMAL;
return SMALL;
}
@Override
public boolean isFakeValue() {
return false;
}
@Override
public boolean isValidValueForDevice() {
return true;
}
}
@@ -0,0 +1,90 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.itsaky.androidide.layoutlib.resources;
/**
* Touch screen enum.
* <p>This is used in the manifest in the uses-configuration node and in the resource folder names.
*/
public enum TouchScreen implements ResourceEnum {
NOTOUCH("notouch", "No Touch", "No-touch screen"), //$NON-NLS-1$
STYLUS("stylus", "Stylus", "Stylus-based touchscreen"), //$NON-NLS-1$
FINGER("finger", "Finger", "Finger-based touchscreen"); //$NON-NLS-1$
private final String mValue;
private final String mShortDisplayValue;
private final String mLongDisplayValue;
TouchScreen(String value, String displayValue, String longDisplayValue) {
mValue = value;
mShortDisplayValue = displayValue;
mLongDisplayValue = longDisplayValue;
}
/**
* Returns the enum for matching the provided qualifier value.
* @param value The qualifier value.
* @return the enum for the qualifier value or null if no matching was found.
*/
public static TouchScreen getEnum(String value) {
for (TouchScreen orient : values()) {
if (orient.mValue.equals(value)) {
return orient;
}
}
return null;
}
@Override
public String getResourceValue() {
return mValue;
}
@Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
@Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
public static int getIndex(TouchScreen touch) {
return touch == null ? -1 : touch.ordinal();
}
public static TouchScreen getByIndex(int index) {
TouchScreen[] values = values();
if (index >= 0 && index < values.length) {
return values[index];
}
return null;
}
@Override
public boolean isFakeValue() {
return false;
}
@Override
public boolean isValidValueForDevice() {
return true;
}
}
@@ -0,0 +1,97 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.itsaky.androidide.layoutlib.resources;
/**
* UI Mode enum.
* <p>This is used in the resource folder names.
*/
public enum UiMode implements ResourceEnum {
NORMAL("", "Normal", 1),
CAR("car", "Car Dock", 8),
DESK("desk", "Desk Dock", 8),
TELEVISION("television", "Television", 13),
APPLIANCE("appliance", "Appliance", 16),
WATCH("watch", "Watch", 20),
VR_HEADSET("vrheadset", "VR Headset", 26);
private final String mValue;
private final String mDisplayValue;
private final int mSince;
UiMode(String value, String display, int since) {
mValue = value;
mDisplayValue = display;
mSince = since;
}
/**
* Returns the enum for matching the provided qualifier value.
* @param value The qualifier value.
* @return the enum for the qualifier value or null if no matching was found.
*/
public static UiMode getEnum(String value) {
for (UiMode mode : values()) {
if (mode.mValue.equals(value)) {
return mode;
}
}
return null;
}
@Override
public String getResourceValue() {
return mValue;
}
public int since() {
return mSince;
}
@Override
public String getShortDisplayValue() {
return mDisplayValue;
}
@Override
public String getLongDisplayValue() {
return mDisplayValue;
}
public static int getIndex(UiMode value) {
return value == null ? -1 : value.ordinal();
}
public static UiMode getByIndex(int index) {
UiMode[] values = values();
if (index >= 0 && index < values.length) {
return values[index];
}
return null;
}
@Override
public boolean isFakeValue() {
return this == NORMAL; // NORMAL is not a real enum. it's used for internal state only.
}
@Override
public boolean isValidValueForDevice() {
return this != NORMAL;
}
}
@@ -0,0 +1,90 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.itsaky.androidide.layoutlib.resources;
/**
* Wide Color Gamut
*
* <p>This is used in the resource folder names.
*/
public enum WideGamutColor implements ResourceEnum {
WIDECG("widecg", "Wide Color Gamut", "Wide Color Gamut"),
NOWIDECG("nowidecg", "No Wide Color Gamut", "No Wide Color Gamut");
private final String mValue;
private final String mShortDisplayValue;
private final String mLongDisplayValue;
WideGamutColor(String value, String displayValue, String longDisplayValue) {
mValue = value;
mShortDisplayValue = displayValue;
mLongDisplayValue = longDisplayValue;
}
/**
* Returns the enum for matching the provided qualifier value.
*
* @param value The qualifier value.
* @return the enum for the qualifier value or null if no matching was found.
*/
public static WideGamutColor getEnum(String value) {
for (WideGamutColor orient : values()) {
if (orient.mValue.equals(value)) {
return orient;
}
}
return null;
}
@Override
public String getResourceValue() {
return mValue;
}
@Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
@Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
public static int getIndex(WideGamutColor value) {
return value == null ? -1 : value.ordinal();
}
public static WideGamutColor getByIndex(int index) {
WideGamutColor[] values = values();
if (index >= 0 && index < values.length) {
return values[index];
}
return null;
}
@Override
public boolean isFakeValue() {
return false;
}
@Override
public boolean isValidValueForDevice() {
return true;
}
}
+1
View File
@@ -15,3 +15,4 @@ org.gradle.jvmargs=-Xmx2048M --add-opens java.base/java.lang=ALL-UNNAMED --add-o
android.useAndroidX=true
android.enableJetifier=true
androidide.tooling.supportedAgpVersion=7.2.0
android.jetifier.ignorelist=common-30.2.2.jar
+2
View File
@@ -37,3 +37,5 @@ include ':build-tools:jsonrpc'
include ':build-tools:projects'
include ':build-tools:tooling-api-testing'
include ':lookup'
include ':build-tools:aaptcompiler'
include ':build-tools:layoutlib-api'
Binary file not shown.