diff options
24 files changed, 1747 insertions, 213 deletions
diff --git a/src/test/java/com/google/devtools/build/android/AndroidDataSerializerAndDeserializerTest.java b/src/test/java/com/google/devtools/build/android/AndroidDataSerializerAndDeserializerTest.java index 0afaf0cfa5..94529e3db3 100644 --- a/src/test/java/com/google/devtools/build/android/AndroidDataSerializerAndDeserializerTest.java +++ b/src/test/java/com/google/devtools/build/android/AndroidDataSerializerAndDeserializerTest.java @@ -66,7 +66,7 @@ public class AndroidDataSerializerAndDeserializerTest { expected.serializeTo(serializer); serializer.flushTo(binaryPath); - AndroidDataDeserializer deserializer = AndroidDataDeserializer.create(); + AndroidDataDeserializer deserializer = AndroidParsedDataDeserializer.create(); TestMapConsumer<DataAsset> assets = TestMapConsumer.ofAssets(); deserializer.read(binaryPath, KeyValueConsumers.of(null, null, assets)); Truth.assertThat(assets).isEqualTo(expected.getPrimary().getAssets()); @@ -87,7 +87,7 @@ public class AndroidDataSerializerAndDeserializerTest { expected.serializeTo(serializer); serializer.flushTo(binaryPath); - AndroidDataDeserializer deserializer = AndroidDataDeserializer.create(); + AndroidDataDeserializer deserializer = AndroidParsedDataDeserializer.create(); TestMapConsumer<DataResource> resources = TestMapConsumer.ofResources(); deserializer.read( binaryPath, @@ -113,7 +113,7 @@ public class AndroidDataSerializerAndDeserializerTest { expected.serializeTo(serializer); serializer.flushTo(binaryPath); - AndroidDataDeserializer deserializer = AndroidDataDeserializer.create(); + AndroidDataDeserializer deserializer = AndroidParsedDataDeserializer.create(); TestMapConsumer<DataResource> resources = TestMapConsumer.ofResources(); deserializer.read( binaryPath, @@ -166,7 +166,7 @@ public class AndroidDataSerializerAndDeserializerTest { expected.serializeTo(serializer); serializer.flushTo(binaryPath); - AndroidDataDeserializer deserializer = AndroidDataDeserializer.create(); + AndroidDataDeserializer deserializer = AndroidParsedDataDeserializer.create(); TestMapConsumer<DataResource> overwriting = TestMapConsumer.ofResources(); TestMapConsumer<DataResource> combining = TestMapConsumer.ofResources(); deserializer.read( @@ -213,7 +213,7 @@ public class AndroidDataSerializerAndDeserializerTest { TestMapConsumer.ofAssets() // assets ); - AndroidDataDeserializer deserializer = AndroidDataDeserializer.create(); + AndroidDataDeserializer deserializer = AndroidParsedDataDeserializer.create(); deserializer.read(binaryPath, primary); Truth.assertThat(primary.overwritingConsumer) .isEqualTo(expected.getPrimary().getOverwritingResources()); @@ -249,7 +249,7 @@ public class AndroidDataSerializerAndDeserializerTest { serializer.flushTo(binaryPath); AndroidDataDeserializer deserializer = - AndroidDataDeserializer.withFilteredResources( + AndroidParsedDataDeserializer.withFilteredResources( ImmutableList.of("the/boojum", "values/ids.xml", "layout/banker.xml")); KeyValueConsumers primary = diff --git a/src/test/java/com/google/devtools/build/android/DataResourceXmlTest.java b/src/test/java/com/google/devtools/build/android/DataResourceXmlTest.java index 315a9c9ae6..aa91bf91c3 100644 --- a/src/test/java/com/google/devtools/build/android/DataResourceXmlTest.java +++ b/src/test/java/com/google/devtools/build/android/DataResourceXmlTest.java @@ -1071,7 +1071,7 @@ public class DataResourceXmlTest { serializer.queueForSerialization(dimenKey, dimenValue); serializer.flushTo(serialized); - AndroidDataDeserializer deserializer = AndroidDataDeserializer.create(); + AndroidDataDeserializer deserializer = AndroidParsedDataDeserializer.create(); final Map<DataKey, DataResource> toOverwrite = new HashMap<>(); final Map<DataKey, DataResource> toCombine = new HashMap<>(); deserializer.read( @@ -1132,7 +1132,7 @@ public class DataResourceXmlTest { serializer.queueForSerialization(themeKey, themeValue); serializer.flushTo(serialized); - AndroidDataDeserializer deserializer = AndroidDataDeserializer.create(); + AndroidDataDeserializer deserializer = AndroidParsedDataDeserializer.create(); final Map<DataKey, DataResource> toOverwrite = new HashMap<>(); final Map<DataKey, DataResource> toCombine = new HashMap<>(); deserializer.read( @@ -1258,7 +1258,7 @@ public class DataResourceXmlTest { serializer.queueForSerialization(key, value); serializer.flushTo(serialized); - AndroidDataDeserializer deserializer = AndroidDataDeserializer.create(); + AndroidDataDeserializer deserializer = AndroidParsedDataDeserializer.create(); final Map<DataKey, DataResource> toOverwrite = new HashMap<>(); final Map<DataKey, DataResource> toCombine = new HashMap<>(); deserializer.read( diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidCompiledDataDeserializer.java b/src/tools/android/java/com/google/devtools/build/android/AndroidCompiledDataDeserializer.java new file mode 100644 index 0000000000..50ba095cf6 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidCompiledDataDeserializer.java @@ -0,0 +1,279 @@ +// 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 com.android.SdkConstants; +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.Type; +import com.android.aapt.Resources.Value; +import com.android.resources.ResourceType; +import com.google.common.base.Preconditions; +import com.google.common.base.Stopwatch; +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.proto.Format.CompiledFile; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.AbstractMap.SimpleEntry; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** 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()); + + 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( + InputStream resourceTableStream, + KeyValueConsumers consumers, + Factory fqnFactory) throws IOException { + ResourceTable resourceTable = ResourceTable.parseFrom(resourceTableStream); + + List<String> sourcePool = + decodeSourcePool(resourceTable.getSourcePool().getData().toByteArray()); + + Map<String, Entry<FullyQualifiedName, Boolean>> fullyQualifiedNames = new HashMap<>(); + + for (int i = resourceTable.getPackageCount() - 1; i >= 0; i--) { + Package resourceTablePackage = resourceTable.getPackage(i); + + String packageName = ""; + if (!resourceTablePackage.getPackageName().isEmpty()) { + packageName = resourceTablePackage.getPackageName() + ":"; + } + + for (Type resourceFormatType : resourceTablePackage.getTypeList()) { + ResourceType resourceType = ResourceType.getEnum(resourceFormatType.getName()); + + for (Resources.Entry resource : resourceFormatType.getEntryList()) { + Value resourceValue = resource.getConfigValue(0).getValue(); + String resourceName = packageName + resource.getName(); + List<ConfigValue> configValues = resource.getConfigValueList(); + + Preconditions.checkArgument(configValues.size() == 1); + int sourceIndex = + configValues.get(0) + .getValue() + .getSource() + .getPathIdx(); + + String source = sourcePool.get(sourceIndex); + + DataSource dataSource = DataSource.of(Paths.get(source)); + FullyQualifiedName fqn = fqnFactory.create(resourceType, resourceName); + fullyQualifiedNames.put( + packageName + resourceType + "/" + resource.getName(), + new SimpleEntry<FullyQualifiedName, Boolean>(fqn, packageName.isEmpty())); + + if (packageName.isEmpty()) { + DataResourceXml dataResourceXml = DataResourceXml + .from(resourceValue, dataSource, resourceType, fullyQualifiedNames); + if (resourceType == ResourceType.ID + || resourceType == ResourceType.PUBLIC + || resourceType == ResourceType.STYLEABLE) { + consumers.combiningConsumer.accept(fqn, dataResourceXml); + } else { + consumers.overwritingConsumer.accept(fqn, dataResourceXml); + } + } + } + } + } + } + + /** + * 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( + InputStream compiledFileStream, + KeyValueConsumers consumers, + Factory fqnFactory) throws IOException { + LittleEndianDataInputStream dataInputStream = + new LittleEndianDataInputStream(compiledFileStream); + + int numberOfCompiledFiles = dataInputStream.readInt(); + if (numberOfCompiledFiles != 1) { + logger.warning("Compiled resource file has " + + numberOfCompiledFiles + " files. Expected 1 compiled file."); + } + + long length = dataInputStream.readLong(); + byte[] file = new byte[(int) length]; + dataInputStream.read(file, 0, (int) length); + 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.getExportedSymbolsList()) { + FullyQualifiedName symbolFqn = + fqnFactory.create( + ResourceType.ID, exportedSymbol.getResourceName().replaceFirst("id/", "")); + + DataResourceXml dataResourceXml = + DataResourceXml.from(null, dataSource, ResourceType.ID, null); + consumers.combiningConsumer.accept(symbolFqn, dataResourceXml); + } + } + + @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(); + InputStream resourceFileStream = zipFile.getInputStream(resourceFile); + + 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; + } + + final String[] dirNameAndQualifiers = filePath.getParent().getFileName().toString() + .split(SdkConstants.RES_QUALIFIER_SEP); + Factory fqnFactory = Factory.fromDirectoryName(dirNameAndQualifiers); + + if (fileZipPath.endsWith(".arsc.flat")) { + readResourceTable(resourceFileStream, consumers, fqnFactory); + } else { + readCompiledFile(resourceFileStream, consumers, fqnFactory); + } + } + } 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; + } + +} diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidCompiledResourceMergingAction.java b/src/tools/android/java/com/google/devtools/build/android/AndroidCompiledResourceMergingAction.java new file mode 100644 index 0000000000..88ae2be56b --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidCompiledResourceMergingAction.java @@ -0,0 +1,121 @@ +// 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 com.android.builder.core.VariantConfiguration; +import com.android.utils.StdLogger; +import com.google.common.base.Preconditions; +import com.google.common.base.Stopwatch; +import com.google.common.base.Strings; +import com.google.devtools.build.android.AndroidDataMerger.MergeConflictException; +import com.google.devtools.build.android.AndroidResourceMerger.MergingException; +import com.google.devtools.build.android.AndroidResourceMergingAction.Options; +import com.google.devtools.build.android.AndroidResourceProcessor.AaptConfigOptions; +import com.google.devtools.common.options.OptionsParser; +import com.google.devtools.common.options.ShellQuotedParamsFilePreProcessor; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Provides an entry point for the compiled resource merging action. + * + * <p>This action merges compiled intermediate resource files from aapt2 + * and reports merge conflicts. It also provides a merged manifest file + * to {@link ValidateAndLinkResourcesAction} and builds the resource class + * jar for the lib jar + */ +public class AndroidCompiledResourceMergingAction { + + private static final StdLogger stdLogger = new StdLogger(StdLogger.Level.WARNING); + + private static final Logger logger = + Logger.getLogger(AndroidCompiledResourceMergingAction.class.getName()); + + public static void main(String[] args) throws Exception { + final Stopwatch timer = Stopwatch.createStarted(); + OptionsParser optionsParser = + OptionsParser.newOptionsParser(Options.class, AaptConfigOptions.class); + optionsParser.enableParamsFileSupport( + new ShellQuotedParamsFilePreProcessor(FileSystems.getDefault())); + optionsParser.parseAndExitUponError(args); + AaptConfigOptions aaptConfigOptions = optionsParser.getOptions(AaptConfigOptions.class); + Options options = optionsParser.getOptions(Options.class); + + Preconditions.checkNotNull(options.primaryData); + Preconditions.checkNotNull(options.primaryManifest); + Preconditions.checkNotNull(options.manifestOutput); + Preconditions.checkNotNull(options.classJarOutput); + + try (ScopedTemporaryDirectory scopedTmp = + new ScopedTemporaryDirectory("android_resource_merge_tmp")) { + Path tmp = scopedTmp.getPath(); + Path generatedSources = tmp.resolve("generated_resources"); + Path processedManifest = tmp.resolve("manifest-processed/AndroidManifest.xml"); + + logger.fine(String.format("Setup finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); + + String packageForR = options.packageForR; + if (packageForR == null) { + packageForR = + Strings.nullToEmpty( + VariantConfiguration.getManifestPackage(options.primaryManifest.toFile())); + } + AndroidResourceClassWriter resourceClassWriter = + AndroidResourceClassWriter.createWith( + aaptConfigOptions.androidJar, generatedSources, packageForR); + resourceClassWriter.setIncludeClassFile(true); + resourceClassWriter.setIncludeJavaFile(false); + + AndroidResourceMerger.mergeCompiledData( + options.primaryData, + options.primaryManifest, + options.directData, + options.transitiveData, + resourceClassWriter, + options.throwOnResourceConflict); + logger.fine(String.format("Merging finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); + + AndroidResourceOutputs.createClassJar(generatedSources, options.classJarOutput); + logger.fine( + String.format( + "Create classJar finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); + + // Until enough users with manifest placeholders migrate to the new manifest merger, + // we need to replace ${applicationId} and ${packageName} with options.packageForR to make + // the manifests compatible with the old manifest merger. + processedManifest = AndroidManifestProcessor.with(stdLogger) + .processLibraryManifest(options.packageForR, options.primaryManifest, processedManifest); + + Files.createDirectories(options.manifestOutput.getParent()); + Files.copy(processedManifest, options.manifestOutput); + } catch (MergeConflictException e) { + logger.log(Level.SEVERE, e.getMessage()); + System.exit(1); + } catch (MergingException e) { + logger.log(Level.SEVERE, "Error during merging resources", e); + throw e; + } catch (AndroidManifestProcessor.ManifestProcessingException e) { + System.exit(1); + } catch (Exception e) { + logger.log(Level.SEVERE, "Unexpected", e); + throw e; + } + logger.fine(String.format("Resources merged in %sms", timer.elapsed(TimeUnit.MILLISECONDS))); + } + +} diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidDataDeserializer.java b/src/tools/android/java/com/google/devtools/build/android/AndroidDataDeserializer.java index 62756c9ebc..60212bdb20 100644 --- a/src/tools/android/java/com/google/devtools/build/android/AndroidDataDeserializer.java +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidDataDeserializer.java @@ -13,202 +13,12 @@ // limitations under the License. package com.google.devtools.build.android; -import com.google.common.base.Stopwatch; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Maps; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; -import com.google.devtools.build.android.AndroidResourceMerger.MergingException; -import com.google.devtools.build.android.ParsedAndroidData.Builder; -import com.google.devtools.build.android.ParsedAndroidData.KeyValueConsumer; -import com.google.devtools.build.android.proto.SerializeFormat; -import com.google.devtools.build.android.proto.SerializeFormat.Header; -import java.io.Closeable; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.FileSystem; -import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.concurrent.Callable; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.logging.Logger; -/** Deserializes {@link DataKey}, {@link DataValue} entries from a binary file. */ -public class AndroidDataDeserializer { - /** Task to deserialize resources from a path. */ - static final class Deserialize implements Callable<Boolean> { +/** Represents a deserializer to deserialize {@link DataKey} + * and {@link DataValue} from a path and feed to it consumers. + */ +public interface AndroidDataDeserializer { - private final Path symbolPath; - - private final Builder finalDataBuilder; - private final AndroidDataDeserializer deserializer; - - private Deserialize( - AndroidDataDeserializer deserializer, Path symbolPath, Builder finalDataBuilder) { - this.deserializer = deserializer; - this.symbolPath = symbolPath; - this.finalDataBuilder = finalDataBuilder; - } - - @Override - public Boolean call() throws Exception { - final Builder parsedDataBuilder = ParsedAndroidData.Builder.newBuilder(); - deserializer.read(symbolPath, parsedDataBuilder.consumers()); - // The builder isn't threadsafe, so synchronize the copyTo call. - synchronized (finalDataBuilder) { - // All the resources are sorted before writing, so they can be aggregated in - // whatever order here. - parsedDataBuilder.copyTo(finalDataBuilder); - } - return Boolean.TRUE; - } - } - - private static final Logger logger = Logger.getLogger(AndroidDataDeserializer.class.getName()); - - 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 AndroidDataDeserializer withFilteredResources( - Collection<String> filteredResources) { - return new AndroidDataDeserializer(ImmutableSet.copyOf(filteredResources)); - } - - public static AndroidDataDeserializer create() { - return new AndroidDataDeserializer(ImmutableSet.<String>of()); - } - - private AndroidDataDeserializer(ImmutableSet<String> filteredResources) { - this.filteredResources = filteredResources; - } - - /** - * Reads the serialized {@link DataKey} and {@link DataValue} to the {@link KeyValueConsumers}. - * - * @param inPath The path to the serialized protocol buffer. - * @param consumers The {@link KeyValueConsumers} for the entries {@link DataKey} -> - * {@link DataValue}. - * @throws DeserializationException Raised for an IOException or when the inPath is not a valid - * proto buffer. - */ - public void read(Path inPath, KeyValueConsumers consumers) { - Stopwatch timer = Stopwatch.createStarted(); - try (InputStream in = Files.newInputStream(inPath, StandardOpenOption.READ)) { - FileSystem currentFileSystem = inPath.getFileSystem(); - Header header = Header.parseDelimitedFrom(in); - if (header == null) { - throw new DeserializationException("No Header found in " + inPath); - } - readEntriesSegment(consumers, in, currentFileSystem, header); - } catch (IOException e) { - throw new DeserializationException("Error deserializing " + inPath, e); - } finally { - logger.fine( - String.format("Deserialized in merged in %sms", timer.elapsed(TimeUnit.MILLISECONDS))); - } - } - - private void readEntriesSegment( - KeyValueConsumers consumers, - InputStream in, - FileSystem currentFileSystem, - Header header) - throws IOException { - int numberOfEntries = header.getEntryCount(); - Map<DataKey, KeyValueConsumer<DataKey, ? extends DataValue>> keys = - Maps.newLinkedHashMapWithExpectedSize(numberOfEntries); - for (int i = 0; i < numberOfEntries; i++) { - SerializeFormat.DataKey protoKey = SerializeFormat.DataKey.parseDelimitedFrom(in); - if (protoKey.hasResourceType()) { - FullyQualifiedName resourceName = FullyQualifiedName.fromProto(protoKey); - keys.put( - resourceName, - resourceName.isOverwritable() - ? consumers.overwritingConsumer - : consumers.combiningConsumer); - } else { - keys.put(RelativeAssetPath.fromProto(protoKey, currentFileSystem), consumers.assetConsumer); - } - } - - // Read back the sources table. - DataSourceTable sourceTable = DataSourceTable.read(in, currentFileSystem, header); - - // TODO(corysmith): Make this a lazy read of the values. - for (Entry<DataKey, KeyValueConsumer<DataKey, ?>> entry : keys.entrySet()) { - SerializeFormat.DataValue protoValue = SerializeFormat.DataValue.parseDelimitedFrom(in); - DataSource source = sourceTable.sourceFromId(protoValue.getSourceId()); - // Compose the `shortPath` manually to ensure it uses a forward slash. - // Using Path.subpath would return a backslash-using path on Windows. - String shortPath = - source.getPath().getParent().getFileName() + "/" + source.getPath().getFileName(); - if (filteredResources.contains(shortPath) && !Files.exists(source.getPath())) { - // 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; - } - if (protoValue.hasXmlValue()) { - // TODO(corysmith): Figure out why the generics are wrong. - // If I use Map<DataKey, KeyValueConsumer<DataKey, ? extends DataValue>>, I can put - // consumers into the map, but I can't call accept. - // If I use Map<DataKey, KeyValueConsumer<DataKey, ? super DataValue>>, I can consume - // but I can't put. - // Same for below. - @SuppressWarnings("unchecked") - KeyValueConsumer<DataKey, DataValue> value = - (KeyValueConsumer<DataKey, DataValue>) entry.getValue(); - value.accept(entry.getKey(), DataResourceXml.from(protoValue, source)); - } else { - @SuppressWarnings("unchecked") - KeyValueConsumer<DataKey, DataValue> value = - (KeyValueConsumer<DataKey, DataValue>) entry.getValue(); - value.accept(entry.getKey(), DataValueFile.of(source)); - } - } - } - - /** Deserializes a list of serialized resource paths to a {@link ParsedAndroidData}. */ - public static ParsedAndroidData deserializeSymbolsToData(List<Path> symbolPaths) - throws IOException, MergingException { - AndroidDataDeserializer deserializer = create(); - final ListeningExecutorService executorService = - MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(15)); - final Builder deserializedDataBuilder = ParsedAndroidData.Builder.newBuilder(); - try (Closeable closeable = ExecutorServiceCloser.createWith(executorService)) { - List<ListenableFuture<Boolean>> deserializing = new ArrayList<>(); - for (final Path symbolPath : symbolPaths) { - deserializing.add( - executorService.submit( - new AndroidDataDeserializer.Deserialize( - deserializer, symbolPath, deserializedDataBuilder))); - } - FailedFutureAggregator<MergingException> aggregator = - FailedFutureAggregator.createForMergingExceptionWithMessage( - "Failure(s) during dependency parsing"); - aggregator.aggregateAndMaybeThrow(deserializing); - } - return deserializedDataBuilder.build(); - } - - public static ParsedAndroidData deserializeSingleAndroidData(SerializedAndroidData data) - throws MergingException { - final ParsedAndroidData.Builder builder = ParsedAndroidData.Builder.newBuilder(); - final AndroidDataDeserializer deserializer = create(); - data.deserialize(deserializer, builder.consumers()); - return builder.build(); - } + void read(Path inPath, KeyValueConsumers consumers); } - - diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidDataMerger.java b/src/tools/android/java/com/google/devtools/build/android/AndroidDataMerger.java index 8e550a2d60..16b43b848e 100644 --- a/src/tools/android/java/com/google/devtools/build/android/AndroidDataMerger.java +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidDataMerger.java @@ -119,7 +119,9 @@ class AndroidDataMerger { @VisibleForTesting static AndroidDataMerger createWithDefaultThreadPool(SourceChecker deDuplicator) { return new AndroidDataMerger( - deDuplicator, MoreExecutors.newDirectExecutorService(), AndroidDataDeserializer.create()); + deDuplicator, + MoreExecutors.newDirectExecutorService(), + AndroidParsedDataDeserializer.create()); } /** Creates a merger with a file contents hashing deduplicator. */ diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidParsedDataDeserializer.java b/src/tools/android/java/com/google/devtools/build/android/AndroidParsedDataDeserializer.java new file mode 100644 index 0000000000..71787b4b39 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidParsedDataDeserializer.java @@ -0,0 +1,215 @@ +// 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 com.google.common.base.Stopwatch; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.devtools.build.android.AndroidResourceMerger.MergingException; +import com.google.devtools.build.android.ParsedAndroidData.Builder; +import com.google.devtools.build.android.ParsedAndroidData.KeyValueConsumer; +import com.google.devtools.build.android.proto.SerializeFormat; +import com.google.devtools.build.android.proto.SerializeFormat.Header; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +/** Deserializes {@link DataKey}, {@link DataValue} entries from a binary file. */ +public class AndroidParsedDataDeserializer implements AndroidDataDeserializer { + /** Task to deserialize resources from a path. */ + static final class Deserialize implements Callable<Boolean> { + + private final Path symbolPath; + + private final Builder finalDataBuilder; + private final AndroidParsedDataDeserializer deserializer; + + private Deserialize( + AndroidParsedDataDeserializer deserializer, Path symbolPath, Builder finalDataBuilder) { + this.deserializer = deserializer; + this.symbolPath = symbolPath; + this.finalDataBuilder = finalDataBuilder; + } + + @Override + public Boolean call() throws Exception { + final Builder parsedDataBuilder = ParsedAndroidData.Builder.newBuilder(); + deserializer.read(symbolPath, parsedDataBuilder.consumers()); + // The builder isn't threadsafe, so synchronize the copyTo call. + synchronized (finalDataBuilder) { + // All the resources are sorted before writing, so they can be aggregated in + // whatever order here. + parsedDataBuilder.copyTo(finalDataBuilder); + } + return Boolean.TRUE; + } + } + + private static final Logger logger = + Logger.getLogger(AndroidParsedDataDeserializer.class.getName()); + + 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 AndroidParsedDataDeserializer withFilteredResources( + Collection<String> filteredResources) { + return new AndroidParsedDataDeserializer(ImmutableSet.copyOf(filteredResources)); + } + + public static AndroidParsedDataDeserializer create() { + return new AndroidParsedDataDeserializer(ImmutableSet.<String>of()); + } + + private AndroidParsedDataDeserializer(ImmutableSet<String> filteredResources) { + this.filteredResources = filteredResources; + } + + /** + * Reads the serialized {@link DataKey} and {@link DataValue} to the {@link KeyValueConsumers}. + * + * @param inPath The path to the serialized protocol buffer. + * @param consumers The {@link KeyValueConsumers} for the entries {@link DataKey} -> {@link + * DataValue}. + * @throws DeserializationException Raised for an IOException or when the inPath is not a valid + * proto buffer. + */ + @Override + public void read(Path inPath, KeyValueConsumers consumers) { + Stopwatch timer = Stopwatch.createStarted(); + try (InputStream in = Files.newInputStream(inPath, StandardOpenOption.READ)) { + FileSystem currentFileSystem = inPath.getFileSystem(); + Header header = Header.parseDelimitedFrom(in); + if (header == null) { + throw new DeserializationException("No Header found in " + inPath); + } + readEntriesSegment(consumers, in, currentFileSystem, header); + } catch (IOException e) { + throw new DeserializationException("Error deserializing " + inPath, e); + } finally { + logger.fine( + String.format("Deserialized in merged in %sms", timer.elapsed(TimeUnit.MILLISECONDS))); + } + } + + private void readEntriesSegment( + KeyValueConsumers consumers, + InputStream in, + FileSystem currentFileSystem, + Header header) + throws IOException { + int numberOfEntries = header.getEntryCount(); + Map<DataKey, KeyValueConsumer<DataKey, ? extends DataValue>> keys = + Maps.newLinkedHashMapWithExpectedSize(numberOfEntries); + for (int i = 0; i < numberOfEntries; i++) { + SerializeFormat.DataKey protoKey = SerializeFormat.DataKey.parseDelimitedFrom(in); + if (protoKey.hasResourceType()) { + FullyQualifiedName resourceName = FullyQualifiedName.fromProto(protoKey); + keys.put( + resourceName, + resourceName.isOverwritable() + ? consumers.overwritingConsumer + : consumers.combiningConsumer); + } else { + keys.put(RelativeAssetPath.fromProto(protoKey, currentFileSystem), consumers.assetConsumer); + } + } + + // Read back the sources table. + DataSourceTable sourceTable = DataSourceTable.read(in, currentFileSystem, header); + + // TODO(corysmith): Make this a lazy read of the values. + for (Entry<DataKey, KeyValueConsumer<DataKey, ?>> entry : keys.entrySet()) { + SerializeFormat.DataValue protoValue = SerializeFormat.DataValue.parseDelimitedFrom(in); + DataSource source = sourceTable.sourceFromId(protoValue.getSourceId()); + // Compose the `shortPath` manually to ensure it uses a forward slash. + // Using Path.subpath would return a backslash-using path on Windows. + String shortPath = + source.getPath().getParent().getFileName() + "/" + source.getPath().getFileName(); + if (filteredResources.contains(shortPath) && !Files.exists(source.getPath())) { + // 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; + } + if (protoValue.hasXmlValue()) { + // TODO(corysmith): Figure out why the generics are wrong. + // If I use Map<DataKey, KeyValueConsumer<DataKey, ? extends DataValue>>, I can put + // consumers into the map, but I can't call accept. + // If I use Map<DataKey, KeyValueConsumer<DataKey, ? super DataValue>>, I can consume + // but I can't put. + // Same for below. + @SuppressWarnings("unchecked") + KeyValueConsumer<DataKey, DataValue> value = + (KeyValueConsumer<DataKey, DataValue>) entry.getValue(); + value.accept(entry.getKey(), DataResourceXml.from(protoValue, source)); + } else { + @SuppressWarnings("unchecked") + KeyValueConsumer<DataKey, DataValue> value = + (KeyValueConsumer<DataKey, DataValue>) entry.getValue(); + value.accept(entry.getKey(), DataValueFile.of(source)); + } + } + } + + /** Deserializes a list of serialized resource paths to a {@link ParsedAndroidData}. */ + public static ParsedAndroidData deserializeSymbolsToData(List<Path> symbolPaths) + throws IOException { + AndroidParsedDataDeserializer deserializer = create(); + final ListeningExecutorService executorService = + MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(15)); + final Builder deserializedDataBuilder = ParsedAndroidData.Builder.newBuilder(); + try (Closeable closeable = ExecutorServiceCloser.createWith(executorService)) { + List<ListenableFuture<Boolean>> deserializing = new ArrayList<>(); + for (final Path symbolPath : symbolPaths) { + deserializing.add( + executorService.submit( + new AndroidParsedDataDeserializer.Deserialize( + deserializer, symbolPath, deserializedDataBuilder))); + } + FailedFutureAggregator<MergingException> aggregator = + FailedFutureAggregator.createForMergingExceptionWithMessage( + "Failure(s) during dependency parsing"); + aggregator.aggregateAndMaybeThrow(deserializing); + } + return deserializedDataBuilder.build(); + } + + public static ParsedAndroidData deserializeSingleAndroidData(SerializedAndroidData data) { + final ParsedAndroidData.Builder builder = ParsedAndroidData.Builder.newBuilder(); + final AndroidParsedDataDeserializer deserializer = create(); + data.deserialize(deserializer, builder.consumers()); + return builder.build(); + } +} + + diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceMerger.java b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceMerger.java index eb7fd70711..bc9bf6d408 100644 --- a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceMerger.java +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceMerger.java @@ -161,7 +161,7 @@ public class AndroidResourceMerger { type, symbolsOut, null /* rclassWriter */, - AndroidDataDeserializer.withFilteredResources(filteredResources), + AndroidParsedDataDeserializer.withFilteredResources(filteredResources), throwOnResourceConflict); } catch (IOException e) { throw MergingException.wrapException(e); @@ -185,7 +185,7 @@ public class AndroidResourceMerger { @Nullable final AndroidResourceClassWriter rclassWriter, boolean throwOnResourceConflict) { final ParsedAndroidData.Builder primaryBuilder = ParsedAndroidData.Builder.newBuilder(); - final AndroidDataDeserializer deserializer = AndroidDataDeserializer.create(); + final AndroidDataDeserializer deserializer = AndroidParsedDataDeserializer.create(); primary.deserialize(deserializer, primaryBuilder.consumers()); ParsedAndroidData primaryData = primaryBuilder.build(); return mergeData( @@ -202,6 +202,48 @@ public class AndroidResourceMerger { deserializer, throwOnResourceConflict); } + + /** + * Merges all secondary compiled resources with the primary compiled resources, + * given that the primary resources have been separately compiled + */ + public static void mergeCompiledData( + final SerializedAndroidData primary, + final Path primaryManifest, + final List<? extends SerializedAndroidData> direct, + final List<? extends SerializedAndroidData> transitive, + @Nullable final AndroidResourceClassWriter rclassWriter, + boolean throwOnResourceConflict) { + final ParsedAndroidData.Builder primaryBuilder = ParsedAndroidData.Builder.newBuilder(); + final AndroidDataDeserializer deserializer = AndroidCompiledDataDeserializer.create(); + primary.deserialize(deserializer, primaryBuilder.consumers()); + ParsedAndroidData primaryData = primaryBuilder.build(); + + Stopwatch timer = Stopwatch.createStarted(); + final ListeningExecutorService executorService = ExecutorServiceCloser.createDefaultService(); + try (Closeable closeable = ExecutorServiceCloser.createWith(executorService)) { + UnwrittenMergedAndroidData merged = + mergeData( + executorService, + transitive, + direct, + primaryData, + primaryManifest, + false, + deserializer, + throwOnResourceConflict); + timer.reset().start(); + merged.writeResourceClass(rclassWriter); + logger.fine( + String.format("write classes finished in %sms", timer.elapsed(TimeUnit.MILLISECONDS))); + timer.reset().start(); + } catch (IOException e) { + throw MergingException.wrapException(e); + } finally { + logger.fine( + String.format("write merge finished in %sms", timer.elapsed(TimeUnit.MILLISECONDS))); + } + } } diff --git a/src/tools/android/java/com/google/devtools/build/android/BUILD b/src/tools/android/java/com/google/devtools/build/android/BUILD index d563c772df..74080c9960 100644 --- a/src/tools/android/java/com/google/devtools/build/android/BUILD +++ b/src/tools/android/java/com/google/devtools/build/android/BUILD @@ -61,6 +61,8 @@ java_library( "//src/java_tools/singlejar/java/com/google/devtools/build/zip", "//src/main/java/com/google/devtools/common/options", "//src/tools/android/java/com/google/devtools/build/android/junctions", + "//src/tools/android/java/com/google/devtools/build/android/proto:format_java_pb", + "//src/tools/android/java/com/google/devtools/build/android/proto:resources_java_proto", "//src/tools/android/java/com/google/devtools/build/android/proto:serialize_format_java_pb", "//src/tools/android/java/com/google/devtools/build/android/resources", "//third_party:android_common_25_0_0", diff --git a/src/tools/android/java/com/google/devtools/build/android/DataResourceXml.java b/src/tools/android/java/com/google/devtools/build/android/DataResourceXml.java index 51b0688f10..145725f941 100644 --- a/src/tools/android/java/com/google/devtools/build/android/DataResourceXml.java +++ b/src/tools/android/java/com/google/devtools/build/android/DataResourceXml.java @@ -17,6 +17,7 @@ import static com.android.resources.ResourceType.DECLARE_STYLEABLE; import static com.android.resources.ResourceType.ID; import static com.android.resources.ResourceType.PUBLIC; +import com.android.aapt.Resources.Value; import com.android.resources.ResourceType; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; @@ -44,6 +45,8 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; import java.util.Objects; import javax.xml.stream.FactoryConfigurationError; import javax.xml.stream.XMLEventReader; @@ -201,6 +204,62 @@ public class DataResourceXml implements DataResource { } } + public static DataResourceXml from( + Value protoValue, + DataSource source, + ResourceType resourceType, + Map<String, Entry<FullyQualifiedName, Boolean>> fullyQualifiedNames) + throws InvalidProtocolBufferException { + DataResourceXml dataResourceXml = createWithNamespaces( + source, + valueFromProto(protoValue, resourceType, fullyQualifiedNames), + Namespaces.empty()); + return dataResourceXml; + } + + private static XmlResourceValue valueFromProto( + Value proto, + ResourceType resourceType, + Map<String, Entry<FullyQualifiedName, Boolean>> fullyQualifiedNames) + throws InvalidProtocolBufferException { + switch (resourceType) { + case STYLE: + return StyleXmlResourceValue.from(proto); + case ARRAY: + return ArrayXmlResourceValue.from(proto); + case PLURALS: + return PluralXmlResourceValue.from(proto); + case ATTR: + return AttrXmlResourceValue.from(proto); + case PUBLIC: + throw new RuntimeException(); + case STYLEABLE: + return StyleableXmlResourceValue.from(proto, fullyQualifiedNames); + case ID: + return IdXmlResourceValue.of(); + case DIMEN: + case LAYOUT: + case STRING: + case BOOL: + case COLOR: + case FRACTION: + case INTEGER: + case DRAWABLE: + case ANIM: + case ANIMATOR: + case DECLARE_STYLEABLE: + case INTERPOLATOR: + case MENU: + case MIPMAP: + case RAW: + case TRANSITION: + case XML: + return SimpleXmlResourceValue.from(proto, resourceType); + default: + throw new IllegalArgumentException(); + } + } + private static XmlResourceValue parseXmlElements( ResourceType resourceType, XMLEventReader eventReader, diff --git a/src/tools/android/java/com/google/devtools/build/android/DependencyAndroidData.java b/src/tools/android/java/com/google/devtools/build/android/DependencyAndroidData.java index d2c72cd644..f59267d23e 100644 --- a/src/tools/android/java/com/google/devtools/build/android/DependencyAndroidData.java +++ b/src/tools/android/java/com/google/devtools/build/android/DependencyAndroidData.java @@ -38,7 +38,7 @@ class DependencyAndroidData extends SerializedAndroidData { private static final Pattern VALID_REGEX = Pattern.compile(".*:.*:.+:.+(:.*){0,2}"); public static final String EXPECTED_FORMAT = - "resources[#resources]:assets[#assets]:manifest:r.txt:static.library.ap_:symbols.bin"; + "resources[#resources]:assets[#assets]:manifest:r.txt(:symbols.zip?):symbols.bin"; public static DependencyAndroidData valueOf(String text) { return valueOf(text, FileSystems.getDefault()); @@ -61,7 +61,9 @@ class DependencyAndroidData extends SerializedAndroidData { if (parts.length == 6) { // contains symbols bin and compiled symbols compiledSymbols = CompiledResources.from(exists(fileSystem.getPath(parts[4]))); symbolsBin = exists(fileSystem.getPath(parts[5])); - } else if (parts.length == 5) { // contains symbols bin + } else if (parts.length == 5) { + //This is either symbols bin or compiled symbols depending on "useCompiledResourcesForMerge" + compiledSymbols = CompiledResources.from(exists(fileSystem.getPath(parts[4]))); symbolsBin = exists(fileSystem.getPath(parts[4])); } diff --git a/src/tools/android/java/com/google/devtools/build/android/GenerateRobolectricResourceSymbolsAction.java b/src/tools/android/java/com/google/devtools/build/android/GenerateRobolectricResourceSymbolsAction.java index 3cd3d9c6e5..fa71b5f3e1 100644 --- a/src/tools/android/java/com/google/devtools/build/android/GenerateRobolectricResourceSymbolsAction.java +++ b/src/tools/android/java/com/google/devtools/build/android/GenerateRobolectricResourceSymbolsAction.java @@ -136,7 +136,7 @@ public class GenerateRobolectricResourceSymbolsAction { final PlaceholderIdFieldInitializerBuilder robolectricIds = PlaceholderIdFieldInitializerBuilder.from(aaptConfigOptions.androidJar); ParsedAndroidData.loadedFrom( - options.data, executorService, AndroidDataDeserializer.create()) + options.data, executorService, AndroidParsedDataDeserializer.create()) .writeResourcesTo( new AndroidResourceSymbolSink() { diff --git a/src/tools/android/java/com/google/devtools/build/android/LibraryRClassGeneratorAction.java b/src/tools/android/java/com/google/devtools/build/android/LibraryRClassGeneratorAction.java index 9adc2fa55c..2cce77e3cf 100644 --- a/src/tools/android/java/com/google/devtools/build/android/LibraryRClassGeneratorAction.java +++ b/src/tools/android/java/com/google/devtools/build/android/LibraryRClassGeneratorAction.java @@ -122,7 +122,7 @@ public class LibraryRClassGeneratorAction { logger.fine(String.format("Setup finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); final ParsedAndroidData data = - AndroidDataDeserializer.deserializeSymbolsToData(options.symbols); + AndroidParsedDataDeserializer.deserializeSymbolsToData(options.symbols); logger.fine( String.format("Deserialization finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); diff --git a/src/tools/android/java/com/google/devtools/build/android/ResourceProcessorBusyBox.java b/src/tools/android/java/com/google/devtools/build/android/ResourceProcessorBusyBox.java index 40de32f0c2..f1da34a520 100644 --- a/src/tools/android/java/com/google/devtools/build/android/ResourceProcessorBusyBox.java +++ b/src/tools/android/java/com/google/devtools/build/android/ResourceProcessorBusyBox.java @@ -92,6 +92,12 @@ public class ResourceProcessorBusyBox { AndroidResourceMergingAction.main(args); } }, + MERGE_COMPILED() { + @Override + void call(String[] args) throws Exception { + AndroidCompiledResourceMergingAction.main(args); + } + }, GENERATE_AAR() { @Override void call(String[] args) throws Exception { @@ -159,7 +165,7 @@ public class ResourceProcessorBusyBox { "The processing tool to execute. " + "Valid tools: PACKAGE, VALIDATE, GENERATE_BINARY_R, GENERATE_LIBRARY_R, PARSE, " + "MERGE, GENERATE_AAR, SHRINK, MERGE_MANIFEST, COMPILE_LIBRARY_RESOURCES, " - + "LINK_STATIC_LIBRARY, AAPT2_PACKAGE, SHRINK_AAPT2." + + "LINK_STATIC_LIBRARY, AAPT2_PACKAGE, SHRINK_AAPT2, MERGE_COMPILED." ) public Tool tool; } diff --git a/src/tools/android/java/com/google/devtools/build/android/aapt2/Aapt2ConfigOptions.java b/src/tools/android/java/com/google/devtools/build/android/aapt2/Aapt2ConfigOptions.java index 0225041418..aa02712957 100644 --- a/src/tools/android/java/com/google/devtools/build/android/aapt2/Aapt2ConfigOptions.java +++ b/src/tools/android/java/com/google/devtools/build/android/aapt2/Aapt2ConfigOptions.java @@ -157,4 +157,14 @@ public class Aapt2ConfigOptions extends OptionsBase { + " the output package name following an underscore." ) public List<String> splits; + + @Option( + name = "useCompiledResourcesForMerge", + defaultValue = "false", + category = "config", + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + help = "Use compiled resources for merging rather than parsed symbols binary." + ) + public boolean useCompiledResourcesForMerge; } diff --git a/src/tools/android/java/com/google/devtools/build/android/proto/BUILD b/src/tools/android/java/com/google/devtools/build/android/proto/BUILD index 8b28e76d02..b64629645a 100644 --- a/src/tools/android/java/com/google/devtools/build/android/proto/BUILD +++ b/src/tools/android/java/com/google/devtools/build/android/proto/BUILD @@ -12,6 +12,27 @@ proto_library( srcs = ["serialize_format.proto"], ) +java_proto_library( + name = "format_java_pb", + deps = [":format_proto"], +) + +proto_library( + name = "format_proto", + srcs = ["format.proto"], +) + +java_proto_library( + name = "resources_java_proto", + strict_deps = 0, + deps = [":resources_proto"], +) + +proto_library( + name = "resources_proto", + srcs = ["Resources.proto"], +) + filegroup( name = "srcs", srcs = glob(["**"]), diff --git a/src/tools/android/java/com/google/devtools/build/android/proto/Resources.proto b/src/tools/android/java/com/google/devtools/build/android/proto/Resources.proto new file mode 100644 index 0000000000..27789a3162 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/proto/Resources.proto @@ -0,0 +1,687 @@ +/* + * 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. + */ + +syntax = "proto3"; + +package aapt.pb; + +option java_package = "com.android.aapt"; + +// A string pool that wraps the binary form of the C++ class +// android::ResStringPool. +message StringPool { + bytes data = 1; +} + +// The position of a declared entity within a file. +message SourcePosition { + uint32 line_number = 1; + uint32 column_number = 2; +} + +// Developer friendly source file information for an entity in the resource +// table. +message Source { + // The index of the string path within the source string pool of a + // ResourceTable. + uint32 path_idx = 1; + SourcePosition position = 2; +} + +// Top level message representing a resource table. +message ResourceTable { + // The string pool containing source paths referenced throughout the resource + // table. This does not end up in the final binary ARSC file. + StringPool source_pool = 1; + + // Resource definitions corresponding to an Android package. + repeated Package package = 2; +} + +// A package ID in the range [0x00, 0xff]. +message PackageId { + uint32 id = 1; +} + +// Defines resources for an Android package. +message Package { + // The package ID of this package, in the range [0x00, 0xff]. + // - ID 0x00 is reserved for shared libraries, or when the ID is assigned at + // run-time. + // - ID 0x01 is reserved for the 'android' package (framework). + // - ID range [0x02, 0x7f) is reserved for auto-assignment to shared libraries + // at run-time. + // - ID 0x7f is reserved for the application package. + // - IDs > 0x7f are reserved for the application as well and are treated as + // feature splits. This may not be set if no ID was assigned. + PackageId package_id = 1; + + // The Java compatible Android package name of the app. + string package_name = 2; + + // The series of types defined by the package. + repeated Type type = 3; +} + +// A type ID in the range [0x01, 0xff]. +message TypeId { + uint32 id = 1; +} + +// A set of resources grouped under a common type. Such types include string, +// layout, xml, dimen, attr, etc. This maps to the second part of a resource +// identifier in Java (R.type.entry). +message Type { + // The ID of the type. This may not be set if no ID was assigned. + TypeId id = 1; + + // The name of the type. This corresponds to the 'type' part of a full + // resource name of the form package:type/entry. The set of legal type names + // is listed in Resource.cpp. + string name = 2; + + // The entries defined for this type. + repeated Entry entry = 3; +} + +// The status of a symbol/entry. This contains information like visibility +// (public/private), comments, and whether the entry can be overridden. +message SymbolStatus { + // The visibility of the resource outside of its package. + enum Visibility { + // No visibility was explicitly specified. This is typically treated as + // private. The distinction is important when two separate R.java files are + // generated: a public and private one. An unknown visibility, in this case, + // would cause the resource to be omitted from either R.java. + UNKNOWN = 0; + + // A resource was explicitly marked as private. This means the resource can + // not be accessed outside of its package unless the @*package:type/entry + // notation is used (the asterisk being the private accessor). If two R.java + // files are generated (private + public), the resource will only be emitted + // to the private R.java file. + PRIVATE = 1; + + // A resource was explicitly marked as public. This means the resource can + // be accessed from any package, and is emitted into all R.java files, + // public and private. + PUBLIC = 2; + } + + Visibility visibility = 1; + + // The path at which this entry's visibility was defined (eg. public.xml). + Source source = 2; + + // The comment associated with the <public> tag. + string comment = 3; + + // Whether the symbol can be merged into another resource table without there + // being an existing definition to override. Used for overlays and set to true + // when <add-resource> is specified. + bool allow_new = 4; +} + +// An entry ID in the range [0x0000, 0xffff]. +message EntryId { + uint32 id = 1; +} + +// An entry declaration. An entry has a full resource ID that is the combination +// of package ID, type ID, and its own entry ID. An entry on its own has no +// value, but values are defined for various configurations/variants. +message Entry { + // The ID of this entry. Together with the package ID and type ID, this forms + // a full resource ID of the form 0xPPTTEEEE, where PP is the package ID, TT + // is the type ID, and EEEE is the entry ID. This may not be set if no ID was + // assigned. + EntryId id = 1; + + // The name of this entry. This corresponds to the 'entry' part of a full + // resource name of the form package:type/entry. + string name = 2; + + // The symbol status of this entry, which includes visibility information. + SymbolStatus symbol_status = 3; + + // The set of values defined for this entry, each corresponding to a different + // configuration/variant. + repeated ConfigValue config_value = 4; +} + +// A Configuration/Value pair. +message ConfigValue { + Configuration config = 1; + Value value = 2; +} + +// The generic meta-data for every value in a resource table. +message Value { + // Where the value was defined. + Source source = 1; + + // Any comment associated with the value. + string comment = 2; + + // Whether the value can be overridden. + bool weak = 3; + + // The value is either an Item or a CompoundValue. + oneof value { + Item item = 4; + CompoundValue compound_value = 5; + } +} + +// An Item is an abstract type. It represents a value that can appear inline in +// many places, such as XML attribute values or on the right hand side of style +// attribute definitions. The concrete type is one of the types below. Only one +// can be set. +message Item { + oneof value { + Reference ref = 1; + String str = 2; + RawString raw_str = 3; + StyledString styled_str = 4; + FileReference file = 5; + Id id = 6; + Primitive prim = 7; + } +} + +// A CompoundValue is an abstract type. It represents a value that is a made of +// other values. These can only usually appear as top-level resources. The +// concrete type is one of the types below. Only one can be set. +message CompoundValue { + oneof value { + Attribute attr = 1; + Style style = 2; + Styleable styleable = 3; + Array array = 4; + Plural plural = 5; + } +} + +// A value that is a reference to another resource. This reference can be by +// name or resource ID. +message Reference { + enum Type { + // A plain reference (@package:type/entry). + REFERENCE = 0; + + // A reference to a theme attribute (?package:type/entry). + ATTRIBUTE = 1; + } + + Type type = 1; + + // The resource ID (0xPPTTEEEE) of the resource being referred. This is + // optional. + uint32 id = 2; + + // The name of the resource being referred. This is optional if the resource + // ID is set. + string name = 3; + + // Whether this reference is referencing a private resource + // (@*package:type/entry). + bool private = 4; +} + +// A value that represents an ID. This is just a placeholder, as ID values are +// used to occupy a resource ID (0xPPTTEEEE) as a unique identifier. Their value +// is unimportant. +message Id { +} + +// A value that is a string. +message String { + string value = 1; +} + +// A value that is a raw string, which is unescaped/uninterpreted. This is +// typically used to represent the value of a style attribute before the +// attribute is compiled and the set of allowed values is known. +message RawString { + string value = 1; +} + +// A string with styling information, like html tags that specify boldness, +// italics, etc. +message StyledString { + // The raw text of the string. + string value = 1; + + // A Span marks a region of the string text that is styled. + message Span { + // The name of the tag, and its attributes, encoded as follows: + // tag_name;attr1=value1;attr2=value2;[...] + string tag = 1; + + // The first character position this span applies to, in UTF-16 offset. + uint32 first_char = 2; + + // The last character position this span applies to, in UTF-16 offset. + uint32 last_char = 3; + } + + repeated Span span = 2; +} + +// A value that is a reference to an external entity, like an XML file or a PNG. +message FileReference { + // Path to a file within the APK (typically res/type-config/entry.ext). + string path = 1; +} + +// A value that represents a primitive data type (float, int, boolean, etc.). +// Corresponds to the fields (type/data) of the C struct android::Res_value. +message Primitive { + uint32 type = 1; + uint32 data = 2; +} + +// A value that represents an XML attribute and what values it accepts. +message Attribute { + // A Symbol used to represent an enum or a flag. + message Symbol { + // Where the enum/flag item was defined. + Source source = 1; + + // Any comments associated with the enum or flag. + string comment = 2; + + // The name of the enum/flag as a reference. Enums/flag items are generated + // as ID resource values. + Reference name = 3; + + // The value of the enum/flag. + uint32 value = 4; + } + + // Bitmask of formats allowed for an attribute. + enum FormatFlags { + NONE = 0x0; // Proto3 requires a default of 0. + ANY = 0x0000ffff; // Allows any type except ENUM and FLAGS. + REFERENCE = 0x01; // Allows Reference values. + STRING = 0x02; // Allows String/StyledString values. + INTEGER = 0x04; // Allows any integer BinaryPrimitive values. + BOOLEAN = 0x08; // Allows any boolean BinaryPrimitive values. + COLOR = 0x010; // Allows any color BinaryPrimitive values. + FLOAT = 0x020; // Allows any float BinaryPrimitive values. + DIMENSION = 0x040; // Allows any dimension BinaryPrimitive values. + FRACTION = 0x080; // Allows any fraction BinaryPrimitive values. + ENUM = 0x00010000; // Allows enums that are defined in the Attribute's + // symbols. + // ENUM and FLAGS cannot BOTH be set. + FLAGS = 0x00020000; // Allows flags that are defined in the Attribute's + // symbols. + // ENUM and FLAGS cannot BOTH be set. + } + + // A bitmask of types that this XML attribute accepts. Corresponds to the + // flags in the enum FormatFlags. + uint32 format_flags = 1; + + // The smallest integer allowed for this XML attribute. Only makes sense if + // the format includes FormatFlags::INTEGER. + int32 min_int = 2; + + // The largest integer allowed for this XML attribute. Only makes sense if the + // format includes FormatFlags::INTEGER. + int32 max_int = 3; + + // The set of enums/flags defined in this attribute. Only makes sense if the + // format includes either FormatFlags::ENUM or FormatFlags::FLAGS. Having both + // is an error. + repeated Symbol symbol = 4; +} + +// A value that represents a style. +message Style { + // An XML attribute/value pair defined in the style. + message Entry { + // Where the entry was defined. + Source source = 1; + + // Any comments associated with the entry. + string comment = 2; + + // A reference to the XML attribute. + Reference key = 3; + + // The Item defined for this XML attribute. + Item item = 4; + } + + // The optinal style from which this style inherits attributes. + Reference parent = 1; + + // The source file information of the parent inheritance declaration. + Source parent_source = 2; + + // The set of XML attribute/value pairs for this style. + repeated Entry entry = 3; +} + +// A value that represents a <declare-styleable> XML resource. These are not +// real resources and only end up as Java fields in the generated R.java. They +// do not end up in the binary ARSC file. +message Styleable { + // An attribute defined for this styleable. + message Entry { + // Where the attribute was defined within the <declare-styleable> block. + Source source = 1; + + // Any comments associated with the declaration. + string comment = 2; + + // The reference to the attribute. + Reference attr = 3; + } + + // The set of attribute declarations. + repeated Entry entry = 1; +} + +// A value that represents an array of resource values. +message Array { + // A single element of the array. + message Element { + // Where the element was defined. + Source source = 1; + + // Any comments associated with the element. + string comment = 2; + + // The value assigned to this element. + Item item = 3; + } + + // The list of array elements. + repeated Element element = 1; +} + +// A value that represents a string and its many variations based on plurality. +message Plural { + // The arity of the plural. + enum Arity { + ZERO = 0; + ONE = 1; + TWO = 2; + FEW = 3; + MANY = 4; + OTHER = 5; + } + + // The plural value for a given arity. + message Entry { + // Where the plural was defined. + Source source = 1; + + // Any comments associated with the plural. + string comment = 2; + + // The arity of the plural. + Arity arity = 3; + + // The value assigned to this plural. + Item item = 4; + } + + // The set of arity/plural mappings. + repeated Entry entry = 1; +} + +// Defines an abstract XmlNode that must be either an XmlElement, or +// a text node represented by a string. +message XmlNode { + oneof node { + XmlElement element = 1; + string text = 2; + } + + // Source line and column info. + SourcePosition source = 3; +} + +// An <element> in an XML document. +message XmlElement { + // Namespaces defined on this element. + repeated XmlNamespace namespace_declaration = 1; + + // The namespace URI of this element. + string namespace_uri = 2; + + // The name of this element. + string name = 3; + + // The attributes of this element. + repeated XmlAttribute attribute = 4; + + // The children of this element. + repeated XmlNode child = 5; +} + +// A namespace declaration on an XmlElement (xmlns:android="http://..."). +message XmlNamespace { + string prefix = 1; + string uri = 2; + + // Source line and column info. + SourcePosition source = 3; +} + +// An attribute defined on an XmlElement (android:text="..."). +message XmlAttribute { + string namespace_uri = 1; + string name = 2; + string value = 3; + + // Source line and column info. + SourcePosition source = 4; + + // The optional resource ID (0xPPTTEEEE) of the attribute. + uint32 resource_id = 5; + + // The optional interpreted/compiled version of the `value` string. + Item compiled_item = 6; +} + +// A description of the requirements a device must have in order for a +// resource to be matched and selected. +message Configuration { + enum LayoutDirection { + LAYOUT_DIRECTION_UNSET = 0; + LAYOUT_DIRECTION_LTR = 1; + LAYOUT_DIRECTION_RTL = 2; + } + + enum ScreenLayoutSize { + SCREEN_LAYOUT_SIZE_UNSET = 0; + SCREEN_LAYOUT_SIZE_SMALL = 1; + SCREEN_LAYOUT_SIZE_NORMAL = 2; + SCREEN_LAYOUT_SIZE_LARGE = 3; + SCREEN_LAYOUT_SIZE_XLARGE = 4; + } + + enum ScreenLayoutLong { + SCREEN_LAYOUT_LONG_UNSET = 0; + SCREEN_LAYOUT_LONG_LONG = 1; + SCREEN_LAYOUT_LONG_NOTLONG = 2; + } + + enum ScreenRound { + SCREEN_ROUND_UNSET = 0; + SCREEN_ROUND_ROUND = 1; + SCREEN_ROUND_NOTROUND = 2; + } + + enum WideColorGamut { + WIDE_COLOR_GAMUT_UNSET = 0; + WIDE_COLOR_GAMUT_WIDECG = 1; + WIDE_COLOR_GAMUT_NOWIDECG = 2; + } + + enum Hdr { + HDR_UNSET = 0; + HDR_HIGHDR = 1; + HDR_LOWDR = 2; + } + + enum Orientation { + ORIENTATION_UNSET = 0; + ORIENTATION_PORT = 1; + ORIENTATION_LAND = 2; + ORIENTATION_SQUARE = 3; + } + + enum UiModeType { + UI_MODE_TYPE_UNSET = 0; + UI_MODE_TYPE_NORMAL = 1; + UI_MODE_TYPE_DESK = 2; + UI_MODE_TYPE_CAR = 3; + UI_MODE_TYPE_TELEVISION = 4; + UI_MODE_TYPE_APPLIANCE = 5; + UI_MODE_TYPE_WATCH = 6; + UI_MODE_TYPE_VRHEADSET = 7; + } + + enum UiModeNight { + UI_MODE_NIGHT_UNSET = 0; + UI_MODE_NIGHT_NIGHT = 1; + UI_MODE_NIGHT_NOTNIGHT = 2; + } + + enum Touchscreen { + TOUCHSCREEN_UNSET = 0; + TOUCHSCREEN_NOTOUCH = 1; + TOUCHSCREEN_STYLUS = 2; + TOUCHSCREEN_FINGER = 3; + } + + enum KeysHidden { + KEYS_HIDDEN_UNSET = 0; + KEYS_HIDDEN_KEYSEXPOSED = 1; + KEYS_HIDDEN_KEYSHIDDEN = 2; + KEYS_HIDDEN_KEYSSOFT = 3; + } + + enum Keyboard { + KEYBOARD_UNSET = 0; + KEYBOARD_NOKEYS = 1; + KEYBOARD_QWERTY = 2; + KEYBOARD_TWELVEKEY = 3; + } + + enum NavHidden { + NAV_HIDDEN_UNSET = 0; + NAV_HIDDEN_NAVEXPOSED = 1; + NAV_HIDDEN_NAVHIDDEN = 2; + } + + enum Navigation { + NAVIGATION_UNSET = 0; + NAVIGATION_NONAV = 1; + NAVIGATION_DPAD = 2; + NAVIGATION_TRACKBALL = 3; + NAVIGATION_WHEEL = 4; + } + + // + // Axis/dimensions that are understood by the runtime. + // + + // Mobile country code. + uint32 mcc = 1; + + // Mobile network code. + uint32 mnc = 2; + + // BCP-47 locale tag. + string locale = 3; + + // Left-to-right, right-to-left... + LayoutDirection layout_direction = 4; + + // Screen width in pixels. Prefer screen_width_dp. + uint32 screen_width = 5; + + // Screen height in pixels. Prefer screen_height_dp. + uint32 screen_height = 6; + + // Screen width in density independent pixels (dp). + uint32 screen_width_dp = 7; + + // Screen height in density independent pixels (dp). + uint32 screen_height_dp = 8; + + // The smallest screen dimension, regardless of orientation, in dp. + uint32 smallest_screen_width_dp = 9; + + // Whether the device screen is classified as small, normal, large, xlarge. + ScreenLayoutSize screen_layout_size = 10; + + // Whether the device screen is long. + ScreenLayoutLong screen_layout_long = 11; + + // Whether the screen is round (Android Wear). + ScreenRound screen_round = 12; + + // Whether the screen supports wide color gamut. + WideColorGamut wide_color_gamut = 13; + + // Whether the screen has high dynamic range. + Hdr hdr = 14; + + // Which orientation the device is in (portrait, landscape). + Orientation orientation = 15; + + // Which type of UI mode the device is in (television, car, etc.). + UiModeType ui_mode_type = 16; + + // Whether the device is in night mode. + UiModeNight ui_mode_night = 17; + + // The device's screen density in dots-per-inch (dpi). + uint32 density = 18; + + // Whether a touchscreen exists, supports a stylus, or finger. + Touchscreen touchscreen = 19; + + // Whether the keyboard hardware keys are currently hidden, exposed, or + // if the keyboard is a software keyboard. + KeysHidden keys_hidden = 20; + + // The type of keyboard present (none, QWERTY, 12-key). + Keyboard keyboard = 21; + + // Whether the navigation is exposed or hidden. + NavHidden nav_hidden = 22; + + // The type of navigation present on the device + // (trackball, wheel, dpad, etc.). + Navigation navigation = 23; + + // The minimum SDK version of the device. + uint32 sdk_version = 24; + + // + // Build-time only dimensions. + // + + string product = 25; +} diff --git a/src/tools/android/java/com/google/devtools/build/android/proto/format.proto b/src/tools/android/java/com/google/devtools/build/android/proto/format.proto new file mode 100644 index 0000000000..6caeedb368 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/proto/format.proto @@ -0,0 +1,43 @@ +// 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. + +syntax = "proto2"; + + +// option java_api_version = 2; +option optimize_for = LITE_RUNTIME; + +package aapt.pb; +option java_package = "com.google.devtools.build.android.proto"; + +message ConfigDescription { + optional bytes data = 1; + optional string product = 2; +} + +message StringPool { + optional bytes data = 1; +} + +message CompiledFile { + message Symbol { + optional string resource_name = 1; + optional uint32 line_no = 2; + } + + optional string resource_name = 1; + optional ConfigDescription config = 2; + optional string source_path = 3; + repeated Symbol exported_symbols = 4; +} diff --git a/src/tools/android/java/com/google/devtools/build/android/xml/ArrayXmlResourceValue.java b/src/tools/android/java/com/google/devtools/build/android/xml/ArrayXmlResourceValue.java index e3f5571c7d..fb74fd2208 100644 --- a/src/tools/android/java/com/google/devtools/build/android/xml/ArrayXmlResourceValue.java +++ b/src/tools/android/java/com/google/devtools/build/android/xml/ArrayXmlResourceValue.java @@ -13,6 +13,10 @@ // limitations under the License. package com.google.devtools.build.android.xml; +import com.android.aapt.Resources.Array; +import com.android.aapt.Resources.Array.Element; +import com.android.aapt.Resources.Item; +import com.android.aapt.Resources.Value; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableList; @@ -117,6 +121,29 @@ public class ArrayXmlResourceValue implements XmlResourceValue { ImmutableMap.copyOf(proto.getAttribute())); } + public static XmlResourceValue from(Value proto) { + Array array = proto.getCompoundValue().getArray(); + List<String> items = new ArrayList<>(); + + for (Element entry : array.getElementList()) { + Item item = entry.getItem(); + + if (item.hasPrim()) { + String stringValue = "#" + Integer.toHexString(item.getPrim().getData()); + items.add(stringValue); + } else if (item.hasRef()) { + items.add("@" + item.getRef().getName()); + } else if (item.hasStr()) { + items.add(item.getStr().getValue()); + } + } + + return of( + ArrayType.ARRAY, + items, + ImmutableMap.of()); + } + @Override public void write( FullyQualifiedName key, DataSource source, AndroidDataWritingVisitor mergedDataWriter) { diff --git a/src/tools/android/java/com/google/devtools/build/android/xml/AttrXmlResourceValue.java b/src/tools/android/java/com/google/devtools/build/android/xml/AttrXmlResourceValue.java index 661e5ddf98..f2645ac0a3 100644 --- a/src/tools/android/java/com/google/devtools/build/android/xml/AttrXmlResourceValue.java +++ b/src/tools/android/java/com/google/devtools/build/android/xml/AttrXmlResourceValue.java @@ -16,6 +16,9 @@ package com.google.devtools.build.android.xml; import static com.google.common.base.Predicates.equalTo; import static com.google.common.base.Predicates.not; +import com.android.aapt.Resources.Attribute; +import com.android.aapt.Resources.Attribute.Symbol; +import com.android.aapt.Resources.Value; import com.android.resources.ResourceType; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; @@ -39,6 +42,7 @@ import java.io.IOException; import java.io.OutputStream; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; @@ -195,6 +199,66 @@ public class AttrXmlResourceValue implements XmlResourceValue { return of(formats.build()); } + public static XmlResourceValue from(Value proto) throws InvalidProtocolBufferException { + Builder<String, ResourceXmlAttrValue> formats = + ImmutableMap.builder(); + + Attribute attribute = proto.getCompoundValue().getAttr(); + int formatFlags = attribute.getFormatFlags(); + + if (formatFlags != 0xFFFF) { + //These flags are defined in AOSP in ResourceTypes.h:ResTable_map + if ((formatFlags & 1 << 0) != 0) { + formats.put("reference", ReferenceResourceXmlAttrValue.of()); + } + if ((formatFlags & 1 << 1) != 0) { + formats.put("string", StringResourceXmlAttrValue.of()); + } + if ((formatFlags & 1 << 2) != 0) { + formats.put("integer", IntegerResourceXmlAttrValue.of()); + } + if ((formatFlags & 1 << 3) != 0) { + formats.put("boolean", BooleanResourceXmlAttrValue.of()); + } + if ((formatFlags & 1 << 4) != 0) { + formats.put("color", ColorResourceXmlAttrValue.of()); + } + if ((formatFlags & 1 << 5) != 0) { + formats.put("float", FloatResourceXmlAttrValue.of()); + } + if ((formatFlags & 1 << 6) != 0) { + formats.put("dimension", DimensionResourceXmlAttrValue.of()); + } + if ((formatFlags & 1 << 7) != 0) { + formats.put("fraction", FractionResourceXmlAttrValue.of()); + } + if ((formatFlags & 1 << 16) != 0) { + Map<String, String> enums = new HashMap<>(); + + for (Symbol attrSymbol : attribute.getSymbolList()) { + String name = attrSymbol.getName().getName().replaceFirst("id/", ""); + enums.put(name, Integer.toString(attrSymbol.getValue())); + } + + formats.put("enum", EnumResourceXmlAttrValue.of(enums)); + } + if ((formatFlags & 1 << 17) != 0) { + Map<String, String> flags = new HashMap<>(); + for (Symbol attrSymbol : attribute.getSymbolList()) { + String name = attrSymbol.getName().getName().replaceFirst("id/", ""); + flags.put(name, Integer.toString(attrSymbol.getValue())); + } + + formats.put("flags", FlagResourceXmlAttrValue.of(flags)); + } + if ((formatFlags & 0xFFFCFF00) != 0) { + throw new InvalidProtocolBufferException( + "Unexpected format flags: " + formatFlags); + } + } + return of(formats.build()); + } + /** * Creates a new {@link AttrXmlResourceValue}. Returns null if there are no formats. */ diff --git a/src/tools/android/java/com/google/devtools/build/android/xml/PluralXmlResourceValue.java b/src/tools/android/java/com/google/devtools/build/android/xml/PluralXmlResourceValue.java index 1ff1578eab..6e1fba18c7 100644 --- a/src/tools/android/java/com/google/devtools/build/android/xml/PluralXmlResourceValue.java +++ b/src/tools/android/java/com/google/devtools/build/android/xml/PluralXmlResourceValue.java @@ -13,6 +13,8 @@ // limitations under the License. package com.google.devtools.build.android.xml; +import com.android.aapt.Resources.Plural; +import com.android.aapt.Resources.Value; import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableMap; import com.google.devtools.build.android.AndroidDataWritingVisitor; @@ -27,6 +29,8 @@ import com.google.devtools.build.android.proto.SerializeFormat.DataValueXml.XmlT import com.google.protobuf.CodedOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import javax.annotation.concurrent.Immutable; @@ -131,6 +135,29 @@ public class PluralXmlResourceValue implements XmlResourceValue { ImmutableMap.copyOf(proto.getMappedStringValue())); } + public static XmlResourceValue from(Value proto) { + Plural plural = proto.getCompoundValue().getPlural(); + + Map<String, String> items = new HashMap<>(); + + for (Plural.Entry entry : plural.getEntryList()) { + String name = entry.getArity().toString().toLowerCase(); + String value = + entry + .getItem() + .getStr() + .toString() + .replace("value: \"", "") + .replace("\"", "") + .replace('\n', ' '); + items.put(name, value); + } + + return createWithAttributesAndValues( + ImmutableMap.of(), + ImmutableMap.copyOf(items)); + } + @Override public int serializeTo(int sourceId, Namespaces namespaces, OutputStream output) throws IOException { diff --git a/src/tools/android/java/com/google/devtools/build/android/xml/SimpleXmlResourceValue.java b/src/tools/android/java/com/google/devtools/build/android/xml/SimpleXmlResourceValue.java index 923c163ad3..4006d4d7a4 100644 --- a/src/tools/android/java/com/google/devtools/build/android/xml/SimpleXmlResourceValue.java +++ b/src/tools/android/java/com/google/devtools/build/android/xml/SimpleXmlResourceValue.java @@ -13,6 +13,10 @@ // limitations under the License. package com.google.devtools.build.android.xml; +import com.android.aapt.Resources.Item; +import com.android.aapt.Resources.StyledString; +import com.android.aapt.Resources.StyledString.Span; +import com.android.aapt.Resources.Value; import com.android.resources.ResourceType; import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableMap; @@ -54,6 +58,8 @@ public class SimpleXmlResourceValue implements XmlResourceValue { static final QName TAG_FRACTION = QName.valueOf("fraction"); static final QName TAG_INTEGER = QName.valueOf("integer"); static final QName TAG_ITEM = QName.valueOf("item"); + static final QName TAG_LAYOUT = QName.valueOf("layout"); + static final QName TAG_MIPMAP = QName.valueOf("mipmap"); static final QName TAG_PUBLIC = QName.valueOf("public"); static final QName TAG_STRING = QName.valueOf("string"); @@ -108,6 +114,20 @@ public class SimpleXmlResourceValue implements XmlResourceValue { return true; } }, + LAYOUT(TAG_LAYOUT) { + @Override + public boolean validate(String value) { + // TODO(corysmith): Validate the item type. + return true; + } + }, + MIPMAP(TAG_MIPMAP) { + @Override + public boolean validate(String value) { + // TODO(corysmith): Validate the item type. + return true; + } + }, PUBLIC(TAG_PUBLIC) { @Override public boolean validate(String value) { @@ -121,7 +141,7 @@ public class SimpleXmlResourceValue implements XmlResourceValue { return true; } }; - private QName tagName; + private final QName tagName; Type(QName tagName) { this.tagName = tagName; @@ -211,6 +231,45 @@ public class SimpleXmlResourceValue implements XmlResourceValue { proto.hasValue() ? proto.getValue() : null); } + public static XmlResourceValue from(Value proto, ResourceType resourceType) { + Item item = proto.getItem(); + String stringValue = null; + + if (item.hasStr()) { + stringValue = item.getStr().toString(); + } else if (item.hasRef()) { + stringValue = "@" + item.getRef().getName(); + } else if (item.hasStyledStr()) { + StyledString styledString = item.getStyledStr(); + StringBuilder stringBuilder = new StringBuilder(styledString.getValue()); + + for (Span span : styledString.getSpanList()) { + stringBuilder.append( + String.format(";%s,%d,%d", span.getTag(), span.getFirstChar(), span.getLastChar())); + } + stringValue = stringBuilder.toString(); + } else if ((resourceType == ResourceType.COLOR + || resourceType == ResourceType.DRAWABLE) && item.hasPrim()) { + stringValue = + String.format("#%1$8s", Integer.toHexString(item.getPrim().getData())).replace(' ', '0'); + } else if (resourceType == ResourceType.INTEGER && item.hasPrim()){ + stringValue = Integer.toString(item.getPrim().getData()); + } else if (resourceType == ResourceType.BOOL && item.hasPrim()) { + stringValue = item.getPrim().getData() == 0 ? "false" : "true"; + } else if (resourceType == ResourceType.FRACTION + || resourceType == ResourceType.DIMEN) { + stringValue = Integer.toString(item.getPrim().getData()); + } else { + throw new IllegalArgumentException( + String.format("'%s' is not a valid resource type.", resourceType)); + } + + return of( + Type.valueOf(resourceType.toString().toUpperCase()), + ImmutableMap.of(), + stringValue); + } + @Override public void writeResourceToClass(FullyQualifiedName key, AndroidResourceSymbolSink sink) { sink.acceptSimpleResource(key.type(), key.name()); diff --git a/src/tools/android/java/com/google/devtools/build/android/xml/StyleXmlResourceValue.java b/src/tools/android/java/com/google/devtools/build/android/xml/StyleXmlResourceValue.java index ccf52b6480..55c3d00fdd 100644 --- a/src/tools/android/java/com/google/devtools/build/android/xml/StyleXmlResourceValue.java +++ b/src/tools/android/java/com/google/devtools/build/android/xml/StyleXmlResourceValue.java @@ -13,6 +13,8 @@ // limitations under the License. package com.google.devtools.build.android.xml; +import com.android.aapt.Resources.Style; +import com.android.aapt.Resources.Value; import com.google.common.base.Function; import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableMap; @@ -27,6 +29,7 @@ import com.google.devtools.build.android.proto.SerializeFormat; import com.google.devtools.build.android.proto.SerializeFormat.DataValueXml.XmlType; import java.io.IOException; import java.io.OutputStream; +import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; @@ -77,6 +80,42 @@ public class StyleXmlResourceValue implements XmlResourceValue { this.values = values; } + public static XmlResourceValue from(Value proto) { + Style style = proto.getCompoundValue().getStyle(); + String parent = ""; + + if (style.hasParent()) { + parent = proto.getCompoundValue().getStyle().getParent().getName(); + } + + Map<String, String> items = itemMapFromProto(style); + + return of(parent, items); + } + + private static Map<String, String> itemMapFromProto(Style style) { + Map<String, String> result = new HashMap<>(); + + for (Style.Entry styleEntry : style.getEntryList()) { + String itemName = styleEntry.getKey().getName().replace("attr/", ""); + String itemValue; + + if (styleEntry.getItem().hasRawStr()) { + itemValue = styleEntry.getItem().getRawStr().getValue(); + } else if (styleEntry.getItem().hasRef()) { + itemValue = "@" + styleEntry.getItem().getRef().getName(); + if (itemValue.equals("@")) { + itemValue = "@null"; + } + } else { + throw new IllegalArgumentException("Could not parse item value from Style resource."); + } + result.put(itemName, itemValue); + } + + return result; + } + @Override public void write( FullyQualifiedName key, DataSource source, AndroidDataWritingVisitor mergedDataWriter) { diff --git a/src/tools/android/java/com/google/devtools/build/android/xml/StyleableXmlResourceValue.java b/src/tools/android/java/com/google/devtools/build/android/xml/StyleableXmlResourceValue.java index 8e55e089d3..c9661690cf 100644 --- a/src/tools/android/java/com/google/devtools/build/android/xml/StyleableXmlResourceValue.java +++ b/src/tools/android/java/com/google/devtools/build/android/xml/StyleableXmlResourceValue.java @@ -13,6 +13,8 @@ // limitations under the License. package com.google.devtools.build.android.xml; +import com.android.aapt.Resources.Styleable; +import com.android.aapt.Resources.Value; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.MoreObjects; @@ -31,6 +33,7 @@ import com.google.devtools.build.android.proto.SerializeFormat.DataValueXml.XmlT import java.io.IOException; import java.io.OutputStream; import java.util.AbstractMap.SimpleEntry; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; @@ -161,6 +164,22 @@ public class StyleableXmlResourceValue implements XmlResourceValue { Iterables.transform(proto.getReferencesList(), DATA_KEY_TO_FULLY_QUALIFIED_NAME))); } + public static XmlResourceValue from( + Value proto, Map<String, Entry<FullyQualifiedName, Boolean>> fullyQualifiedNames) { + Map<FullyQualifiedName, Boolean> attributes = new HashMap<>(); + + Styleable styleable = proto.getCompoundValue().getStyleable(); + for (Styleable.Entry entry : styleable.getEntryList()) { + String attrName = entry.getAttr().getName(); + + Entry<FullyQualifiedName, Boolean> fqnEntry = fullyQualifiedNames.get(attrName); + attributes.put(fqnEntry.getKey(), fqnEntry.getValue()); + fqnEntry.setValue(false); + } + + return of(ImmutableMap.copyOf(attributes)); + } + @Override public int hashCode() { return attrs.hashCode(); |