| // Copyright 2017 The Bazel Authors. All rights reserved. |
| // |
| // 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.google.devtools.build.android; |
| |
| import static com.google.common.base.Predicates.not; |
| import static com.google.common.base.Verify.verify; |
| import static java.util.stream.Collectors.toList; |
| |
| import android.aapt.pb.internal.ResourcesInternal.CompiledFile; |
| import com.android.aapt.ConfigurationOuterClass.Configuration; |
| import com.android.aapt.ConfigurationOuterClass.Configuration.KeysHidden; |
| import com.android.aapt.ConfigurationOuterClass.Configuration.NavHidden; |
| import com.android.aapt.ConfigurationOuterClass.Configuration.Orientation; |
| import com.android.aapt.ConfigurationOuterClass.Configuration.ScreenLayoutLong; |
| import com.android.aapt.ConfigurationOuterClass.Configuration.ScreenLayoutSize; |
| import com.android.aapt.ConfigurationOuterClass.Configuration.Touchscreen; |
| import com.android.aapt.ConfigurationOuterClass.Configuration.UiModeNight; |
| import com.android.aapt.ConfigurationOuterClass.Configuration.UiModeType; |
| import com.android.aapt.Resources; |
| import com.android.aapt.Resources.ConfigValue; |
| import com.android.aapt.Resources.Package; |
| import com.android.aapt.Resources.ResourceTable; |
| import com.android.aapt.Resources.Value; |
| import com.android.aapt.Resources.XmlNode; |
| import com.android.ide.common.resources.configuration.CountryCodeQualifier; |
| import com.android.ide.common.resources.configuration.DensityQualifier; |
| import com.android.ide.common.resources.configuration.FolderConfiguration; |
| import com.android.ide.common.resources.configuration.KeyboardStateQualifier; |
| import com.android.ide.common.resources.configuration.LayoutDirectionQualifier; |
| import com.android.ide.common.resources.configuration.LocaleQualifier; |
| import com.android.ide.common.resources.configuration.NavigationMethodQualifier; |
| import com.android.ide.common.resources.configuration.NavigationStateQualifier; |
| import com.android.ide.common.resources.configuration.NetworkCodeQualifier; |
| import com.android.ide.common.resources.configuration.NightModeQualifier; |
| import com.android.ide.common.resources.configuration.ResourceQualifier; |
| import com.android.ide.common.resources.configuration.ScreenDimensionQualifier; |
| import com.android.ide.common.resources.configuration.ScreenHeightQualifier; |
| import com.android.ide.common.resources.configuration.ScreenOrientationQualifier; |
| import com.android.ide.common.resources.configuration.ScreenRatioQualifier; |
| import com.android.ide.common.resources.configuration.ScreenRoundQualifier; |
| import com.android.ide.common.resources.configuration.ScreenSizeQualifier; |
| import com.android.ide.common.resources.configuration.ScreenWidthQualifier; |
| import com.android.ide.common.resources.configuration.SmallestScreenWidthQualifier; |
| import com.android.ide.common.resources.configuration.TextInputMethodQualifier; |
| import com.android.ide.common.resources.configuration.TouchScreenQualifier; |
| import com.android.ide.common.resources.configuration.UiModeQualifier; |
| import com.android.ide.common.resources.configuration.VersionQualifier; |
| import com.android.resources.Density; |
| import com.android.resources.Keyboard; |
| import com.android.resources.KeyboardState; |
| import com.android.resources.LayoutDirection; |
| import com.android.resources.Navigation; |
| import com.android.resources.NavigationState; |
| import com.android.resources.NightMode; |
| import com.android.resources.ResourceType; |
| import com.android.resources.ScreenOrientation; |
| import com.android.resources.ScreenRatio; |
| import com.android.resources.ScreenRound; |
| import com.android.resources.ScreenSize; |
| import com.android.resources.TouchScreen; |
| import com.android.resources.UiMode; |
| import com.google.auto.value.AutoValue; |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Stopwatch; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.hash.HashCode; |
| import com.google.common.hash.Hashing; |
| import com.google.common.io.ByteStreams; |
| import com.google.common.io.LittleEndianDataInputStream; |
| import com.google.devtools.build.android.aapt2.CompiledResources; |
| import com.google.devtools.build.android.aapt2.ResourceCompiler; |
| import com.google.devtools.build.android.proto.SerializeFormat; |
| import com.google.devtools.build.android.proto.SerializeFormat.Header; |
| import com.google.devtools.build.android.resources.Visibility; |
| import com.google.devtools.build.android.xml.ResourcesAttribute.AttributeType; |
| import com.google.protobuf.ExtensionRegistry; |
| import com.google.protobuf.InvalidProtocolBufferException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.UnsupportedEncodingException; |
| import java.nio.ByteBuffer; |
| import java.nio.ByteOrder; |
| import java.nio.file.FileSystem; |
| import java.nio.file.FileSystems; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Enumeration; |
| import java.util.HashSet; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Optional; |
| import java.util.Set; |
| import java.util.concurrent.TimeUnit; |
| import java.util.logging.Logger; |
| import java.util.zip.ZipEntry; |
| import java.util.zip.ZipFile; |
| import java.util.zip.ZipInputStream; |
| import javax.annotation.Nullable; |
| import javax.annotation.concurrent.NotThreadSafe; |
| |
| /** Deserializes {@link DataKey}, {@link DataValue} entries from compiled resource files. */ |
| public class AndroidCompiledDataDeserializer implements AndroidDataDeserializer { |
| private static final Logger logger = |
| Logger.getLogger(AndroidCompiledDataDeserializer.class.getName()); |
| |
| // Magic numbers from |
| // https://android.googlesource.com/platform/frameworks/base.git/+/refs/tags/platform-tools-29.0.2/tools/aapt2/formats.md |
| // |
| // TODO(b/143116130): aapt2 should just use protobuf directly instead of this custom |
| // layer on top. |
| private static final int AAPT_CONTAINER_MAGIC = 0x54504141; // "AAPT" as little-endian |
| private static final int AAPT_CONTAINER_VERSION = 1; |
| private static final int AAPT_CONTAINER_ENTRY_RES_TABLE = 0; |
| private static final int AAPT_CONTAINER_ENTRY_RES_FILE = 1; |
| |
| // aapt2 insists on aligning things to 4-byte boundaries in *.flat files, though there's no actual |
| // performance or maintainability benefit. |
| private static final int AAPT_FLAT_FILE_ALIGNMENT = 4; |
| |
| private static final ImmutableMap<Configuration.LayoutDirection, LayoutDirection> |
| LAYOUT_DIRECTION_MAP = |
| ImmutableMap.of( |
| Configuration.LayoutDirection.LAYOUT_DIRECTION_LTR, |
| LayoutDirection.LTR, |
| Configuration.LayoutDirection.LAYOUT_DIRECTION_RTL, |
| LayoutDirection.RTL); |
| |
| private static final ImmutableMap<Configuration.ScreenLayoutSize, ScreenSize> LAYOUT_SIZE_MAP = |
| ImmutableMap.of( |
| ScreenLayoutSize.SCREEN_LAYOUT_SIZE_SMALL, |
| ScreenSize.SMALL, |
| ScreenLayoutSize.SCREEN_LAYOUT_SIZE_NORMAL, |
| ScreenSize.NORMAL, |
| ScreenLayoutSize.SCREEN_LAYOUT_SIZE_LARGE, |
| ScreenSize.LARGE, |
| ScreenLayoutSize.SCREEN_LAYOUT_SIZE_XLARGE, |
| ScreenSize.XLARGE); |
| |
| private static final ImmutableMap<Configuration.ScreenLayoutLong, ScreenRatio> SCREEN_LONG_MAP = |
| ImmutableMap.of( |
| ScreenLayoutLong.SCREEN_LAYOUT_LONG_LONG, |
| ScreenRatio.LONG, |
| ScreenLayoutLong.SCREEN_LAYOUT_LONG_NOTLONG, |
| ScreenRatio.NOTLONG); |
| |
| private static final ImmutableMap<Configuration.ScreenRound, ScreenRound> SCREEN_ROUND_MAP = |
| ImmutableMap.of( |
| Configuration.ScreenRound.SCREEN_ROUND_ROUND, ScreenRound.ROUND, |
| Configuration.ScreenRound.SCREEN_ROUND_NOTROUND, ScreenRound.NOTROUND); |
| |
| private static final ImmutableMap<Configuration.Orientation, ScreenOrientation> |
| SCREEN_ORIENTATION_MAP = |
| ImmutableMap.of( |
| Orientation.ORIENTATION_LAND, ScreenOrientation.LANDSCAPE, |
| Orientation.ORIENTATION_PORT, ScreenOrientation.PORTRAIT, |
| Orientation.ORIENTATION_SQUARE, ScreenOrientation.SQUARE); |
| |
| private static final ImmutableMap<UiModeType, UiMode> SCREEN_UI_MODE = |
| ImmutableMap.<UiModeType, UiMode>builder() |
| .put(UiModeType.UI_MODE_TYPE_APPLIANCE, UiMode.APPLIANCE) |
| .put(UiModeType.UI_MODE_TYPE_CAR, UiMode.CAR) |
| .put(UiModeType.UI_MODE_TYPE_DESK, UiMode.DESK) |
| .put(UiModeType.UI_MODE_TYPE_NORMAL, UiMode.NORMAL) |
| .put(UiModeType.UI_MODE_TYPE_TELEVISION, UiMode.TELEVISION) |
| .put(UiModeType.UI_MODE_TYPE_VRHEADSET, UiMode.NORMAL) |
| .put(UiModeType.UI_MODE_TYPE_WATCH, UiMode.WATCH) |
| .build(); |
| |
| private static final ImmutableMap<Configuration.UiModeNight, NightMode> NIGHT_MODE_MAP = |
| ImmutableMap.of( |
| UiModeNight.UI_MODE_NIGHT_NIGHT, NightMode.NIGHT, |
| UiModeNight.UI_MODE_NIGHT_NOTNIGHT, NightMode.NOTNIGHT); |
| |
| private static final ImmutableMap<Configuration.KeysHidden, KeyboardState> KEYBOARD_STATE_MAP = |
| ImmutableMap.of( |
| KeysHidden.KEYS_HIDDEN_KEYSEXPOSED, |
| KeyboardState.EXPOSED, |
| KeysHidden.KEYS_HIDDEN_KEYSSOFT, |
| KeyboardState.SOFT, |
| KeysHidden.KEYS_HIDDEN_KEYSHIDDEN, |
| KeyboardState.HIDDEN); |
| |
| private static final ImmutableMap<Configuration.Touchscreen, TouchScreen> TOUCH_TYPE_MAP = |
| ImmutableMap.of( |
| Touchscreen.TOUCHSCREEN_FINGER, |
| TouchScreen.FINGER, |
| Touchscreen.TOUCHSCREEN_NOTOUCH, |
| TouchScreen.NOTOUCH, |
| Touchscreen.TOUCHSCREEN_STYLUS, |
| TouchScreen.STYLUS); |
| |
| private static final ImmutableMap<Configuration.Keyboard, Keyboard> KEYBOARD_MAP = |
| ImmutableMap.of( |
| Configuration.Keyboard.KEYBOARD_NOKEYS, |
| Keyboard.NOKEY, |
| Configuration.Keyboard.KEYBOARD_QWERTY, |
| Keyboard.QWERTY, |
| Configuration.Keyboard.KEYBOARD_TWELVEKEY, |
| Keyboard.TWELVEKEY); |
| |
| private static final ImmutableMap<Configuration.NavHidden, NavigationState> NAV_STATE_MAP = |
| ImmutableMap.of( |
| NavHidden.NAV_HIDDEN_NAVHIDDEN, |
| NavigationState.HIDDEN, |
| NavHidden.NAV_HIDDEN_NAVEXPOSED, |
| NavigationState.EXPOSED); |
| |
| private static final ImmutableMap<Configuration.Navigation, Navigation> NAVIGATION_MAP = |
| ImmutableMap.of( |
| Configuration.Navigation.NAVIGATION_DPAD, |
| Navigation.DPAD, |
| Configuration.Navigation.NAVIGATION_NONAV, |
| Navigation.NONAV, |
| Configuration.Navigation.NAVIGATION_TRACKBALL, |
| Navigation.TRACKBALL, |
| Configuration.Navigation.NAVIGATION_WHEEL, |
| Navigation.WHEEL); |
| |
| private static final ImmutableMap<Integer, Density> DENSITY_MAP = |
| ImmutableMap.<Integer, Density>builder() |
| .put(0xfffe, Density.ANYDPI) |
| .put(0xffff, Density.NODPI) |
| .put(120, Density.LOW) |
| .put(160, Density.MEDIUM) |
| .put(213, Density.TV) |
| .put(240, Density.HIGH) |
| .put(320, Density.XHIGH) |
| .put(480, Density.XXHIGH) |
| .put(640, Density.XXXHIGH) |
| .build(); |
| |
| private final boolean includeFileContentsForValidation; |
| |
| public static AndroidCompiledDataDeserializer create(boolean includeFileContentsForValidation) { |
| return new AndroidCompiledDataDeserializer(includeFileContentsForValidation); |
| } |
| |
| private AndroidCompiledDataDeserializer(boolean includeFileContentsForValidation) { |
| this.includeFileContentsForValidation = includeFileContentsForValidation; |
| } |
| |
| private static void consumeResourceTable( |
| DependencyInfo dependencyInfo, |
| KeyValueConsumers consumers, |
| ResourceTable resourceTable, |
| VisibilityRegistry registry) |
| throws UnsupportedEncodingException, InvalidProtocolBufferException { |
| List<String> sourcePool = |
| decodeSourcePool(resourceTable.getSourcePool().getData().toByteArray()); |
| ReferenceResolver resolver = ReferenceResolver.asRoot(); |
| |
| for (Package resourceTablePackage : resourceTable.getPackageList()) { |
| ReferenceResolver packageResolver = |
| resolver.resolveFor(resourceTablePackage.getPackageName()); |
| String packageName = resourceTablePackage.getPackageName(); |
| |
| for (Resources.Type resourceFormatType : resourceTablePackage.getTypeList()) { |
| ResourceType resourceType = ResourceType.getEnum(resourceFormatType.getName()); |
| |
| for (Resources.Entry resource : resourceFormatType.getEntryList()) { |
| if (!"android".equals(packageName)) { |
| // This means this resource is not in the android sdk, add it to the set. |
| for (ConfigValue configValue : resource.getConfigValueList()) { |
| FullyQualifiedName fqn = |
| createAndRecordFqn( |
| packageResolver, |
| packageName, |
| resourceType, |
| resource, |
| convertToQualifiers(configValue.getConfig())); |
| Visibility visibility = |
| registry.getVisibility( |
| ResourceName.create(packageName, resourceType, resource.getName())); |
| |
| int sourceIndex = configValue.getValue().getSource().getPathIdx(); |
| String source = sourcePool.get(sourceIndex); |
| DataSource dataSource = DataSource.of(dependencyInfo, Paths.get(source)); |
| |
| Value resourceValue = configValue.getValue(); |
| DataResource dataResource = |
| resourceValue.getItem().hasFile() |
| ? DataValueFile.of( |
| visibility, dataSource, /*fingerprint=*/ null, /*rootXmlNode=*/ null) |
| : DataResourceXml.from( |
| resourceValue, visibility, dataSource, resourceType, packageResolver); |
| |
| if (!fqn.isOverwritable()) { |
| consumers.combiningConsumer.accept(fqn, dataResource); |
| } else { |
| consumers.overwritingConsumer.accept(fqn, dataResource); |
| } |
| } |
| } else { |
| // In the sdk, just add the fqn for styleables |
| createAndRecordFqn( |
| packageResolver, packageName, resourceType, resource, ImmutableList.of()); |
| } |
| } |
| } |
| } |
| } |
| |
| /** Maintains state for all references in each package of a resource table. */ |
| // TODO(b/112848607): Remove this! This machinery is all really for pretty-printing styleables, |
| // and only ever used for emitting XML with tools:keep attributes. |
| // https://github.com/bazelbuild/bazel/blob/2419d4b2780fc68a0e501c1fab558b045eb054d3/src/tools/android/java/com/google/devtools/build/android/aapt2/ResourceLinker.java#L523 |
| @NotThreadSafe |
| public static class ReferenceResolver { |
| |
| enum InlineStatus { |
| INLINEABLE, |
| INLINED, |
| } |
| |
| private final Optional<String> packageName; |
| private final Map<FullyQualifiedName, InlineStatus> qualifiedReferenceInlineStatus; |
| |
| private ReferenceResolver( |
| Optional<String> packageName, |
| Map<FullyQualifiedName, InlineStatus> qualifiedReferenceInlineStatus) { |
| this.packageName = packageName; |
| this.qualifiedReferenceInlineStatus = qualifiedReferenceInlineStatus; |
| } |
| |
| static ReferenceResolver asRoot() { |
| return new ReferenceResolver(Optional.empty(), new LinkedHashMap<>()); |
| } |
| |
| public ReferenceResolver resolveFor(String packageName) { |
| return new ReferenceResolver( |
| Optional.of(packageName).filter(not(String::isEmpty)), qualifiedReferenceInlineStatus); |
| } |
| |
| public FullyQualifiedName parse(String reference) { |
| return FullyQualifiedName.fromReference(reference, packageName); |
| } |
| |
| public FullyQualifiedName register(FullyQualifiedName fullyQualifiedName) { |
| // The default is that the name can be inlined. |
| qualifiedReferenceInlineStatus.put(fullyQualifiedName, InlineStatus.INLINEABLE); |
| return fullyQualifiedName; |
| } |
| |
| /** Indicates if a reference can be inlined in a styleable. */ |
| public boolean shouldInline(FullyQualifiedName reference) { |
| // Only inline if it's in the current package. |
| if (!reference.isInPackage(packageName.orElse(FullyQualifiedName.DEFAULT_PACKAGE))) { |
| return false; |
| } |
| |
| return InlineStatus.INLINEABLE.equals(qualifiedReferenceInlineStatus.get(reference)); |
| } |
| |
| /** Update the reference's inline state. */ |
| public FullyQualifiedName markInlined(FullyQualifiedName reference) { |
| qualifiedReferenceInlineStatus.put(reference, InlineStatus.INLINED); |
| return reference; |
| } |
| } |
| |
| private static FullyQualifiedName createAndRecordFqn( |
| ReferenceResolver packageResolver, |
| String packageName, |
| ResourceType resourceType, |
| Resources.Entry resource, |
| List<String> qualifiers) { |
| final FullyQualifiedName fqn = |
| FullyQualifiedName.of( |
| packageName.isEmpty() ? FullyQualifiedName.DEFAULT_PACKAGE : packageName, |
| qualifiers, |
| resourceType, |
| resource.getName()); |
| packageResolver.register(fqn); |
| return fqn; |
| } |
| |
| // TODO(b/146498565): remove this and use 'Configuration' directly, which is typesafe and free. |
| private static List<String> convertToQualifiers(Configuration protoConfig) { |
| FolderConfiguration configuration = new FolderConfiguration(); |
| if (protoConfig.getMcc() > 0) { |
| configuration.setCountryCodeQualifier(new CountryCodeQualifier(protoConfig.getMcc())); |
| } |
| // special code for 0, as MNC can be zero |
| // https://android.googlesource.com/platform/frameworks/native/+/master/include/android/configuration.h#473 |
| if (protoConfig.getMnc() != 0) { |
| configuration.setNetworkCodeQualifier( |
| NetworkCodeQualifier.getQualifier( |
| String.format( |
| Locale.US, |
| "mnc%1$03d", |
| protoConfig.getMnc() == 0xffff ? 0 : protoConfig.getMnc()))); |
| } |
| |
| if (!protoConfig.getLocale().isEmpty()) { |
| // The proto stores it in a BCP-47 format, but the parser requires a b+ and all the - as +. |
| // It's a nice a little impedance mismatch. |
| new LocaleQualifier() |
| .checkAndSet("b+" + protoConfig.getLocale().replace("-", "+"), configuration); |
| } |
| |
| if (LAYOUT_DIRECTION_MAP.containsKey(protoConfig.getLayoutDirection())) { |
| configuration.setLayoutDirectionQualifier( |
| new LayoutDirectionQualifier(LAYOUT_DIRECTION_MAP.get(protoConfig.getLayoutDirection()))); |
| } |
| |
| if (protoConfig.getSmallestScreenWidthDp() > 0) { |
| configuration.setSmallestScreenWidthQualifier( |
| new SmallestScreenWidthQualifier(protoConfig.getSmallestScreenWidthDp())); |
| } |
| |
| // screen dimension is defined if one number is greater than 0 |
| if (Math.max(protoConfig.getScreenHeight(), protoConfig.getScreenWidth()) > 0) { |
| configuration.setScreenDimensionQualifier( |
| new ScreenDimensionQualifier( |
| Math.max( |
| protoConfig.getScreenHeight(), |
| protoConfig.getScreenWidth()), // biggest is always first |
| Math.min(protoConfig.getScreenHeight(), protoConfig.getScreenWidth()))); |
| } |
| |
| if (protoConfig.getScreenWidthDp() > 0) { |
| configuration.setScreenWidthQualifier( |
| new ScreenWidthQualifier(protoConfig.getScreenWidthDp())); |
| } |
| |
| if (protoConfig.getScreenHeightDp() > 0) { |
| configuration.setScreenHeightQualifier( |
| new ScreenHeightQualifier(protoConfig.getScreenHeightDp())); |
| } |
| |
| if (LAYOUT_SIZE_MAP.containsKey(protoConfig.getScreenLayoutSize())) { |
| configuration.setScreenSizeQualifier( |
| new ScreenSizeQualifier(LAYOUT_SIZE_MAP.get(protoConfig.getScreenLayoutSize()))); |
| } |
| |
| if (SCREEN_LONG_MAP.containsKey(protoConfig.getScreenLayoutLong())) { |
| configuration.setScreenRatioQualifier( |
| new ScreenRatioQualifier(SCREEN_LONG_MAP.get(protoConfig.getScreenLayoutLong()))); |
| } |
| |
| if (SCREEN_ROUND_MAP.containsKey(protoConfig.getScreenRound())) { |
| configuration.setScreenRoundQualifier( |
| new ScreenRoundQualifier(SCREEN_ROUND_MAP.get(protoConfig.getScreenRound()))); |
| } |
| |
| if (SCREEN_ORIENTATION_MAP.containsKey(protoConfig.getOrientation())) { |
| configuration.setScreenOrientationQualifier( |
| new ScreenOrientationQualifier(SCREEN_ORIENTATION_MAP.get(protoConfig.getOrientation()))); |
| } |
| |
| if (SCREEN_UI_MODE.containsKey(protoConfig.getUiModeType())) { |
| configuration.setUiModeQualifier( |
| new UiModeQualifier(SCREEN_UI_MODE.get(protoConfig.getUiModeType()))); |
| } |
| |
| if (NIGHT_MODE_MAP.containsKey(protoConfig.getUiModeNight())) { |
| configuration.setNightModeQualifier( |
| new NightModeQualifier(NIGHT_MODE_MAP.get(protoConfig.getUiModeNight()))); |
| } |
| |
| if (DENSITY_MAP.containsKey(protoConfig.getDensity())) { |
| configuration.setDensityQualifier( |
| new DensityQualifier(DENSITY_MAP.get(protoConfig.getDensity()))); |
| } |
| |
| if (TOUCH_TYPE_MAP.containsKey(protoConfig.getTouchscreen())) { |
| configuration.setTouchTypeQualifier( |
| new TouchScreenQualifier(TOUCH_TYPE_MAP.get(protoConfig.getTouchscreen()))); |
| } |
| |
| if (KEYBOARD_STATE_MAP.containsKey(protoConfig.getKeysHidden())) { |
| configuration.setKeyboardStateQualifier( |
| new KeyboardStateQualifier(KEYBOARD_STATE_MAP.get(protoConfig.getKeysHidden()))); |
| } |
| |
| if (KEYBOARD_MAP.containsKey(protoConfig.getKeyboard())) { |
| configuration.setTextInputMethodQualifier( |
| new TextInputMethodQualifier(KEYBOARD_MAP.get(protoConfig.getKeyboard()))); |
| } |
| |
| if (NAV_STATE_MAP.containsKey(protoConfig.getNavHidden())) { |
| configuration.setNavigationStateQualifier( |
| new NavigationStateQualifier(NAV_STATE_MAP.get(protoConfig.getNavHidden()))); |
| } |
| |
| if (NAVIGATION_MAP.containsKey(protoConfig.getNavigation())) { |
| configuration.setNavigationMethodQualifier( |
| new NavigationMethodQualifier(NAVIGATION_MAP.get(protoConfig.getNavigation()))); |
| } |
| |
| if (protoConfig.getSdkVersion() > 0) { |
| configuration.setVersionQualifier(new VersionQualifier(protoConfig.getSdkVersion())); |
| } |
| |
| return Arrays.stream(configuration.getQualifiers()) |
| .map(ResourceQualifier::getFolderSegment) |
| .collect(toList()); |
| } |
| |
| /** |
| * Forwards compiled resource data files to consumers, extracting any embedded IDs along the way. |
| */ |
| private static void consumeCompiledFile( |
| DependencyInfo dependencyInfo, |
| KeyValueConsumers consumers, |
| CompiledFileWithData compiledFileWithData, |
| VisibilityRegistry registry) |
| throws InvalidProtocolBufferException { |
| CompiledFile compiledFile = compiledFileWithData.compiledFile(); |
| Path sourcePath = Paths.get(compiledFile.getSourcePath()); |
| DataSource dataSource = DataSource.of(dependencyInfo, sourcePath); |
| ResourceName resourceName = ResourceName.parse(compiledFile.getResourceName()); |
| FullyQualifiedName fqn = |
| FullyQualifiedName.of( |
| resourceName.pkg(), |
| convertToQualifiers(compiledFile.getConfig()), |
| resourceName.type(), |
| resourceName.entry()); |
| |
| consumers.overwritingConsumer.accept( |
| fqn, |
| DataValueFile.of( |
| registry.getVisibility(resourceName), |
| dataSource, |
| compiledFileWithData.fingerprint(), |
| compiledFileWithData.rootXmlNode())); |
| |
| for (CompiledFile.Symbol exportedSymbol : compiledFile.getExportedSymbolList()) { |
| if (exportedSymbol.getResourceName().startsWith("android:")) { |
| // Skip writing resource xml's for resources in the sdk |
| continue; |
| } |
| |
| ResourceName exportedResourceName = ResourceName.parse(exportedSymbol.getResourceName()); |
| verify(exportedResourceName.type() == ResourceType.ID); |
| FullyQualifiedName symbolFqn = |
| FullyQualifiedName.of( |
| exportedResourceName.pkg(), |
| /*qualifiers=*/ ImmutableList.of(), |
| exportedResourceName.type(), |
| exportedResourceName.entry()); |
| |
| DataResourceXml dataResourceXml = |
| DataResourceXml.from( |
| Value.getDefaultInstance(), |
| registry.getVisibility(exportedResourceName), |
| dataSource, |
| ResourceType.ID, |
| /*packageResolver=*/ null); |
| consumers.combiningConsumer.accept(symbolFqn, dataResourceXml); |
| } |
| } |
| |
| private static void readAttributesFile( |
| DependencyInfo dependencyInfo, |
| InputStream resourceFileStream, |
| FileSystem fileSystem, |
| ParsedAndroidData.KeyValueConsumer<DataKey, DataResource> combine, |
| ParsedAndroidData.KeyValueConsumer<DataKey, DataResource> overwrite) |
| throws IOException { |
| |
| Header header = Header.parseDelimitedFrom(resourceFileStream); |
| List<FullyQualifiedName> fullyQualifiedNames = new ArrayList<>(); |
| for (int i = 0; i < header.getEntryCount(); i++) { |
| SerializeFormat.DataKey protoKey = |
| SerializeFormat.DataKey.parseDelimitedFrom(resourceFileStream); |
| fullyQualifiedNames.add(FullyQualifiedName.fromProto(protoKey)); |
| } |
| |
| DataSourceTable sourceTable = |
| DataSourceTable.read(dependencyInfo, resourceFileStream, fileSystem, header); |
| |
| for (FullyQualifiedName fullyQualifiedName : fullyQualifiedNames) { |
| SerializeFormat.DataValue protoValue = |
| SerializeFormat.DataValue.parseDelimitedFrom(resourceFileStream); |
| DataSource source = sourceTable.sourceFromId(protoValue.getSourceId()); |
| DataResourceXml dataResourceXml = (DataResourceXml) DataResourceXml.from(protoValue, source); |
| AttributeType attributeType = AttributeType.valueOf(protoValue.getXmlValue().getValueType()); |
| |
| if (attributeType.isCombining()) { |
| combine.accept(fullyQualifiedName, dataResourceXml); |
| } else { |
| overwrite.accept(fullyQualifiedName, dataResourceXml); |
| } |
| } |
| } |
| |
| public static Map<DataKey, DataResource> readAttributes(CompiledResources resources) { |
| try (ZipInputStream zipStream = new ZipInputStream(Files.newInputStream(resources.getZip()))) { |
| Map<DataKey, DataResource> attributes = new LinkedHashMap<>(); |
| for (ZipEntry entry = zipStream.getNextEntry(); |
| entry != null; |
| entry = zipStream.getNextEntry()) { |
| if (entry.getName().endsWith(CompiledResources.ATTRIBUTES_FILE_EXTENSION)) { |
| readAttributesFile( |
| // Don't care about origin of ".attributes" values, since they don't feed into field |
| // initializers. |
| DependencyInfo.UNKNOWN, |
| zipStream, |
| FileSystems.getDefault(), |
| (key, value) -> |
| attributes.put( |
| key, |
| attributes.containsKey(key) ? attributes.get(key).combineWith(value) : value), |
| (key, value) -> |
| attributes.put( |
| key, |
| attributes.containsKey(key) ? attributes.get(key).overwrite(value) : value)); |
| } |
| } |
| return attributes; |
| } catch (IOException e) { |
| throw new DeserializationException(e); |
| } |
| } |
| |
| @VisibleForTesting |
| public static void readTable( |
| DependencyInfo dependencyInfo, InputStream in, KeyValueConsumers consumers) |
| throws IOException { |
| final ResourceTable resourceTable = |
| ResourceTable.parseFrom(in, ExtensionRegistry.getEmptyRegistry()); |
| consumeResourceTable( |
| dependencyInfo, |
| consumers, |
| resourceTable, |
| new VisibilityRegistry(ImmutableSet.of(), ImmutableSet.of())); |
| } |
| |
| public Map<DataKey, DataResource> read(DependencyInfo dependencyInfo, Path inPath) { |
| Map<DataKey, DataResource> resources = new LinkedHashMap<>(); |
| read( |
| dependencyInfo, |
| inPath, |
| KeyValueConsumers.of( |
| resources::put, |
| resources::put, |
| (key, value) -> { |
| throw new IllegalStateException(String.format("Unexpected asset in %s", inPath)); |
| })); |
| return resources; |
| } |
| |
| @Override |
| public void read(DependencyInfo dependencyInfo, Path inPath, KeyValueConsumers consumers) { |
| Stopwatch timer = Stopwatch.createStarted(); |
| |
| try (ZipFile zipFile = new ZipFile(inPath.toFile())) { |
| ResourceContainer resourceContainer = |
| readResourceContainer(zipFile, includeFileContentsForValidation); |
| VisibilityRegistry registry = computeResourceVisibility(resourceContainer); |
| |
| for (ResourceTable resourceTable : resourceContainer.resourceTables()) { |
| consumeResourceTable(dependencyInfo, consumers, resourceTable, registry); |
| } |
| for (CompiledFileWithData compiledFile : resourceContainer.compiledFiles()) { |
| consumeCompiledFile(dependencyInfo, consumers, compiledFile, registry); |
| } |
| |
| Enumeration<? extends ZipEntry> resourceFiles = zipFile.entries(); |
| while (resourceFiles.hasMoreElements()) { |
| ZipEntry resourceFile = resourceFiles.nextElement(); |
| String fileZipPath = resourceFile.getName(); |
| |
| try (InputStream resourceFileStream = zipFile.getInputStream(resourceFile)) { |
| if (fileZipPath.endsWith(CompiledResources.ATTRIBUTES_FILE_EXTENSION)) { |
| readAttributesFile( |
| dependencyInfo, |
| resourceFileStream, |
| inPath.getFileSystem(), |
| consumers.combiningConsumer, |
| consumers.overwritingConsumer); |
| } |
| } |
| } |
| } catch (IOException e) { |
| throw new DeserializationException("Error deserializing " + inPath, e); |
| } finally { |
| logger.fine( |
| String.format( |
| "Deserialized in compiled merged in %sms", timer.elapsed(TimeUnit.MILLISECONDS))); |
| } |
| } |
| |
| private static ResourceContainer readResourceContainer( |
| ZipFile zipFile, boolean includeFileContentsForValidation) throws IOException { |
| List<ResourceTable> resourceTables = new ArrayList<>(); |
| List<CompiledFileWithData> compiledFiles = new ArrayList<>(); |
| |
| Enumeration<? extends ZipEntry> resourceFiles = zipFile.entries(); |
| while (resourceFiles.hasMoreElements()) { |
| ZipEntry resourceFile = resourceFiles.nextElement(); |
| String fileZipPath = resourceFile.getName(); |
| if (fileZipPath.endsWith(CompiledResources.ATTRIBUTES_FILE_EXTENSION)) { |
| continue; |
| } |
| |
| try (LittleEndianDataInputStream dataInputStream = |
| new LittleEndianDataInputStream(zipFile.getInputStream(resourceFile))) { |
| int magic = dataInputStream.readInt(); |
| verify( |
| magic == AAPT_CONTAINER_MAGIC, |
| "Unexpected magic number in %s: %s", |
| resourceFile, |
| magic); |
| int version = dataInputStream.readInt(); |
| verify( |
| version == AAPT_CONTAINER_VERSION, |
| "Unexpected version number in %s: %s", |
| resourceFile, |
| version); |
| |
| int numberOfEntries = dataInputStream.readInt(); |
| for (int i = 0; i < numberOfEntries; i++) { |
| int entryType = dataInputStream.readInt(); |
| verify( |
| entryType == AAPT_CONTAINER_ENTRY_RES_TABLE |
| || entryType == AAPT_CONTAINER_ENTRY_RES_FILE, |
| "Unexpected entry type in %s: %s", |
| resourceFile, |
| entryType); |
| |
| if (entryType == AAPT_CONTAINER_ENTRY_RES_TABLE) { |
| long size = dataInputStream.readLong(); |
| verify(size <= Integer.MAX_VALUE); |
| byte[] tableBytes = readBytesAndSkipPadding(dataInputStream, (int) size); |
| |
| resourceTables.add( |
| ResourceTable.parseFrom(tableBytes, ExtensionRegistry.getEmptyRegistry())); |
| } else { |
| // useless and wrong "size" data; see |
| // https://android-review.googlesource.com/c/platform/frameworks/base/+/1161789 |
| ByteStreams.skipFully(dataInputStream, 8); |
| int headerSize = dataInputStream.readInt(); |
| long payloadSize = dataInputStream.readLong(); |
| byte[] headerBytes = readBytesAndSkipPadding(dataInputStream, headerSize); |
| |
| CompiledFile compiledFile = |
| CompiledFile.parseFrom(headerBytes, ExtensionRegistry.getEmptyRegistry()); |
| |
| HashCode fingerprint; |
| XmlNode rootXmlNode; |
| if (includeFileContentsForValidation) { |
| byte[] payloadBytes = readBytesAndSkipPadding(dataInputStream, (int) payloadSize); |
| fingerprint = Hashing.goodFastHash(/*minimumBits=*/ 64).hashBytes(payloadBytes); |
| rootXmlNode = |
| compiledFile.getType() == Resources.FileReference.Type.PROTO_XML |
| ? XmlNode.parseFrom(payloadBytes, ExtensionRegistry.getEmptyRegistry()) |
| : null; |
| } else { |
| ByteStreams.skipFully( |
| dataInputStream, roundUpToNearest(payloadSize, AAPT_FLAT_FILE_ALIGNMENT)); |
| fingerprint = null; |
| rootXmlNode = null; |
| } |
| |
| compiledFiles.add(CompiledFileWithData.create(compiledFile, fingerprint, rootXmlNode)); |
| } |
| } |
| } |
| } |
| return ResourceContainer.create(resourceTables, compiledFiles); |
| } |
| |
| /** Rounds {@code n} up to the nearest multiple of {@code k}. */ |
| private static long roundUpToNearest(long n, int k) { |
| return ((n + k - 1) / k) * k; |
| } |
| |
| /** |
| * Classifies resources as public/private/unknown. |
| * |
| * <p>Per https://developer.android.com/studio/projects/android-library#PrivateResources, the |
| * {@code <public/>} tag marks a resource as public with all others inferred to be private. Since |
| * {@code android_library} allows mixing directories (with varying ownership) in {@code |
| * resource_files} (b/148110689), we perform the classification on a per-directory basis, so that |
| * marking something in {@code foo/res} public has no impact on {@code bar/res}. |
| */ |
| private static VisibilityRegistry computeResourceVisibility(ResourceContainer resourceContainer) |
| throws UnsupportedEncodingException { |
| if (!ResourceCompiler.USE_VISIBILITY_FROM_AAPT2) { |
| return new VisibilityRegistry(ImmutableSet.of(), ImmutableSet.of()); |
| } |
| |
| // decode source pools ahead of time to avoid having to repeatedly do so later. |
| List<List<String>> sourcePools = new ArrayList<>(); |
| for (ResourceTable resourceTable : resourceContainer.resourceTables()) { |
| sourcePools.add(decodeSourcePool(resourceTable.getSourcePool().getData().toByteArray())); |
| } |
| |
| PublicResources publicResources = findExplicitlyPublicResources(resourceContainer, sourcePools); |
| return new VisibilityRegistry( |
| publicResources.explicitlyPublicResources(), |
| findImpliedPrivateResources(resourceContainer, sourcePools, publicResources)); |
| } |
| |
| private static PublicResources findExplicitlyPublicResources( |
| ResourceContainer resourceContainer, List<List<String>> sourcePools) { |
| Set<ResourceName> explicitlyPublicResources = new HashSet<>(); |
| Set<Path> directoriesWithPublicResources = new HashSet<>(); |
| for (int i = 0; i < resourceContainer.resourceTables().size(); i++) { |
| ResourceTable resourceTable = resourceContainer.resourceTables().get(i); |
| List<String> sourcePool = sourcePools.get(i); |
| |
| for (Package pkg : resourceTable.getPackageList()) { |
| for (Resources.Type type : pkg.getTypeList()) { |
| ResourceType resourceType = ResourceType.getEnum(type.getName()); |
| for (Resources.Entry entry : type.getEntryList()) { |
| if (entry.getVisibility().getLevel() != Resources.Visibility.Level.PUBLIC) { |
| continue; |
| } |
| |
| explicitlyPublicResources.add( |
| ResourceName.create(pkg.getPackageName(), resourceType, entry.getName())); |
| directoriesWithPublicResources.add( |
| getNormalizedResourceDirectory( |
| sourcePool.get(entry.getVisibility().getSource().getPathIdx()))); |
| } |
| } |
| } |
| } |
| return PublicResources.create(explicitlyPublicResources, directoriesWithPublicResources); |
| } |
| |
| private static ImmutableSet<ResourceName> findImpliedPrivateResources( |
| ResourceContainer resourceContainer, |
| List<List<String>> sourcePools, |
| PublicResources publicResources) { |
| Set<ResourceName> explicitlyPublicResources = publicResources.explicitlyPublicResources(); |
| Set<Path> directoriesWithPublicResources = publicResources.directoriesWithPublicResources(); |
| Set<ResourceName> impliedPrivateResources = new HashSet<>(); |
| for (int i = 0; i < resourceContainer.resourceTables().size(); i++) { |
| ResourceTable resourceTable = resourceContainer.resourceTables().get(i); |
| List<String> sourcePool = sourcePools.get(i); |
| |
| for (Package pkg : resourceTable.getPackageList()) { |
| for (Resources.Type type : pkg.getTypeList()) { |
| ResourceType resourceType = ResourceType.getEnum(type.getName()); |
| for (Resources.Entry entry : type.getEntryList()) { |
| if (entry.getVisibility().getLevel() == Resources.Visibility.Level.PUBLIC) { |
| continue; |
| } |
| |
| ResourceName resourceName = |
| ResourceName.create(pkg.getPackageName(), resourceType, entry.getName()); |
| if (explicitlyPublicResources.contains(resourceName) |
| || impliedPrivateResources.contains(resourceName)) { |
| continue; // we already figured out a classification for this resource. |
| } |
| |
| boolean inDirectoryWithPublic = |
| entry.getConfigValueList().stream() |
| .map( |
| configValue -> |
| getNormalizedResourceDirectory( |
| sourcePool.get(configValue.getValue().getSource().getPathIdx()))) |
| .anyMatch(directoriesWithPublicResources::contains); |
| if (inDirectoryWithPublic) { |
| impliedPrivateResources.add(resourceName); |
| } |
| } |
| } |
| } |
| } |
| for (CompiledFileWithData compiledFileWithData : resourceContainer.compiledFiles()) { |
| CompiledFile compiledFile = compiledFileWithData.compiledFile(); |
| ResourceName resourceName = ResourceName.parse(compiledFile.getResourceName()); |
| if (explicitlyPublicResources.contains(resourceName) |
| || impliedPrivateResources.contains(resourceName)) { |
| continue; // we already figured out a classification for this resource. |
| } |
| if (directoriesWithPublicResources.contains( |
| getNormalizedResourceDirectory(compiledFile.getSourcePath()))) { |
| impliedPrivateResources.add(resourceName); |
| } |
| } |
| return ImmutableSet.copyOf(impliedPrivateResources); |
| } |
| |
| /** |
| * Returns the resource directory (e.g. {@code java/com/pkg/res/}) which contains a file, |
| * stripping off any {@code blaze-*} prefix for normalization. |
| */ |
| private static Path getNormalizedResourceDirectory(String filename) { |
| Path resDir = Paths.get(filename).getParent().getParent(); |
| if (resDir.getName(0).toString().startsWith("blaze-")) { |
| // strip off stuff like blaze-out/k8-fastbuild/bin/. |
| return resDir.subpath(3, resDir.getNameCount()); |
| } else { |
| return resDir; |
| } |
| } |
| |
| private static byte[] readBytesAndSkipPadding(LittleEndianDataInputStream input, int size) |
| throws IOException { |
| byte[] result = new byte[size]; |
| input.readFully(result); |
| |
| long overage = size % AAPT_FLAT_FILE_ALIGNMENT; |
| if (overage != 0) { |
| ByteStreams.skipFully(input, AAPT_FLAT_FILE_ALIGNMENT - overage); |
| } |
| return result; |
| } |
| |
| private static List<String> decodeSourcePool(byte[] bytes) throws UnsupportedEncodingException { |
| ByteBuffer byteBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); |
| |
| int stringCount = byteBuffer.getInt(8); |
| boolean isUtf8 = (byteBuffer.getInt(16) & (1 << 8)) != 0; |
| int stringsStart = byteBuffer.getInt(20); |
| // Position the ByteBuffer after the metadata |
| byteBuffer.position(28); |
| |
| List<String> strings = new ArrayList<>(); |
| |
| for (int i = 0; i < stringCount; i++) { |
| int stringOffset = stringsStart + byteBuffer.getInt(); |
| |
| if (isUtf8) { |
| int characterCount = byteBuffer.get(stringOffset) & 0xFF; |
| if ((characterCount & 0x80) != 0) { |
| characterCount = |
| ((characterCount & 0x7F) << 8) | (byteBuffer.get(stringOffset + 1) & 0xFF); |
| } |
| |
| stringOffset += (characterCount >= 0x80 ? 2 : 1); |
| |
| int length = byteBuffer.get(stringOffset) & 0xFF; |
| if ((length & 0x80) != 0) { |
| length = ((length & 0x7F) << 8) | (byteBuffer.get(stringOffset + 1) & 0xFF); |
| } |
| |
| stringOffset += (length >= 0x80 ? 2 : 1); |
| |
| strings.add(new String(bytes, stringOffset, length, "UTF8")); |
| } else { |
| // TODO(b/148817379): this next block of lines is forming an int with holes in it. |
| int characterCount = byteBuffer.get(stringOffset) & 0xFFFF; |
| if ((characterCount & 0x8000) != 0) { |
| characterCount = |
| ((characterCount & 0x7FFF) << 16) | (byteBuffer.get(stringOffset + 2) & 0xFFFF); |
| } |
| |
| stringOffset += 2 * (characterCount >= 0x8000 ? 2 : 1); |
| |
| int length = byteBuffer.get(stringOffset) & 0xFFFF; |
| if ((length & 0x8000) != 0) { |
| length = ((length & 0x7FFF) << 16) | (byteBuffer.get(stringOffset + 2) & 0xFFFF); |
| } |
| |
| stringOffset += 2 * (length >= 0x8000 ? 2 : 1); |
| |
| strings.add(new String(bytes, stringOffset, length, "UTF16")); |
| } |
| } |
| |
| return strings; |
| } |
| |
| @AutoValue |
| abstract static class ResourceContainer { |
| abstract ImmutableList<ResourceTable> resourceTables(); |
| |
| abstract ImmutableList<CompiledFileWithData> compiledFiles(); |
| |
| static ResourceContainer create( |
| List<ResourceTable> resourceTables, List<CompiledFileWithData> compiledFiles) { |
| return new AutoValue_AndroidCompiledDataDeserializer_ResourceContainer( |
| ImmutableList.copyOf(resourceTables), ImmutableList.copyOf(compiledFiles)); |
| } |
| } |
| |
| @AutoValue |
| abstract static class CompiledFileWithData { |
| abstract CompiledFile compiledFile(); |
| |
| @Nullable |
| abstract HashCode fingerprint(); |
| |
| @Nullable |
| abstract XmlNode rootXmlNode(); |
| |
| static CompiledFileWithData create( |
| CompiledFile compiledFile, @Nullable HashCode fingerprint, @Nullable XmlNode xmlNode) { |
| return new AutoValue_AndroidCompiledDataDeserializer_CompiledFileWithData( |
| compiledFile, fingerprint, xmlNode); |
| } |
| } |
| |
| /** |
| * Wraps metadata regarding {@code <public/>} tags and their locations to help work around targets |
| * which mix res/ directories they own with ones they don't (b/148110689). |
| */ |
| @AutoValue |
| abstract static class PublicResources { |
| abstract ImmutableSet<ResourceName> explicitlyPublicResources(); |
| |
| abstract ImmutableSet<Path> directoriesWithPublicResources(); |
| |
| static PublicResources create( |
| Set<ResourceName> explicitlyPublicResources, Set<Path> directoriesWithPublicResources) { |
| return new AutoValue_AndroidCompiledDataDeserializer_PublicResources( |
| ImmutableSet.copyOf(explicitlyPublicResources), |
| ImmutableSet.copyOf(directoriesWithPublicResources)); |
| } |
| } |
| |
| private static class VisibilityRegistry { |
| private final Set<ResourceName> explicitlyPublicResources; |
| private final Set<ResourceName> impliedPrivateResources; |
| |
| VisibilityRegistry( |
| Set<ResourceName> explicitlyPublicResources, Set<ResourceName> impliedPrivateResources) { |
| this.explicitlyPublicResources = ImmutableSet.copyOf(explicitlyPublicResources); |
| this.impliedPrivateResources = ImmutableSet.copyOf(impliedPrivateResources); |
| } |
| |
| Visibility getVisibility(ResourceName resourceName) { |
| if (explicitlyPublicResources.contains(resourceName)) { |
| return Visibility.PUBLIC; |
| } |
| // TODO(b/146647897): make resources private by default |
| return impliedPrivateResources.contains(resourceName) |
| ? Visibility.PRIVATE |
| : Visibility.UNKNOWN; |
| } |
| } |
| } |