| // 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 java.util.stream.Collectors.toList; |
| |
| import android.aapt.pb.internal.ResourcesInternal.CompiledFile; |
| import com.android.SdkConstants; |
| 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.Visibility.Level; |
| 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.common.base.Preconditions; |
| 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.io.LittleEndianDataInputStream; |
| import com.google.devtools.build.android.FullyQualifiedName.Factory; |
| import com.google.devtools.build.android.aapt2.CompiledResources; |
| import com.google.devtools.build.android.proto.SerializeFormat; |
| import com.google.devtools.build.android.proto.SerializeFormat.Header; |
| import com.google.devtools.build.android.xml.ResourcesAttribute.AttributeType; |
| 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.Collection; |
| import java.util.Enumeration; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Optional; |
| import java.util.concurrent.TimeUnit; |
| import java.util.function.BiConsumer; |
| import java.util.logging.Logger; |
| import java.util.zip.ZipEntry; |
| import java.util.zip.ZipFile; |
| import java.util.zip.ZipInputStream; |
| 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()); |
| |
| 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); |
| |
| 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); |
| |
| 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); |
| |
| 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(); |
| |
| 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); |
| |
| 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); |
| |
| 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); |
| |
| 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); |
| |
| static final ImmutableMap<Configuration.NavHidden, NavigationState> NAV_STATE_MAP = |
| ImmutableMap.of( |
| NavHidden.NAV_HIDDEN_NAVHIDDEN, |
| NavigationState.HIDDEN, |
| NavHidden.NAV_HIDDEN_NAVEXPOSED, |
| NavigationState.EXPOSED); |
| |
| 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); |
| |
| 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 ImmutableSet<String> filteredResources; |
| |
| /** |
| * @param filteredResources resources that were filtered out of this target and should be ignored |
| * if they are referenced in symbols files. |
| */ |
| public static AndroidCompiledDataDeserializer withFilteredResources( |
| Collection<String> filteredResources) { |
| return new AndroidCompiledDataDeserializer(ImmutableSet.copyOf(filteredResources)); |
| } |
| |
| public static AndroidCompiledDataDeserializer create() { |
| return new AndroidCompiledDataDeserializer(ImmutableSet.of()); |
| } |
| |
| private AndroidCompiledDataDeserializer(ImmutableSet<String> filteredResources) { |
| this.filteredResources = filteredResources; |
| } |
| |
| private void readResourceTable( |
| LittleEndianDataInputStream resourceTableStream, KeyValueConsumers consumers) |
| throws IOException { |
| long alignedSize = resourceTableStream.readLong(); |
| Preconditions.checkArgument(alignedSize <= Integer.MAX_VALUE); |
| |
| byte[] tableBytes = new byte[(int) alignedSize]; |
| resourceTableStream.readFully(tableBytes, 0, (int) alignedSize); |
| ResourceTable resourceTable = ResourceTable.parseFrom(tableBytes); |
| |
| readPackages(consumers, resourceTable); |
| } |
| |
| private void readPackages(KeyValueConsumers consumers, ResourceTable resourceTable) |
| throws UnsupportedEncodingException, InvalidProtocolBufferException { |
| List<String> sourcePool = |
| decodeSourcePool(resourceTable.getSourcePool().getData().toByteArray()); |
| ReferenceResolver resolver = ReferenceResolver.asRoot(); |
| |
| for (int i = resourceTable.getPackageCount() - 1; i >= 0; i--) { |
| Package resourceTablePackage = resourceTable.getPackage(i); |
| |
| 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 (resource.getConfigValueList().isEmpty() |
| && resource.getVisibility().getLevel() == Level.PUBLIC) { |
| |
| // This is a public resource definition. |
| int sourceIndex = resource.getVisibility().getSource().getPathIdx(); |
| |
| String source = sourcePool.get(sourceIndex); |
| DataSource dataSource = DataSource.of(Paths.get(source)); |
| |
| DataResourceXml dataResourceXml = |
| DataResourceXml.fromPublic(dataSource, resourceType, resource.getEntryId().getId()); |
| final FullyQualifiedName fqn = |
| createAndRecordFqn( |
| packageResolver, packageName, resourceType, resource, ImmutableList.of()); |
| consumers.combiningConsumer.accept(fqn, dataResourceXml); |
| } else 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)); |
| |
| int sourceIndex = configValue.getValue().getSource().getPathIdx(); |
| |
| String source = sourcePool.get(sourceIndex); |
| DataSource dataSource = DataSource.of(Paths.get(source)); |
| |
| Value resourceValue = configValue.getValue(); |
| |
| DataResource dataResource = |
| resourceValue.getItem().hasFile() |
| ? DataValueFile.of(dataSource) |
| : DataResourceXml.from( |
| resourceValue, 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()) |
| .toPrettyString(); |
| } |
| } |
| } |
| } |
| } |
| |
| /** Maintains state for all references in each package of a resource table. */ |
| @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 HashMap<>()); |
| } |
| |
| 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 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; |
| } |
| |
| private List<String> convertToQualifiers(ConfigValue configValue) { |
| FolderConfiguration configuration = new FolderConfiguration(); |
| final Configuration protoConfig = configValue.getConfig(); |
| 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()); |
| } |
| |
| /** |
| * Reads compiled resource data files and adds them to consumers |
| * |
| * @param compiledFileStream First byte is number of compiled files represented in this file. Next |
| * 8 bytes is a long indicating the length of the metadata describing the compiled file. Next |
| * N bytes is the metadata describing the compiled file. The remaining bytes are the actual |
| * original file. |
| * @param consumers |
| * @param fqnFactory |
| * @throws IOException |
| */ |
| private void readCompiledFile( |
| LittleEndianDataInputStream compiledFileStream, |
| KeyValueConsumers consumers, |
| Factory fqnFactory) |
| throws IOException { |
| // Skip aligned size. We don't need it here. |
| Preconditions.checkArgument(compiledFileStream.skipBytes(8) == 8); |
| |
| int resFileHeaderSize = compiledFileStream.readInt(); |
| |
| // Skip data payload size. We don't need it here. |
| Preconditions.checkArgument(compiledFileStream.skipBytes(8) == 8); |
| |
| byte[] file = new byte[resFileHeaderSize]; |
| compiledFileStream.read(file, 0, resFileHeaderSize); |
| CompiledFile compiledFile = CompiledFile.parseFrom(file); |
| |
| Path sourcePath = Paths.get(compiledFile.getSourcePath()); |
| FullyQualifiedName fqn = fqnFactory.parse(sourcePath); |
| DataSource dataSource = DataSource.of(sourcePath); |
| |
| if (consumers != null) { |
| consumers.overwritingConsumer.accept(fqn, DataValueFile.of(dataSource)); |
| } |
| |
| for (CompiledFile.Symbol exportedSymbol : compiledFile.getExportedSymbolList()) { |
| if (!exportedSymbol.getResourceName().startsWith("android:")) { |
| // Skip writing resource xml's for resources in the sdk |
| FullyQualifiedName symbolFqn = |
| fqnFactory.create( |
| ResourceType.ID, exportedSymbol.getResourceName().replaceFirst("id/", "")); |
| |
| DataResourceXml dataResourceXml = |
| DataResourceXml.from(null, dataSource, ResourceType.ID, null); |
| consumers.combiningConsumer.accept(symbolFqn, dataResourceXml); |
| } |
| } |
| } |
| |
| private void readAttributesFile( |
| InputStream resourceFileStream, |
| FileSystem fileSystem, |
| BiConsumer<DataKey, DataResource> combine, |
| BiConsumer<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(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 Map<DataKey, DataResource> readAttributes(CompiledResources resources) { |
| try (ZipInputStream zipStream = new ZipInputStream(Files.newInputStream(resources.getZip()))) { |
| Map<DataKey, DataResource> attributes = new HashMap<>(); |
| for (ZipEntry entry = zipStream.getNextEntry(); |
| entry != null; |
| entry = zipStream.getNextEntry()) { |
| if (entry.getName().endsWith(".attributes")) { |
| readAttributesFile( |
| 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); |
| } |
| } |
| |
| public void readTable(InputStream in, KeyValueConsumers consumers) throws IOException { |
| final ResourceTable resourceTable = ResourceTable.parseFrom(in); |
| readPackages(consumers, resourceTable); |
| } |
| |
| @Override |
| public void read(Path inPath, KeyValueConsumers consumers) { |
| Stopwatch timer = Stopwatch.createStarted(); |
| try (ZipFile zipFile = new ZipFile(inPath.toFile())) { |
| Enumeration<? extends ZipEntry> resourceFiles = zipFile.entries(); |
| |
| while (resourceFiles.hasMoreElements()) { |
| ZipEntry resourceFile = resourceFiles.nextElement(); |
| String fileZipPath = resourceFile.getName(); |
| int resourceSubdirectoryIndex = fileZipPath.indexOf('_', fileZipPath.lastIndexOf('/')); |
| Path filePath = |
| Paths.get( |
| String.format( |
| "%s%c%s", |
| fileZipPath.substring(0, resourceSubdirectoryIndex), |
| '/', |
| fileZipPath.substring(resourceSubdirectoryIndex + 1))); |
| |
| String shortPath = filePath.getParent().getFileName() + "/" + filePath.getFileName(); |
| |
| if (filteredResources.contains(shortPath) && !Files.exists(filePath)) { |
| // Skip files that were filtered out during analysis. |
| // TODO(asteinb): Properly filter out these files from android_library symbol files during |
| // analysis instead, and remove this list. |
| continue; |
| } |
| |
| try (InputStream resourceFileStream = zipFile.getInputStream(resourceFile)) { |
| final String[] dirNameAndQualifiers = |
| filePath.getParent().getFileName().toString().split(SdkConstants.RES_QUALIFIER_SEP); |
| Factory fqnFactory = Factory.fromDirectoryName(dirNameAndQualifiers); |
| |
| if (fileZipPath.endsWith(".attributes")) { |
| readAttributesFile( |
| resourceFileStream, |
| inPath.getFileSystem(), |
| consumers.combiningConsumer, |
| consumers.overwritingConsumer); |
| } else { |
| LittleEndianDataInputStream dataInputStream = |
| new LittleEndianDataInputStream(resourceFileStream); |
| |
| int magicNumber = dataInputStream.readInt(); |
| int formatVersion = dataInputStream.readInt(); |
| int numberOfEntries = dataInputStream.readInt(); |
| int resourceType = dataInputStream.readInt(); |
| |
| if (resourceType == 0) { // 0 is a resource table |
| readResourceTable(dataInputStream, consumers); |
| } else if (resourceType == 1) { // 1 is a resource file |
| readCompiledFile(dataInputStream, consumers, fqnFactory); |
| } else { |
| throw new DeserializationException( |
| "aapt2 version mismatch.", |
| new DeserializationException( |
| String.format( |
| "Unexpected tag for resourceType %s expected 0 or 1 in %s." |
| + "\n Last known good values:" |
| + "\n\tmagicNumber 1414545729 (is %s)" |
| + "\n\tformatVersion 1 (is %s)" |
| + "\n\tnumberOfEntries 1 (is %s)", |
| resourceType, fileZipPath, magicNumber, formatVersion, numberOfEntries))); |
| } |
| } |
| } |
| } |
| } 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 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 { |
| 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; |
| } |
| } |