diff options
author | Xavier Ducrohet <xav@android.com> | 2012-11-30 17:58:29 -0800 |
---|---|---|
committer | Xavier Ducrohet <xav@android.com> | 2012-12-10 16:39:27 -0800 |
commit | 3b49b176fcbfe1bbae0f63f769a39a3cb41c79ef (patch) | |
tree | 12b5ab771ec4bcbcdf2810a64373abe4fb09a5e5 /builder | |
parent | ec43c79dad9cae08f059a00b155b334f614a1e1c (diff) | |
download | build-3b49b176fcbfe1bbae0f63f769a39a3cb41c79ef.tar.gz |
Adds a ResourceMerger to the AndroidBuilder library.
This will allow us to manually merge various resource folders.
This supports having multiple non-overlay folders while detecting
duplicates, as well as applying overlays.
The merger then creates a single resource folder that can be
fed to aapt.
This merger should be incremental (not yet) and be as efficient as possible.
Change-Id: I9e9b32954668160f97ff12fa70cd6078386b51c6
Diffstat (limited to 'builder')
25 files changed, 1186 insertions, 0 deletions
diff --git a/builder/prebuilts/common.jar b/builder/prebuilts/common.jar Binary files differindex f6897fa..0398dbc 100644 --- a/builder/prebuilts/common.jar +++ b/builder/prebuilts/common.jar diff --git a/builder/prebuilts/layoutlib_api.jar b/builder/prebuilts/layoutlib_api.jar Binary files differnew file mode 100644 index 0000000..cd6f17a --- /dev/null +++ b/builder/prebuilts/layoutlib_api.jar diff --git a/builder/src/main/java/com/android/builder/resources/DuplicateResourceException.java b/builder/src/main/java/com/android/builder/resources/DuplicateResourceException.java new file mode 100644 index 0000000..94593af --- /dev/null +++ b/builder/src/main/java/com/android/builder/resources/DuplicateResourceException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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.android.builder.resources; + +/** + * Exception when a resource is declared more than once in a {@link ResourceSet} + */ +public class DuplicateResourceException extends Exception { + + private Resource mOne; + private Resource mTwo; + + DuplicateResourceException(Resource one, Resource two) { + mOne = one; + mTwo = two; + } + + public Resource getOne() { + return mOne; + } + + public Resource getTwo() { + return mTwo; + } +} diff --git a/builder/src/main/java/com/android/builder/resources/Resource.java b/builder/src/main/java/com/android/builder/resources/Resource.java new file mode 100644 index 0000000..b5dd69c --- /dev/null +++ b/builder/src/main/java/com/android/builder/resources/Resource.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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.android.builder.resources; + +import com.android.resources.ResourceType; +import org.w3c.dom.Node; + +/** + * A resource. + * + * This includes the name, type, source file as a {@link ResourceFile} and an optional {@link Node} + * in case of a resource coming from a value file. + * + */ +public class Resource { + + private final String mName; + private final ResourceType mType; + + private final Node mValue; + private ResourceFile mSource; + + Resource(String name, ResourceType type, Node value) { + mName = name; + mType = type; + mValue = value; + } + + public String getName() { + return mName; + } + + public ResourceType getType() { + return mType; + } + + public ResourceFile getSource() { + return mSource; + } + + public Node getValue() { + return mValue; + } + + void setSource(ResourceFile sourceFile) { + mSource = sourceFile; + } + + /** + * Returns a key for this resource. They key uniquely identifies this resource by combining + * resource type, qualifiers, and name. + * @return the key for this resource. + */ + String getKey() { + String qualifiers = mSource != null ? mSource.getQualifiers() : null; + if (qualifiers != null) { + return mType.getName() + "-" + qualifiers + "/" + mName; + } + + return mType.getName() + "/" + mName; + } + + @Override + public String toString() { + return "Resource{" + + "mName='" + mName + '\'' + + ", mType=" + mType + + '}'; + } +} diff --git a/builder/src/main/java/com/android/builder/resources/ResourceFile.java b/builder/src/main/java/com/android/builder/resources/ResourceFile.java new file mode 100644 index 0000000..7c335ee --- /dev/null +++ b/builder/src/main/java/com/android/builder/resources/ResourceFile.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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.android.builder.resources; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.google.common.collect.Lists; + +import java.io.File; +import java.util.Collections; +import java.util.List; + +/** + */ +class ResourceFile { + + private final File mFile; + private final List<Resource> mItems; + private final String mQualifiers; + + ResourceFile(File file, Resource item, String qualifiers) { + mFile = file; + mItems = Collections.singletonList(item); + mQualifiers = qualifiers; + + item.setSource(this); + } + + ResourceFile(@NonNull File file, @NonNull List<Resource> items, @Nullable String qualifiers) { + mFile = file; + mItems = Lists.newArrayList(items); + mQualifiers = qualifiers; + + for (Resource item : items) { + item.setSource(this); + } + } + + @NonNull File getFile() { + return mFile; + } + + @Nullable String getQualifiers() { + return mQualifiers; + } +} diff --git a/builder/src/main/java/com/android/builder/resources/ResourceMerger.java b/builder/src/main/java/com/android/builder/resources/ResourceMerger.java new file mode 100644 index 0000000..a059ec2 --- /dev/null +++ b/builder/src/main/java/com/android/builder/resources/ResourceMerger.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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.android.builder.resources; + +import com.google.common.collect.Maps; + +import java.util.Map; + +/** + */ +public class ResourceMerger { + + /** + * The merged resources + */ + private final Map<String, Resource> mItems = Maps.newHashMap(); + + public ResourceMerger() { + + } + + /** + * adds a new sourceset and overlays it on top of the existing resource items + * @param resourceSet the ResourceSet to add. + */ + public void addResourceSet(ResourceSet resourceSet) { + // TODO figure out if we allow partial overlay through a per-resource flag. + mItems.putAll(resourceSet.getResourceMap()); + } + + public ResourceSet getMergedSet() { + return new ResourceSet(mItems); + } +} diff --git a/builder/src/main/java/com/android/builder/resources/ResourceSet.java b/builder/src/main/java/com/android/builder/resources/ResourceSet.java new file mode 100644 index 0000000..7fd7cb5 --- /dev/null +++ b/builder/src/main/java/com/android/builder/resources/ResourceSet.java @@ -0,0 +1,340 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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.android.builder.resources; + +import com.android.SdkConstants; +import com.android.resources.FolderTypeRelationship; +import com.android.resources.ResourceConstants; +import com.android.resources.ResourceFolderType; +import com.android.resources.ResourceType; +import com.android.utils.XmlUtils; +import com.google.common.base.Charsets; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; +import com.google.common.io.Files; +import com.sun.xml.internal.rngom.ast.builder.BuildException; +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * Represents a set of resources at the same level (not overlay) coming from different sources + * (folders or resource bundles) + */ +public class ResourceSet { + + /** + * Resource files + */ + //private final List<ResourceFile> mFiles = Lists.newArrayList(); + + /** + * The key is the {@link com.android.builder.resources.Resource#getKey()}. + */ + private final Map<String, Resource> mItems = Maps.newHashMap(); + + public ResourceSet() { + // nothing done here + } + + ResourceSet(Map<String, Resource> items) { + mItems.putAll(items); + } + + public void addSource(File file) throws DuplicateResourceException { + if (file.isDirectory()) { + readFolder(file); + + } else if (file.isFile()) { + // TODO + } + } + + public int getSize() { + return mItems.size(); + } + + public Collection<Resource> getResources() { + return mItems.values(); + } + + public Map<String, Resource> getResourceMap() { + return mItems; + } + + public void writeTo(File rootFolder) throws IOException { + // map of XML values files to write after parsing all the files. + // the key is the qualifier. + Multimap<String, Resource> map = ArrayListMultimap.create(); + + for (Resource item : mItems.values()) { + if (item.getValue() != null) { + String qualifier = item.getSource().getQualifiers(); + if (qualifier == null) { + qualifier = ""; + } + + map.put(qualifier, item); + } else { + // we can write the file. + ResourceFile resourceFile = item.getSource(); + File file = resourceFile.getFile(); + + String filename = file.getName(); + String folderName = item.getType().getName(); + String qualifiers = resourceFile.getQualifiers(); + if (qualifiers != null && qualifiers.length() > 0) { + folderName = folderName + SdkConstants.RES_QUALIFIER_SEP + qualifiers; + } + + File typeFolder = new File(rootFolder, folderName); + if (!typeFolder.isDirectory()) { + typeFolder.mkdirs(); + } + + File outFile = new File(typeFolder, filename); + Files.copy(file, outFile); + } + } + + // now write the values files. + for (String key : map.keySet()) { + // the key is the qualifier. + String folderName = key.length() > 0 ? + ResourceFolderType.VALUES.getName() + SdkConstants.RES_QUALIFIER_SEP + key : + ResourceFolderType.VALUES.getName(); + + File valuesFolder = new File(rootFolder, folderName); + valuesFolder.mkdirs(); + File outFile = new File(valuesFolder, "values.xml"); + + // get the list of items to write + Collection<Resource> items = map.get(key); + + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setValidating(false); + factory.setIgnoringComments(true); + DocumentBuilder builder; + + try { + builder = factory.newDocumentBuilder(); + Document document = builder.newDocument(); + + Node rootNode = document.createElement(SdkConstants.TAG_RESOURCES); + document.appendChild(rootNode); + + for (Resource item : items) { + Node adoptedNode = adoptNode(document, item.getValue()); + rootNode.appendChild(adoptedNode); + } + + String content = XmlUtils.toXml(document, false /*preserveWhitespace*/); + + Files.write(content, outFile, Charsets.UTF_8); + } catch (ParserConfigurationException e) { + throw new BuildException(e); + } + } + } + + /** + * Makes a new document adopt a node from a different document, and correctly reassign namespace + * and prefix + * @param document the new document + * @param node the node to adopt. + * @return the adopted node. + */ + private Node adoptNode(Document document, Node node) { + Node newNode = document.adoptNode(node); + + updateNamespace(newNode, document); + + return newNode; + } + + /** + * Updates the namespace of a given node (and its children) to work in a given document + * @param node the node to update + * @param document the new document + */ + private void updateNamespace(Node node, Document document) { + + // first process this node + processSingleNodeNamespace(node, document); + + // then its attributes + NamedNodeMap attributes = node.getAttributes(); + if (attributes != null) { + for (int i = 0, n = attributes.getLength(); i < n; i++) { + processSingleNodeNamespace(attributes.item(i), document); + } + } + + // then do it for the children nodes. + NodeList children = node.getChildNodes(); + if (children != null) { + for (int i = 0, n = children.getLength(); i < n; i++) { + Node child = children.item(i); + if (child != null) { + updateNamespace(child, document); + } + } + } + } + + /** + * Update the namespace of a given node to work with a given document. + * @param node the node to update + * @param document the new document + */ + private void processSingleNodeNamespace(Node node, Document document) { + String ns = node.getNamespaceURI(); + if (ns != null) { + NamedNodeMap docAttributes = document.getAttributes(); + + String prefix = getPrefixForNs(docAttributes, ns); + if (prefix == null) { + prefix = getUniqueNsAttribute(docAttributes); + Attr nsAttr = document.createAttribute(prefix); + nsAttr.setValue(ns); + document.getChildNodes().item(0).getAttributes().setNamedItem(nsAttr); + } + + // set the prefix on the node, by removing the xmlns: start + prefix = prefix.substring(6); + node.setPrefix(prefix); + } + } + + /** + * Looks for an existing prefix for a a given namespace. + * The prefix must start with "xmlns:". The whole prefix is returned. + * @param attributes the list of attributes to look through + * @param ns the namespace to find. + * @return the found prefix or null if none is found. + */ + private String getPrefixForNs(NamedNodeMap attributes, String ns) { + if (attributes != null) { + for (int i = 0, n = attributes.getLength(); i < n; i++) { + Attr attribute = (Attr) attributes.item(i); + if (ns.equals(attribute.getValue()) && ns.startsWith(SdkConstants.XMLNS_PREFIX)) { + return attribute.getName(); + } + } + } + + return null; + } + + private String getUniqueNsAttribute(NamedNodeMap attributes) { + if (attributes == null) { + return "xmlns:ns1"; + } + + int i = 2; + while (true) { + String name = String.format("xmlns:ns%d", i++); + if (attributes.getNamedItem(name) == null) { + return name; + } + } + } + + private void readFolder(File rootFolder) throws DuplicateResourceException { + File[] folders = rootFolder.listFiles(); + if (folders != null) { + for (File folder : folders) { + if (folder.isDirectory()) { + parseFolder(folder); + } + } + } + } + + private void parseFolder(File folder) throws DuplicateResourceException { + + // get the type. + String folderName = folder.getName(); + int pos = folderName.indexOf(ResourceConstants.RES_QUALIFIER_SEP); + ResourceFolderType folderType; + String qualifiers = null; + if (pos != -1) { + folderType = ResourceFolderType.getTypeByName(folderName.substring(0, pos)); + qualifiers = folderName.substring(pos + 1); + } else { + folderType = ResourceFolderType.getTypeByName(folderName); + } + + boolean singleResourceFile = folderType != ResourceFolderType.VALUES; + List<ResourceType> types = FolderTypeRelationship.getRelatedResourceTypes(folderType); + + // get the files + File[] files = folder.listFiles(); + if (files != null && files.length > 0) { + for (File file : files) { + if (singleResourceFile) { + // get the resource name based on the filename + String name = file.getName(); + pos = name.indexOf('.'); + name = name.substring(0, pos); + + Resource item = new Resource(name, types.get(0), null); + ResourceFile resourceFile = new ResourceFile(file, item, qualifiers); + + checkItem(item); + + mItems.put(item.getKey(), item); + } else { + ValueResourceParser parser = new ValueResourceParser(file); + try { + List<Resource> items = parser.parseFile(); + + ResourceFile resourceFile = new ResourceFile(file, items, qualifiers); + + for (Resource item : items) { + checkItem(item); + mItems.put(item.getKey(), item); + } + } catch (FileNotFoundException e) { + // wont happen as we know the file exists. + } + } + } + } + } + + private void checkItem(Resource item) throws DuplicateResourceException { + Resource otherItem = mItems.get(item.getKey()); + if (otherItem != null) { + throw new DuplicateResourceException(item, otherItem); + } + } +} diff --git a/builder/src/main/java/com/android/builder/resources/ValueResourceParser.java b/builder/src/main/java/com/android/builder/resources/ValueResourceParser.java new file mode 100644 index 0000000..2ff61df --- /dev/null +++ b/builder/src/main/java/com/android/builder/resources/ValueResourceParser.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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.android.builder.resources; + +import com.android.resources.ResourceType; +import com.google.common.collect.Lists; +import com.sun.xml.internal.rngom.ast.builder.BuildException; +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.List; + +import static com.android.SdkConstants.ATTR_NAME; +import static com.android.SdkConstants.ATTR_TYPE; +import static com.android.SdkConstants.TAG_ITEM; + +/** + */ +class ValueResourceParser { + + private final File mFile; + + ValueResourceParser(File file) { + mFile = file; + } + + List<Resource> parseFile() throws FileNotFoundException { + Document document = parseDocument(); + + NodeList nodes = document.getChildNodes(); + + // get the root node + Node rootNode = nodes.item(0); + nodes = rootNode.getChildNodes(); + + List<Resource> resources = Lists.newArrayListWithExpectedSize(nodes.getLength()); + + for (int i = 0, n = nodes.getLength(); i < n; i++) { + Node node = nodes.item(i); + + if (node.getNodeType() != Node.ELEMENT_NODE) { + continue; + } + + ResourceType type = getType(node); + String name = getName(node); + + if (type != null && name != null) { + Resource r = new Resource(name, type, node); + resources.add(r); + } + } + + return resources; + } + + private ResourceType getType(Node node) { + String nodeName = node.getLocalName(); + String typeString = null; + + if (TAG_ITEM.equals(nodeName)) { + Attr attribute = (Attr) node.getAttributes().getNamedItemNS(null, ATTR_TYPE); + if (attribute != null) { + typeString = attribute.getValue(); + } + } else { + // the type is the name of the node. + typeString = nodeName; + } + + if (typeString != null) { + return ResourceType.getEnum(typeString); + } + + return null; + } + + private String getName(Node node) { + Attr attribute = (Attr) node.getAttributes().getNamedItemNS(null, ATTR_NAME); + + if (attribute != null) { + return attribute.getValue(); + } + + return null; + } + + private Document parseDocument() throws FileNotFoundException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + BufferedInputStream stream = new BufferedInputStream(new FileInputStream(mFile)); + InputSource is = new InputSource(stream); + factory.setNamespaceAware(true); + factory.setValidating(false); + try { + DocumentBuilder builder = factory.newDocumentBuilder(); + return builder.parse(is); + } catch (Exception e) { + throw new BuildException(e); + } finally { + try { + stream.close(); + } catch (IOException e) { + } + } + } +} diff --git a/builder/src/test/java/com/android/builder/resources/BaseTestCase.java b/builder/src/test/java/com/android/builder/resources/BaseTestCase.java new file mode 100644 index 0000000..ac9f788 --- /dev/null +++ b/builder/src/test/java/com/android/builder/resources/BaseTestCase.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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.android.builder.resources; + +import junit.framework.TestCase; + +import java.io.File; +import java.util.Map; + +public abstract class BaseTestCase extends TestCase { + + protected static File getRoot(String name) { + File root = new File("src/test/resources/testData/" + name); + assertTrue("Test folder '" + name + "' does not exist!", + root.isDirectory()); + + return root; + } + + protected void verifyResources(ResourceSet resourceSet, String... resourceKeys) { + Map<String, Resource> map = resourceSet.getResourceMap(); + + for (String res : resourceKeys) { + assertNotNull("resource '" + res + "' is missing!", map.get(res)); + } + } +} diff --git a/builder/src/test/java/com/android/builder/resources/ResourceMergerTest.java b/builder/src/test/java/com/android/builder/resources/ResourceMergerTest.java new file mode 100644 index 0000000..6b856e2 --- /dev/null +++ b/builder/src/test/java/com/android/builder/resources/ResourceMergerTest.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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.android.builder.resources; + +import com.google.common.io.Files; + +import java.io.File; +import java.io.IOException; +import java.util.Map; + +public class ResourceMergerTest extends BaseTestCase { + + private static ResourceSet sMergedSet = null; + + public void testMergeByCount() throws Exception { + ResourceSet mergedSet = getMergedSet(); + + assertEquals(25, mergedSet.getSize()); + } + + public void testMergedResourcesByName() throws Exception { + ResourceSet mergedSet = getMergedSet(); + + verifyResources(mergedSet, + "drawable/icon", + "drawable-ldpi/icon", + "drawable/icon2", + "raw/foo", + "layout/main", + "layout/layout_ref", + "layout/alias_replaced_by_file", + "layout/file_replaced_by_alias", + "drawable/color_drawable", + "drawable/drawable_ref", + "color/color", + "string/basic_string", + "string/xliff_string", + "string/styled_string", + "style/style", + "array/string_array", + "attr/dimen_attr", + "attr/string_attr", + "attr/enum_attr", + "attr/flag_attr", + "declare-styleable/declare_styleable", + "dimen/dimen", + "id/item_id", + "integer/integer" + ); + } + + public void testReplacedLayout() throws Exception { + ResourceSet mergedSet = getMergedSet(); + + Resource mainLayout = mergedSet.getResourceMap().get("layout/main"); + ResourceFile sourceFile = mainLayout.getSource(); + assertTrue(sourceFile.getFile().getAbsolutePath().endsWith("overlay/layout/main.xml")); + } + + public void testReplacedAlias() throws Exception { + ResourceSet mergedSet = getMergedSet(); + + Resource layout = mergedSet.getResourceMap().get("layout/alias_replaced_by_file"); + // since it's replaced by a file, there's no node. + assertNull(layout.getValue()); + } + + public void testReplacedFile() throws Exception { + ResourceSet mergedSet = getMergedSet(); + + Resource layout = mergedSet.getResourceMap().get("layout/file_replaced_by_alias"); + // since it's replaced by a file, there's no node. + assertNotNull(layout.getValue()); + } + + public void testMergeWrite() throws Exception { + ResourceSet mergedSet = getMergedSet(); + + File folder = getWrittenSet(); + + ResourceSet writtenSet = new ResourceSet(); + writtenSet.addSource(folder); + + // compare the two sets. + assertEquals(mergedSet.getSize(), writtenSet.getSize()); + + // compare the resources are all the same + Map<String, Resource> map = writtenSet.getResourceMap(); + for (Resource item : mergedSet.getResources()) { + assertNotNull(map.get(item.getKey())); + } + } + + private static ResourceSet getMergedSet() throws DuplicateResourceException { + if (sMergedSet == null) { + File root = getRoot("baseMerge"); + + ResourceSet res = ResourceSetTest.getBaseResourceSet(); + + ResourceSet overlay = new ResourceSet(); + overlay.addSource(new File(root, "overlay")); + + ResourceMerger merger = new ResourceMerger(); + merger.addResourceSet(res); + merger.addResourceSet(overlay); + + sMergedSet = merger.getMergedSet(); + } + + return sMergedSet; + } + + private static File getWrittenSet() throws DuplicateResourceException, IOException { + ResourceSet mergedSet = getMergedSet(); + + File folder = Files.createTempDir(); + + mergedSet.writeTo(folder); + + return folder; + } +} diff --git a/builder/src/test/java/com/android/builder/resources/ResourceSetTest.java b/builder/src/test/java/com/android/builder/resources/ResourceSetTest.java new file mode 100644 index 0000000..bcc7b88 --- /dev/null +++ b/builder/src/test/java/com/android/builder/resources/ResourceSetTest.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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.android.builder.resources; + +import java.io.File; + +public class ResourceSetTest extends BaseTestCase { + + private static ResourceSet sBaseResourceSet = null; + + public void testBaseResourceSetByCount() throws Exception { + ResourceSet resourceSet = getBaseResourceSet(); + assertEquals(23, resourceSet.getSize()); + } + + public void testBaseResourceSetByName() throws Exception { + ResourceSet resourceSet = getBaseResourceSet(); + + verifyResources(resourceSet, + "drawable/icon", + "drawable/patch", + "raw/foo", + "layout/main", + "layout/layout_ref", + "layout/alias_replaced_by_file", + "layout/file_replaced_by_alias", + "drawable/color_drawable", + "drawable/drawable_ref", + "color/color", + "string/basic_string", + "string/xliff_string", + "string/styled_string", + "style/style", + "array/string_array", + "attr/dimen_attr", + "attr/string_attr", + "attr/enum_attr", + "attr/flag_attr", + "declare-styleable/declare_styleable", + "dimen/dimen", + "id/item_id", + "integer/integer" + ); + } + + public void testDupResourceSet() throws Exception { + File root = getRoot("dupResourceSet"); + + ResourceSet set = new ResourceSet(); + set.addSource(new File(root, "res1")); + boolean gotException = false; + try { + set.addSource(new File(root, "res2")); + } catch (DuplicateResourceException e) { + gotException = true; + } + + assertTrue(gotException); + } + + static ResourceSet getBaseResourceSet() throws DuplicateResourceException { + if (sBaseResourceSet == null) { + File root = getRoot("baseResourceSet"); + + sBaseResourceSet = new ResourceSet(); + sBaseResourceSet.addSource(root); + } + + return sBaseResourceSet; + } +} diff --git a/builder/src/test/java/com/android/builder/resources/ValueResourceParserTest.java b/builder/src/test/java/com/android/builder/resources/ValueResourceParserTest.java new file mode 100644 index 0000000..dbd31a5 --- /dev/null +++ b/builder/src/test/java/com/android/builder/resources/ValueResourceParserTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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.android.builder.resources; + +import com.google.common.collect.Maps; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.List; +import java.util.Map; + +/** + */ +public class ValueResourceParserTest extends BaseTestCase { + + private static List<Resource> sResources = null; + + public void testParsedResourcesByCount() throws Exception { + List<Resource> resources = getParsedResources(); + + assertEquals(18, resources.size()); + } + + public void testParsedResourcesByName() throws Exception { + List<Resource> resources = getParsedResources(); + + // convert to a map + Map<String, Resource> map = Maps.newHashMapWithExpectedSize(resources.size()); + for (Resource r : resources) { + map.put(r.getKey(), r); + } + + String[] resourceNames = new String[] { + "drawable/color_drawable", + "drawable/drawable_ref", + "color/color", + "string/basic_string", + "string/xliff_string", + "string/styled_string", + "style/style", + "array/string_array", + "attr/dimen_attr", + "attr/string_attr", + "attr/enum_attr", + "attr/flag_attr", + "declare-styleable/declare_styleable", + "dimen/dimen", + "id/item_id", + "integer/integer", + "layout/layout_ref" + }; + + for (String name : resourceNames) { + assertNotNull(name, map.get(name)); + } + } + + private static List<Resource> getParsedResources() throws FileNotFoundException { + if (sResources == null) { + File root = getRoot("baseResourceSet"); + File values = new File(root, "values"); + File valuesXml = new File(values, "values.xml"); + + ValueResourceParser parser = new ValueResourceParser(valuesXml); + sResources = parser.parseFile(); + } + + return sResources; + } +} diff --git a/builder/src/test/resources/testData/baseMerge/overlay/drawable-ldpi/icon.png b/builder/src/test/resources/testData/baseMerge/overlay/drawable-ldpi/icon.png Binary files differnew file mode 100644 index 0000000..a07c69f --- /dev/null +++ b/builder/src/test/resources/testData/baseMerge/overlay/drawable-ldpi/icon.png diff --git a/builder/src/test/resources/testData/baseMerge/overlay/drawable/icon2.png b/builder/src/test/resources/testData/baseMerge/overlay/drawable/icon2.png Binary files differnew file mode 100644 index 0000000..a07c69f --- /dev/null +++ b/builder/src/test/resources/testData/baseMerge/overlay/drawable/icon2.png diff --git a/builder/src/test/resources/testData/baseMerge/overlay/layout/alias_replaced_by_file.xml b/builder/src/test/resources/testData/baseMerge/overlay/layout/alias_replaced_by_file.xml new file mode 100644 index 0000000..b199751 --- /dev/null +++ b/builder/src/test/resources/testData/baseMerge/overlay/layout/alias_replaced_by_file.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + > +<TextView + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:text="Test App - Basic" + android:id="@+id/text" + /> +</LinearLayout> + diff --git a/builder/src/test/resources/testData/baseMerge/overlay/layout/main.xml b/builder/src/test/resources/testData/baseMerge/overlay/layout/main.xml new file mode 100644 index 0000000..b199751 --- /dev/null +++ b/builder/src/test/resources/testData/baseMerge/overlay/layout/main.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + > +<TextView + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:text="Test App - Basic" + android:id="@+id/text" + /> +</LinearLayout> + diff --git a/builder/src/test/resources/testData/baseMerge/overlay/values/values.xml b/builder/src/test/resources/testData/baseMerge/overlay/values/values.xml new file mode 100644 index 0000000..a3c90cc --- /dev/null +++ b/builder/src/test/resources/testData/baseMerge/overlay/values/values.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <color name="color">#FFFFFFFF</color> + <string name="basic_string">overlay_string</string> + <item type="layout" name="file_replaced_by_alias">@layout/ref</item> +</resources> diff --git a/builder/src/test/resources/testData/baseResourceSet/drawable/icon.png b/builder/src/test/resources/testData/baseResourceSet/drawable/icon.png Binary files differnew file mode 100644 index 0000000..a07c69f --- /dev/null +++ b/builder/src/test/resources/testData/baseResourceSet/drawable/icon.png diff --git a/builder/src/test/resources/testData/baseResourceSet/drawable/patch.9.png b/builder/src/test/resources/testData/baseResourceSet/drawable/patch.9.png Binary files differnew file mode 100644 index 0000000..a07c69f --- /dev/null +++ b/builder/src/test/resources/testData/baseResourceSet/drawable/patch.9.png diff --git a/builder/src/test/resources/testData/baseResourceSet/layout/file_replaced_by_alias.xml b/builder/src/test/resources/testData/baseResourceSet/layout/file_replaced_by_alias.xml new file mode 100644 index 0000000..b199751 --- /dev/null +++ b/builder/src/test/resources/testData/baseResourceSet/layout/file_replaced_by_alias.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + > +<TextView + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:text="Test App - Basic" + android:id="@+id/text" + /> +</LinearLayout> + diff --git a/builder/src/test/resources/testData/baseResourceSet/layout/main.xml b/builder/src/test/resources/testData/baseResourceSet/layout/main.xml new file mode 100644 index 0000000..b199751 --- /dev/null +++ b/builder/src/test/resources/testData/baseResourceSet/layout/main.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + > +<TextView + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:text="Test App - Basic" + android:id="@+id/text" + /> +</LinearLayout> + diff --git a/builder/src/test/resources/testData/baseResourceSet/raw/foo.dat b/builder/src/test/resources/testData/baseResourceSet/raw/foo.dat new file mode 100644 index 0000000..def08db --- /dev/null +++ b/builder/src/test/resources/testData/baseResourceSet/raw/foo.dat @@ -0,0 +1 @@ +Foo!
\ No newline at end of file diff --git a/builder/src/test/resources/testData/baseResourceSet/values/values.xml b/builder/src/test/resources/testData/baseResourceSet/values/values.xml new file mode 100644 index 0000000..ee38060 --- /dev/null +++ b/builder/src/test/resources/testData/baseResourceSet/values/values.xml @@ -0,0 +1,76 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + + <drawable name="color_drawable">#ffffffff</drawable> + <drawable name="drawable_ref">@drawable/stat_notify_sync_anim0</drawable> + <color name="color">#00000000</color> + + <string name="basic_string">basic_string</string> + <string name="xliff_string"><xliff:g id="number" example="123">%1$s</xliff:g><xliff:g id="unit" example="KB">%2$s</xliff:g></string> + <string name="styled_string">Forgot your username or password\?\nVisit <b>google.com/accounts/recovery</b>.</string> + + <style name="style"> + <item name="android:singleLine">true</item> + <item name="android:textAppearance">@style/TextAppearance.WindowTitle</item> + <item name="android:shadowColor">#BB000000</item> + <item name="android:shadowRadius">2.75</item> + </style> + + <!-- list of 3- or 4-letter mnemonics for a 10-key numeric keypad --> + <string-array translatable="false" name="string_array"> + <item></item><!-- 0 --> + <item></item><!-- 1 --> + <item>ABC</item><!-- 2 --> + <item>DEF</item><!-- 3 --> + <item>GHI</item><!-- 4 --> + <item>JKL</item><!-- 5 --> + <item>MNO</item><!-- 6 --> + <item>PQRS</item><!-- 7 --> + <item>TUV</item><!-- 8 --> + <item>WXYZ</item><!-- 9 --> + </string-array> + + <attr name="dimen_attr" format="dimension" /> + + <!-- Default font family. --> + <attr name="string_attr" format="string" /> + + <!-- Default text typeface. --> + <attr name="enum_attr"> + <enum name="normal" value="0" /> + <enum name="sans" value="1" /> + <enum name="serif" value="2" /> + <enum name="monospace" value="3" /> + </attr> + + <!-- Default text typeface style. --> + <attr name="flag_attr"> + <flag name="normal" value="0" /> + <flag name="bold" value="1" /> + <flag name="italic" value="2" /> + </attr> + + <!-- These are the standard attributes that make up a complete theme. --> + <declare-styleable name="declare_styleable"> + <!-- ============== --> + <!-- Generic styles --> + <!-- ============== --> + <eat-comment /> + + <!-- Default color of foreground imagery. --> + <attr name="blah" format="color" /> + <!-- Default color of foreground imagery on an inverted background. --> + <attr name="android:colorForegroundInverse" /> + </declare-styleable> + + <!-- The width that is used when creating thumbnails of applications. --> + <dimen name="dimen">164dp</dimen> + + <item type="id" name="item_id" /> + + <integer name="integer">75</integer> + + <item type="layout" name="layout_ref">@layout/ref</item> + <item type="layout" name="alias_replaced_by_file">@layout/ref</item> + +</resources> diff --git a/builder/src/test/resources/testData/dupResourceSet/res1/drawable/icon.png b/builder/src/test/resources/testData/dupResourceSet/res1/drawable/icon.png Binary files differnew file mode 100644 index 0000000..a07c69f --- /dev/null +++ b/builder/src/test/resources/testData/dupResourceSet/res1/drawable/icon.png diff --git a/builder/src/test/resources/testData/dupResourceSet/res2/drawable/icon.png b/builder/src/test/resources/testData/dupResourceSet/res2/drawable/icon.png Binary files differnew file mode 100644 index 0000000..a07c69f --- /dev/null +++ b/builder/src/test/resources/testData/dupResourceSet/res2/drawable/icon.png |