| // 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 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) { | |
| 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<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; | |
| } | |
| } |