// 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.Preconditions.checkNotNull; import static com.google.common.base.Predicates.not; import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toMap; 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.AbstractMap.SimpleImmutableEntry; 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.Map.Entry; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.logging.Logger; import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; 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 LAYOUT_DIRECTION_MAP = ImmutableMap.of( Configuration.LayoutDirection.LAYOUT_DIRECTION_LTR, LayoutDirection.LTR, Configuration.LayoutDirection.LAYOUT_DIRECTION_RTL, LayoutDirection.RTL); static final ImmutableMap 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 SCREEN_LONG_MAP = ImmutableMap.of( ScreenLayoutLong.SCREEN_LAYOUT_LONG_LONG, ScreenRatio.LONG, ScreenLayoutLong.SCREEN_LAYOUT_LONG_NOTLONG, ScreenRatio.NOTLONG); static final ImmutableMap SCREEN_ROUND_MAP = ImmutableMap.of( Configuration.ScreenRound.SCREEN_ROUND_ROUND, ScreenRound.ROUND, Configuration.ScreenRound.SCREEN_ROUND_NOTROUND, ScreenRound.NOTROUND); private static final ImmutableMap SCREEN_ORIENTATION_MAP = ImmutableMap.of( Orientation.ORIENTATION_LAND, ScreenOrientation.LANDSCAPE, Orientation.ORIENTATION_PORT, ScreenOrientation.PORTRAIT, Orientation.ORIENTATION_SQUARE, ScreenOrientation.SQUARE); private static final ImmutableMap SCREEN_UI_MODE = ImmutableMap.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 NIGHT_MODE_MAP = ImmutableMap.of( UiModeNight.UI_MODE_NIGHT_NIGHT, NightMode.NIGHT, UiModeNight.UI_MODE_NIGHT_NOTNIGHT, NightMode.NOTNIGHT); static final ImmutableMap 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 TOUCH_TYPE_MAP = ImmutableMap.of( Touchscreen.TOUCHSCREEN_FINGER, TouchScreen.FINGER, Touchscreen.TOUCHSCREEN_NOTOUCH, TouchScreen.NOTOUCH, Touchscreen.TOUCHSCREEN_STYLUS, TouchScreen.STYLUS); static final ImmutableMap 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 NAV_STATE_MAP = ImmutableMap.of( NavHidden.NAV_HIDDEN_NAVHIDDEN, NavigationState.HIDDEN, NavHidden.NAV_HIDDEN_NAVEXPOSED, NavigationState.EXPOSED); static final ImmutableMap 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 DENSITY_MAP = ImmutableMap.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 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 filteredResources) { return new AndroidCompiledDataDeserializer(ImmutableSet.copyOf(filteredResources)); } public static AndroidCompiledDataDeserializer create() { return new AndroidCompiledDataDeserializer(ImmutableSet.of()); } private AndroidCompiledDataDeserializer(ImmutableSet 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.read(tableBytes, 0, (int) alignedSize); ResourceTable resourceTable = ResourceTable.parseFrom(tableBytes); readPackages(consumers, resourceTable); } private void readPackages(KeyValueConsumers consumers, ResourceTable resourceTable) throws UnsupportedEncodingException, InvalidProtocolBufferException { List 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 packageName; private final Map qualifiedReferenceInlineStatus; private ReferenceResolver( Optional packageName, Map 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) { return checkNotNull( qualifiedReferenceInlineStatus.get(reference), "%s reference is unsatisfied. Available names: %s", reference, qualifiedReferenceInlineStatus.keySet()) .equals(InlineStatus.INLINEABLE) // Only inline if it's in the current package. && reference.isInPackage(packageName.orElse(FullyQualifiedName.DEFAULT_PACKAGE)); } /** 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 qualifiers) { final FullyQualifiedName fqn = FullyQualifiedName.of( packageName.isEmpty() ? FullyQualifiedName.DEFAULT_PACKAGE : packageName, qualifiers, resourceType, resource.getName()); packageResolver.register(fqn); return fqn; } private List 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 combine, BiConsumer overwrite) throws IOException { Header header = Header.parseDelimitedFrom(resourceFileStream); List 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 readAttributes(CompiledResources resources) { try (ZipFile zipFile = new ZipFile(resources.getZip().toFile())) { return zipFile .stream() .filter(e -> e.getName().endsWith(".attributes")) .flatMap( entry -> { try { final Stream.Builder> builder = Stream.builder(); final BiConsumer consumeToStream = (k, v) -> builder.add(new SimpleImmutableEntry<>(k, v)); readAttributesFile( zipFile.getInputStream(entry), FileSystems.getDefault(), consumeToStream, consumeToStream); return builder.build(); } catch (IOException e) { throw new DeserializationException(e); } }) .collect(toImmutableSetMultimap(Entry::getKey, Entry::getValue)) .asMap() .entrySet() .stream() .collect( toMap( Entry::getKey, e -> e.getValue() .stream() .reduce( ((FullyQualifiedName) e.getKey()).isOverwritable() ? DataResource::overwrite : DataResource::combineWith) .orElseThrow(IllegalStateException::new))); } 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 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 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 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; } }