mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
Added the aaptcompiler and layoutlib-api implementation
This commit is contained in:
Generated
+3
@@ -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" />
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -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
|
||||
+94
@@ -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)
|
||||
}
|
||||
+114
@@ -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)
|
||||
}
|
||||
}
|
||||
+232
@@ -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
|
||||
}
|
||||
Executable
+335
@@ -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
|
||||
|
||||
+138
@@ -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
|
||||
}
|
||||
}
|
||||
+298
@@ -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
|
||||
)
|
||||
+679
@@ -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))
|
||||
}
|
||||
+146
@@ -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
|
||||
}
|
||||
}
|
||||
+268
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+414
@@ -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() != "<") {
|
||||
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()
|
||||
}
|
||||
}
|
||||
Executable
+37
@@ -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)
|
||||
+382
@@ -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)
|
||||
}
|
||||
+95
@@ -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())
|
||||
+106
@@ -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"
|
||||
}
|
||||
+737
@@ -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)
|
||||
+642
@@ -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))
|
||||
}
|
||||
+561
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -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("")
|
||||
}
|
||||
}
|
||||
+431
@@ -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}
|
||||
}
|
||||
}
|
||||
|
||||
+2115
File diff suppressed because it is too large
Load Diff
+21
@@ -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"
|
||||
}
|
||||
+714
@@ -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
|
||||
}
|
||||
+535
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.android.aaptcompiler
|
||||
|
||||
import com.android.aapt.Resources
|
||||
|
||||
data class XmlResource (
|
||||
val file: ResourceFile, val xmlProto: Resources.XmlNode)
|
||||
+211
@@ -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 = ""
|
||||
}
|
||||
}
|
||||
+371
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+444
@@ -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
|
||||
}
|
||||
|
||||
+266
@@ -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;
|
||||
}
|
||||
}
|
||||
Executable
+2316
File diff suppressed because it is too large
Load Diff
+28
@@ -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>"
|
||||
}
|
||||
Executable
+1801
File diff suppressed because it is too large
Load Diff
+659
@@ -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())
|
||||
}
|
||||
+73
@@ -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
|
||||
+188
@@ -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
|
||||
}
|
||||
}
|
||||
Executable
+806
@@ -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
|
||||
}
|
||||
+719
@@ -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()
|
||||
}
|
||||
+47
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -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
|
||||
+71
@@ -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";
|
||||
}
|
||||
+58
@@ -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;
|
||||
}
|
||||
}
|
||||
+197
@@ -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.
|
||||
}
|
||||
}
|
||||
+273
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+90
@@ -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;
|
||||
}
|
||||
}
|
||||
+92
@@ -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;
|
||||
}
|
||||
}
|
||||
+90
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+87
@@ -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;
|
||||
}
|
||||
}
|
||||
+91
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+89
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+89
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+53
@@ -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 = '-';
|
||||
}
|
||||
+60
@@ -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();
|
||||
}
|
||||
+94
@@ -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);
|
||||
}
|
||||
}
|
||||
+302
@@ -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,
|
||||
}
|
||||
}
|
||||
+92
@@ -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;
|
||||
}
|
||||
}
|
||||
+99
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+90
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+90
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+121
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+90
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+97
@@ -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;
|
||||
}
|
||||
}
|
||||
+90
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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.
Reference in New Issue
Block a user