Incremental resource merger support.

First a ChangeManager (and associated classes) that detects changed
files across build runs. This is to be replaced by Gradle once this
information is given to the tasks.

Then, a rewrite of ResourceMerger and ResourceSet to allow saving a
post run state, and reloading it on the next run, to allow for
incremental updates of the merged resource folder by only updating
the resources that changed, were removed or were added.

Finally a change to the BaseTask to support incremental tasks and
provide some default support that actual tasks can use or not.
The BaseTask will do some first checks to figure out if incremental
runs are supported by the class or by the current state, and
call one of two methods to do either full run or incremental run.

The MergerResourceTask is changed to use this, and its incremental
method does a bit more checking and reverts to full run when it
cannot do an incremental run.

Finally a bunch of tests to ensure this works fine.

Change-Id: I3808e2d57aa45882eaf1030e16b092ecd58d9729
diff --git a/build.gradle b/build.gradle
index b27411f..5a6b743 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,4 +1,4 @@
-allprojects {
+subprojects {
     apply plugin: 'idea'
 
     repositories {
@@ -12,6 +12,23 @@
     group = 'com.android.tools.build'
 }
 
-task wrapper(type: Wrapper) {
-    gradleVersion = '1.3'
+// delay evaluation of this project before all subprojects have been evaluated.
+subprojects.each { subproject -> evaluationDependsOn(subproject.name) }
+
+def testTasks = subprojects.collect { it.tasks.withType(Test) }.flatten()
+
+task aggregateResults(type: Copy) {
+    from { testTasks*.testResultsDir }
+    into { file("build/results") }
 }
+aggregateResults.dependsOn testTasks
+
+task makeAggregateReport(dependsOn: aggregateResults) {
+    def report = new org.gradle.api.internal.tasks.testing.junit.report.DefaultTestReport(testReportDir: file("build/reports/tests"), testResultsDir: file("build/results"))
+    report.generateReport()
+}
+
+task check {
+    //nothing
+}
+check.dependsOn makeAggregateReport
diff --git a/builder/prebuilts/common.jar b/builder/prebuilts/common.jar
index 0398dbc..2bb2473 100644
--- a/builder/prebuilts/common.jar
+++ b/builder/prebuilts/common.jar
Binary files differ
diff --git a/builder/prebuilts/sdk_common.jar b/builder/prebuilts/sdk_common.jar
new file mode 100644
index 0000000..357543f
--- /dev/null
+++ b/builder/prebuilts/sdk_common.jar
Binary files differ
diff --git a/builder/src/main/java/com/android/builder/AndroidBuilder.java b/builder/src/main/java/com/android/builder/AndroidBuilder.java
index 55bacb8..b0ce8f3 100644
--- a/builder/src/main/java/com/android/builder/AndroidBuilder.java
+++ b/builder/src/main/java/com/android/builder/AndroidBuilder.java
@@ -35,9 +35,6 @@
 import com.android.builder.packaging.DuplicateFileException;
 import com.android.builder.packaging.PackagerException;
 import com.android.builder.packaging.SealedPackageException;
-import com.android.builder.resources.DuplicateResourceException;
-import com.android.builder.resources.ResourceMerger;
-import com.android.builder.resources.ResourceSet;
 import com.android.manifmerger.ManifestMerger;
 import com.android.manifmerger.MergerLog;
 import com.android.prefs.AndroidLocation.AndroidLocationException;
@@ -195,51 +192,6 @@
     }
 
     /**
-     * Merge resources together so that they can be fed to aapt.
-     *
-     * This also pre-processes the images (crunches the pngs and processes the 9 patch files)
-     * This is incremental.
-     *
-     * @param resOutputDir where the processed resources are stored.
-     * @param inputFolders the input resource folders
-     * @throws DuplicateResourceException
-     * @throws IOException
-     */
-    public void mergeResources(@NonNull String resOutputDir, @Nullable List<List<File>> inputFolders)
-            throws DuplicateResourceException, IOException {
-        checkState(mTarget != null, "Target not set.");
-        checkNotNull(resOutputDir, "resOutputDir cannot be null.");
-
-        if (inputFolders == null || inputFolders.isEmpty()) {
-            return;
-        }
-
-        ResourceMerger merger = new ResourceMerger();
-
-        boolean runMerger = false;
-
-        for (List<File> setFolders : inputFolders) {
-            // create a set and add all the folders from the list to it.
-            ResourceSet set = new ResourceSet();
-            for (File folder : setFolders) {
-                if (folder.isDirectory()) {
-                    set.addSource(folder);
-                }
-            }
-
-            if (!set.isEmpty()) {
-                merger.addResourceSet(set);
-                runMerger = true;
-            }
-        }
-
-        if (runMerger) {
-            ResourceSet mergedSet = merger.getMergedSet();
-            mergedSet.writeTo(new File(resOutputDir));
-        }
-    }
-
-    /**
      * Merges all the manifests into a single manifest
      *
      * @param mainManifest The main manifest of the application.
diff --git a/builder/src/main/java/com/android/builder/VariantConfiguration.java b/builder/src/main/java/com/android/builder/VariantConfiguration.java
index 0fbfbf9..f20c13c 100644
--- a/builder/src/main/java/com/android/builder/VariantConfiguration.java
+++ b/builder/src/main/java/com/android/builder/VariantConfiguration.java
@@ -19,12 +19,12 @@
 import com.android.annotations.NonNull;
 import com.android.annotations.Nullable;
 import com.android.annotations.VisibleForTesting;
+import com.android.builder.resources.ResourceSet;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 
 import java.io.File;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 
@@ -151,7 +151,7 @@
      * @return the config object
      */
     public VariantConfiguration addProductFlavor(@NonNull ProductFlavor productFlavor,
-                                 @NonNull SourceProvider sourceProvider) {
+                                                 @NonNull SourceProvider sourceProvider) {
         mFlavorConfigs.add(productFlavor);
         mFlavorSourceProviders.add(sourceProvider);
         mMergedFlavor = productFlavor.mergeOver(mMergedFlavor);
@@ -506,53 +506,53 @@
     }
 
     /**
-     * Returns the dynamic list of resource sets based on the configuration, its dependencies,
+     * Returns the dynamic list of {@link ResourceSet} based on the configuration, its dependencies,
      * as well as tested config if applicable (test of a library).
      *
-     * The list is a list of folders. Each list of folder represents a
-     * {@link com.android.builder.resources.ResourceSet}.
-     *
      * The list is ordered in ascending order of importance, meaning the first set is meant to be
      * overridden by the 2nd one and so on. This is meant to facilitate usage of the list in a
      * {@link com.android.builder.resources.ResourceMerger}.
      *
-     * @return a list of list of resource folders.
+     * @return a list ResourceSet.
      */
-    @NonNull public List<List<File>> getResourceSets() {
-        List<List<File>> inputs = Lists.newArrayList();
+    @NonNull public List<ResourceSet> getResourceSets() {
+        List<ResourceSet> resourceSets = Lists.newArrayList();
 
         // the list of dependency must be reversed to use the right overlay order.
         for (int n = mFlatLibraries.size() - 1 ; n >= 0 ; n--) {
             AndroidDependency dependency = mFlatLibraries.get(n);
             File resFolder = dependency.getResFolder();
             if (resFolder != null) {
-                inputs.add(Collections.singletonList(resFolder));
+                ResourceSet resourceSet = new ResourceSet(dependency.getFolder().getName());
+                resourceSet.addSource(resFolder);
+                resourceSets.add(resourceSet);
             }
         }
 
         Set<File> mainResDirs = mDefaultSourceProvider.getResourcesDirectories();
-        List<File> mainSet = Lists.newArrayList();
-        mainSet.addAll(mainResDirs);
-        inputs.add(mainSet);
+
+        ResourceSet resourceSet = new ResourceSet("main");
+        resourceSet.addSources(mainResDirs);
+        resourceSets.add(resourceSet);
 
         // the list of flavor must be reversed to use the right overlay order.
         for (int n = mFlavorSourceProviders.size() - 1; n >= 0 ; n--) {
             SourceProvider sourceProvider = mFlavorSourceProviders.get(n);
 
             Set<File> flavorResDirs = sourceProvider.getResourcesDirectories();
-            List<File> flavorSet = Lists.newArrayList();
-            flavorSet.addAll(flavorResDirs);
-            inputs.add(flavorSet);
+            resourceSet = new ResourceSet(mFlavorConfigs.get(n).getName());
+            resourceSet.addSources(flavorResDirs);
+            resourceSets.add(resourceSet);
         }
 
         if (mBuildTypeSourceProvider != null) {
             Set<File> typeResDirs = mBuildTypeSourceProvider.getResourcesDirectories();
-            List<File> typeSet = Lists.newArrayList();
-            typeSet.addAll(typeResDirs);
-            inputs.add(typeSet);
+            resourceSet = new ResourceSet(mBuildType.getName());
+            resourceSet.addSources(typeResDirs);
+            resourceSets.add(resourceSet);
         }
 
-        return inputs;
+        return resourceSets;
     }
 
     public List<File> getAidlSourceList() {
diff --git a/builder/src/main/java/com/android/builder/internal/CommandLineRunner.java b/builder/src/main/java/com/android/builder/internal/CommandLineRunner.java
index e074267..81af9d9 100644
--- a/builder/src/main/java/com/android/builder/internal/CommandLineRunner.java
+++ b/builder/src/main/java/com/android/builder/internal/CommandLineRunner.java
@@ -44,7 +44,7 @@
 
         // get the output and return code from the process
         if (grabProcessOutput(process) != 0) {
-            throw new RuntimeException();
+            throw new RuntimeException(String.format("running %s failed. see output", command[0]));
         }
     }
 
diff --git a/builder/src/main/java/com/android/builder/internal/compiler/AidlProcessor.java b/builder/src/main/java/com/android/builder/internal/compiler/AidlProcessor.java
index 5d7de56..d7401de 100644
--- a/builder/src/main/java/com/android/builder/internal/compiler/AidlProcessor.java
+++ b/builder/src/main/java/com/android/builder/internal/compiler/AidlProcessor.java
@@ -63,14 +63,14 @@
         command.add("-p" + mFrameworkLocation);
         command.add("-o" + sourceOutputDir.getAbsolutePath());
         // add all the source folders as import in case an aidl file in a source folder
-        // imports a parcelable from another source folder.
+        // imports a parceleable from another source folder.
         for (File sourceFolder : sourceFolders) {
             if (sourceFolder.isDirectory()) {
                 command.add("-I" + sourceFolder.getAbsolutePath());
             }
         }
 
-        // add all the library aidl folders to access parcelables that are in libraries
+        // add all the library aidl folders to access parceleables that are in libraries
         for (File f : mImportFolders) {
             command.add("-I" + f.getAbsolutePath());
         }
diff --git a/builder/src/main/java/com/android/builder/internal/incremental/ChangeManager.java b/builder/src/main/java/com/android/builder/internal/incremental/ChangeManager.java
new file mode 100644
index 0000000..aca723e
--- /dev/null
+++ b/builder/src/main/java/com/android/builder/internal/incremental/ChangeManager.java
@@ -0,0 +1,126 @@
+/*
+ * 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.internal.incremental;
+
+import com.android.annotations.NonNull;
+import com.android.builder.resources.FileStatus;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * Class handling changes in input and output.
+ *
+ * The class stores the state of inputs/outputs after a task is run, and on subsequent runs can
+ * compare this to the current state of detect exact file changes in the inputs or outputs.
+ *
+ * Gradle already does this to figure out if a task needs to be run, but does not offer this
+ * information to the task.
+ * This should become available in the Gradle plugin in the future, but in the meantime this
+ * provides the same information, allowing us to build truly incremental tasks.
+ *
+ */
+public class ChangeManager {
+
+    private static final String FN_INPUTS_DATA = "inputs.data";
+    private static final String FN_OUTPUTS_DATA = "outputs.data";
+
+    private FileManager mInputs = new FileManager();
+    private FileManager mOutputs = new FileManager();
+
+    public ChangeManager() {
+    }
+
+    /**
+     * Loads the known state.
+     *
+     * @param incrementalFolder the folder in which to store the incremental data
+     * @return false if the loading failed.
+     */
+    public boolean load(File incrementalFolder) {
+        File inputs = new File(incrementalFolder, FN_INPUTS_DATA);
+        File outputs = new File(incrementalFolder, FN_OUTPUTS_DATA);
+        return inputs.exists() && outputs.exists() &&
+                mInputs.load(inputs) && mOutputs.load(outputs);
+    }
+
+    /**
+     * Writes the incremental data to a given folder.
+     * @param incrementalFolder the name of the folder to write to.
+     */
+    public void write(File incrementalFolder) {
+        incrementalFolder.mkdirs();
+
+        mInputs.write(new File(incrementalFolder, FN_INPUTS_DATA));
+        mOutputs.write(new File(incrementalFolder, FN_OUTPUTS_DATA));
+    }
+
+    /**
+     * Delete the incremental data from the given folder.
+     * @param incrementalFolder the folder to delete the incremental data from.
+     */
+    public static void delete(File incrementalFolder) {
+        File file = new File(incrementalFolder, FN_INPUTS_DATA);
+        file.delete();
+        file = new File(incrementalFolder, FN_OUTPUTS_DATA);
+        file.delete();
+    }
+
+    /**
+     * Add an input file or folder.
+     * @param file the file.
+     */
+    public void addInput(File file) {
+        mInputs.addFile(file);
+    }
+
+    /**
+     * Adds a new output file or folder
+     * @param file the file.
+     */
+    public void addOutput(File file) {
+        mOutputs.addFile(file);
+    }
+
+    /**
+     * Get the list of changed inputs. Empty list means no input changes.
+     *
+     * @return a map of (File, FileStatus) for all changed input.
+     */
+    @NonNull
+    public Map<File, FileStatus> getChangedInputs() {
+        return mInputs.getChangedFiles();
+    }
+
+    /**
+     * Returns a list of changed output. Empty list means no output changes.
+     *
+     * @return a map of (file, status) for all changed output files.
+     */
+    @NonNull
+    public Map<File, FileStatus> getChangedOutputs() {
+        return mOutputs.getChangedFiles();
+    }
+
+    /**
+     * Update the outputs before writing the file states
+     */
+    public void updateOutputs(Collection<File> outputs) {
+        mOutputs.update(outputs);
+    }
+}
diff --git a/builder/src/main/java/com/android/builder/internal/incremental/FileEntity.java b/builder/src/main/java/com/android/builder/internal/incremental/FileEntity.java
new file mode 100644
index 0000000..17eae4c
--- /dev/null
+++ b/builder/src/main/java/com/android/builder/internal/incremental/FileEntity.java
@@ -0,0 +1,230 @@
+/*
+ * 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.internal.incremental;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.util.Formatter;
+
+/**
+ * A {@link File} and its associated data needed to figure out if a file changed or not.
+ */
+class FileEntity {
+
+    private static final byte[] sBuffer = new byte[4096];
+
+    private final File file;
+    private final long lastModified;
+    private long length;
+    private String sha1;
+
+    /**
+     * Exception to indicate a failure to check a jar file's content.
+     */
+    private static final class Sha1Exception extends Exception {
+        private static final long serialVersionUID = 1L;
+        private final File file;
+
+        public Sha1Exception(File jarFile, Throwable cause) {
+            super(cause);
+            file = jarFile;
+        }
+
+        public File getJarFile() {
+            return file;
+        }
+    }
+
+    /**
+     * Creates an entity from cached data.
+     *
+     * @param file the file
+     * @param lastModified when it was last modified
+     * @param length its length
+     * @param sha1 its sha1
+     */
+    FileEntity(File file, long lastModified, long length, String sha1) {
+        this.file = file;
+        this.lastModified = lastModified;
+        this.length = length;
+        this.sha1 = sha1;
+    }
+
+    /**
+     * Creates an entity from a {@link File}.
+     *
+     * The sha1 is not computed yet, it'll be done on demand when {@link #getSha1()} is called.
+     *
+     * @param file the file.
+     */
+    FileEntity(File file) {
+        this.file = file;
+        lastModified = file.lastModified();
+        length = file.length();
+    }
+
+    /**
+     * Returns the file's last modified info.
+     * @return the file's last modified info.
+     */
+    long getLastModified() {
+        return lastModified;
+    }
+
+    /**
+     * Return the file length.
+     * @return the file length.
+     */
+    long getLength() {
+        return length;
+    }
+
+    /**
+     * Returns the file this entity represents.
+     * @return the file.
+     */
+    File getFile() {
+        return file;
+    }
+
+    /**
+     * Returns the file's sha1, computing it if necessary.
+     *
+     * @return the sha1 or null if it couldn't be computed.
+     */
+    String getSha1() {
+        try {
+            return computeAndReturnSha1();
+        } catch (Sha1Exception e) {
+            return null;
+        }
+    }
+
+    /**
+     * Checks whether the {@link File#lastModified()} matches the cached value. If not, length
+     * is updated and the sha1 is reset (but not recomputed, this is done on demand).
+     *
+     * @return return whether the file was changed.
+     */
+    private boolean checkValidity() {
+        if (lastModified != file.lastModified()) {
+            length = file.length();
+            sha1 = null;
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns whether the two entity are different files.
+     *
+     * This will compute the files' sha1 if they are not yet computed.
+     *
+     * @param fileEntity the file to compare to.
+     * @return true if the files are the same, false otherwise.
+     */
+    public boolean isDifferentThan(FileEntity fileEntity) {
+        assert fileEntity.file.equals(file);
+
+        // same date, same files.
+        if (lastModified == fileEntity.lastModified) {
+            return false;
+        }
+
+        try {
+            // different date doesn't necessarily mean different file.
+            // start with size, less computing intensive than sha1.
+            return length != fileEntity.length ||
+                    !computeAndReturnSha1().equals(fileEntity.computeAndReturnSha1());
+        } catch (Sha1Exception e) {
+            // if we can't compute the sha1, we consider the files different.
+            return true;
+        }
+    }
+
+    /**
+     * Returns the file's sha1, computing it if necessary.
+     *
+     * @return the sha1
+     * @throws Sha1Exception
+     */
+    private String computeAndReturnSha1() throws Sha1Exception {
+        if (sha1 == null) {
+            sha1 = getSha1(file);
+        }
+        return sha1;
+    }
+
+    /**
+     * Computes the sha1 of a file and returns it.
+     *
+     * @param f the file to compute the sha1 for.
+     * @return the sha1 value
+     * @throws Sha1Exception if the sha1 value cannot be computed.
+     */
+    static String getSha1(File f) throws Sha1Exception {
+        synchronized (sBuffer) {
+            FileInputStream fis = null;
+            try {
+                MessageDigest md = MessageDigest.getInstance("SHA-1");
+
+                fis = new FileInputStream(f);
+                while (true) {
+                    int length = fis.read(sBuffer);
+                    if (length > 0) {
+                        md.update(sBuffer, 0, length);
+                    } else {
+                        break;
+                    }
+                }
+
+                return byteArray2Hex(md.digest());
+
+            } catch (Exception e) {
+                throw new Sha1Exception(f, e);
+            } finally {
+                if (fis != null) {
+                    try {
+                        fis.close();
+                    } catch (IOException e) {
+                        // ignore
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Converts a byte array to an Hex string.
+     * @param hash the byte array to convert,
+     * @return the converted string.
+     */
+    private static String byteArray2Hex(final byte[] hash) {
+        Formatter formatter = new Formatter();
+        try {
+            for (byte b : hash) {
+                formatter.format("%02x", b);
+            }
+            return formatter.toString();
+        } finally {
+            formatter.close();
+        }
+    }
+}
diff --git a/builder/src/main/java/com/android/builder/internal/incremental/FileManager.java b/builder/src/main/java/com/android/builder/internal/incremental/FileManager.java
new file mode 100644
index 0000000..ff47c6a
--- /dev/null
+++ b/builder/src/main/java/com/android/builder/internal/incremental/FileManager.java
@@ -0,0 +1,231 @@
+/*
+ * 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.internal.incremental;
+
+import com.android.annotations.NonNull;
+import com.android.builder.resources.FileStatus;
+import com.google.common.collect.Maps;
+
+import java.io.*;
+import java.util.Collection;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Class handling changes in a set of files.
+ *
+ * The class can store the state of the files, and later reload it and compare it to the
+ * previous known state.
+ *
+ */
+class FileManager {
+
+    private static final Pattern READ_PATTERN = Pattern.compile("^(\\d+) (\\d+) ([0-9a-f]+) (.+)$");
+
+    private Map<File, FileEntity> mLoadedFiles = Maps.newHashMap();
+    private Map<File, FileEntity> mProcessedFiles = Maps.newHashMap();
+    private Map<File, FileStatus> mResults = Maps.newHashMap();
+
+    public FileManager() {
+    }
+
+    /**
+     * Loads the known state.
+     *
+     * @param stateFile the file to load the state from.
+     * @return false if the loading failed.
+     *
+     * @see #write(java.io.File)
+     */
+    public boolean load(File stateFile) {
+        if (!stateFile.exists()) {
+            return false;
+        }
+
+        BufferedReader reader = null;
+        try {
+            reader = new BufferedReader(new InputStreamReader(
+                    new FileInputStream(stateFile), "UTF-8"));
+
+            String line = null;
+            while ((line = reader.readLine()) != null) {
+                // skip comments
+                if (line.charAt(0) == '#') {
+                    continue;
+                }
+
+                // get the data with a regexp
+                Matcher m = READ_PATTERN.matcher(line);
+                if (m.matches()) {
+                    String path = m.group(4);
+                    File f = new File(path);
+
+                    FileEntity entity = new FileEntity(
+                            f,
+                            Long.parseLong(m.group(1)),
+                            Long.parseLong(m.group(2)),
+                            m.group(3));
+
+                    mLoadedFiles.put(f, entity);
+                }
+            }
+
+            return true;
+        } catch (FileNotFoundException ignored) {
+            // won't happen, we check up front.
+        } catch (UnsupportedEncodingException ignored) {
+            // shouldn't happen, but if it does, we just won't have a cache.
+        } catch (IOException ignored) {
+            // shouldn't happen, but if it does, we just won't have a cache.
+        } finally {
+            if (reader != null) {
+                try {
+                    reader.close();
+                } catch (IOException ignored) {
+                    // ignore
+                }
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Writes the state to a file
+     * @param stateFile the file to write the state to.
+     *
+     * @see #load(java.io.File)
+     */
+    public void write(File stateFile) {
+        OutputStreamWriter writer = null;
+        try {
+            // first make sure the folders exist!
+            File parentFolder = stateFile.getParentFile();
+            parentFolder.mkdirs();
+
+            // then write the file.
+            writer = new OutputStreamWriter(new FileOutputStream(stateFile), "UTF-8");
+
+            writer.write("# incremental data. DO NOT EDIT.\n");
+            writer.write("# format is <lastModified> <length> <SHA-1> <path>\n");
+            writer.write("# Encoding is UTF-8\n");
+
+            for (FileEntity entity : mProcessedFiles.values()) {
+                String sha1 = entity.getSha1();
+                if (sha1 == null) {
+                    sha1 = "0123456789012345678901234567890123456789"; // TODO: find a better way to detect missing sha1
+                }
+
+                writer.write(String.format("%d %d %s %s\n",
+                        entity.getLastModified(),
+                        entity.getLength(),
+                        sha1,
+                        entity.getFile().getAbsolutePath()));
+            }
+        } catch (IOException ignored) {
+        } finally {
+            if (writer != null) {
+                try {
+                    writer.close();
+                } catch (IOException ignored) {
+                }
+            }
+        }
+    }
+
+   /**
+     * Add an input file or folder.
+     * @param file the file.
+     */
+    public void addFile(File file) {
+        processFile(file);
+    }
+
+    /**
+     * Get the list of changed inputs. Empty list means no input changes.
+     *
+     * @return a map of (File, FileStatus) for all changed input.
+     */
+    @NonNull
+    public Map<File, FileStatus> getChangedFiles() {
+        // at this point, all the files that needed processing have been processed,
+        // but there may be removed files remaining in the loaded file map.
+        for (File f : mLoadedFiles.keySet()) {
+            mResults.put(f, FileStatus.REMOVED);
+        }
+
+        return mResults;
+    }
+
+    private void processFile(File file) {
+        if (file.isFile()) {
+            if (file.getName().startsWith(".")) {
+                return;
+            }
+
+            // get the FileEntity for the new(?) version.
+            FileEntity newFileEntity = new FileEntity(file);
+
+            // see if it existed before.
+            FileEntity fileEntity = mLoadedFiles.get(file);
+
+            if (fileEntity == null) {
+                // new file!
+                mResults.put(file, FileStatus.NEW);
+
+                // add it to the list of processed files
+                mProcessedFiles.put(file, newFileEntity);
+            } else {
+                // remove it from the loaded files.
+                mLoadedFiles.remove(file);
+
+                if (newFileEntity.isDifferentThan(fileEntity)) {
+                    mResults.put(file, FileStatus.CHANGED);
+
+                    // put the newFileEntity in the processed files.
+                    mProcessedFiles.put(file, newFileEntity);
+                } else {
+                    // just move the original entity so avoid recomputing the sha1.
+                    // FileEntity.isDifferentThan doesn't necessarily compute it.
+                   mProcessedFiles.put(file, fileEntity);
+                }
+            }
+        } else if (file.isDirectory()) {
+            File[] files = file.listFiles();
+            if (files != null && files.length > 0) {
+                for (File f : files) {
+                    processFile(f);
+                }
+            }
+         }
+    }
+
+    /**
+     * Updates the existing files with the given files/folders.
+     * @param files the new folders/files to process.
+     */
+    void update(Collection<File> files) {
+        mLoadedFiles.clear();
+        mLoadedFiles.putAll(mProcessedFiles);
+        mResults.clear();
+        mProcessedFiles.clear();
+        for (File f : files) {
+            processFile(f);
+        }
+    }
+}
diff --git a/builder/src/main/java/com/android/builder/resources/FileStatus.java b/builder/src/main/java/com/android/builder/resources/FileStatus.java
new file mode 100644
index 0000000..45c3d72
--- /dev/null
+++ b/builder/src/main/java/com/android/builder/resources/FileStatus.java
@@ -0,0 +1,24 @@
+/*
+ * 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;
+
+/**
+ * File change state.
+ */
+public enum FileStatus {
+    NEW, REMOVED, CHANGED
+}
diff --git a/builder/src/main/java/com/android/builder/resources/NodeUtils.java b/builder/src/main/java/com/android/builder/resources/NodeUtils.java
new file mode 100644
index 0000000..dcdfa2a
--- /dev/null
+++ b/builder/src/main/java/com/android/builder/resources/NodeUtils.java
@@ -0,0 +1,224 @@
+/*
+ * 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.annotations.VisibleForTesting;
+import org.w3c.dom.*;
+
+/**
+ * Utility class to handle Nodes.
+ *
+ * - convert Node from one XML {@link Document} to be used by another Document
+ * - compare Nodes and attributes.
+ */
+class NodeUtils {
+
+    /**
+     * 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.
+     */
+    static Node adoptNode(Document document, Node node) {
+        Node newNode = document.adoptNode(node);
+
+        updateNamespace(newNode, document);
+
+        return newNode;
+    }
+
+    static void addAttribute(Document document, Node node,
+                             String NamespaceUri, String attrName, String attrValue) {
+        Attr attr;
+        if (NamespaceUri != null) {
+            attr = document.createAttributeNS(NamespaceUri, attrName);
+        } else {
+            attr = document.createAttribute(attrName);
+        }
+
+        attr.setValue(attrValue);
+
+        if (NamespaceUri != null) {
+            node.getAttributes().setNamedItemNS(attr);
+        } else {
+            node.getAttributes().setNamedItem(attr);
+        }
+    }
+
+    /**
+     * 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 static 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 static 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 static 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 static 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;
+            }
+        }
+    }
+
+    static boolean compareElementNode(Node node1, Node node2) {
+        if (!node1.getNodeName().equals(node2.getNodeName())) {
+            return false;
+        }
+
+        NamedNodeMap attr1 = node1.getAttributes();
+        NamedNodeMap attr2 = node2.getAttributes();
+
+        return compareAttributes(attr1, attr2) &&
+                compareChildren(node1.getChildNodes(), node2.getChildNodes());
+
+    }
+
+    private static boolean compareChildren(NodeList children1, NodeList children2) {
+        // because this represents a resource values, we're going to be very strict about this
+        // comparison.
+        if (children1.getLength() != children2.getLength()) {
+            return false;
+        }
+
+        for (int i = 0, n = children1.getLength(); i < n; i++) {
+            Node child1 = children1.item(i);
+            Node child2 = children2.item(i);
+
+            short nodeType = child1.getNodeType();
+            if (nodeType != child2.getNodeType()) {
+                return false;
+            }
+
+            switch (nodeType) {
+                case Node.ELEMENT_NODE:
+                    if (!compareElementNode(child1, child2)) {
+                        return false;
+                    }
+                    break;
+                case Node.TEXT_NODE:
+                case Node.COMMENT_NODE:
+                    if (!child1.getNodeValue().trim().equals(child2.getNodeValue().trim())) {
+                        return false;
+                    }
+                    break;
+            }
+        }
+
+        return true;
+    }
+
+    @VisibleForTesting
+    static boolean compareAttributes(NamedNodeMap attrMap1, NamedNodeMap attrMap2) {
+        if (attrMap1.getLength() != attrMap2.getLength()) {
+            return false;
+        }
+
+        for (int i = 0, n = attrMap1.getLength(); i < n; i++) {
+            Attr attr1 = (Attr) attrMap1.item(i);
+
+            String ns1 = attr1.getNamespaceURI();
+
+            Attr attr2;
+            if (ns1 != null) {
+                attr2 = (Attr) attrMap2.getNamedItemNS(ns1, attr1.getLocalName());
+            }  else {
+                attr2 = (Attr) attrMap2.getNamedItem(attr1.getName());
+            }
+
+            if (attr2 == null || !attr2.getValue().equals(attr1.getValue())) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+}
diff --git a/builder/src/main/java/com/android/builder/resources/Resource.java b/builder/src/main/java/com/android/builder/resources/Resource.java
index b5dd69c..65f476c 100644
--- a/builder/src/main/java/com/android/builder/resources/Resource.java
+++ b/builder/src/main/java/com/android/builder/resources/Resource.java
@@ -16,6 +16,7 @@
 
 package com.android.builder.resources;
 
+import com.android.annotations.NonNull;
 import com.android.resources.ResourceType;
 import org.w3c.dom.Node;
 
@@ -26,59 +27,219 @@
  * in case of a resource coming from a value file.
  *
  */
-public class Resource {
+class Resource {
+
+    private static final int MASK_TOUCHED = 0x01;
+    private static final int MASK_REMOVED = 0x02;
+    private static final int MASK_WRITTEN = 0x10;
 
     private final String mName;
     private final ResourceType mType;
 
-    private final Node mValue;
+    private Node mValue;
     private ResourceFile mSource;
 
-    Resource(String name, ResourceType type, Node value) {
+    /**
+     * The status of the Resource. It's a bit mask as opposed to an enum
+     * to differentiate removed and removed+written
+     */
+    private int mStatus = 0;
+
+    /**
+     * Constructs the object with a name, type and optional value.
+     *
+     * Note that the object is not fully usable as-is. It must be added to a ResourceFile first.
+     *
+     * @param name the name of the resource
+     * @param type the type of the resource
+     * @param value an optional Node that represents the resource value.
+     */
+    Resource(@NonNull String name, @NonNull ResourceType type, Node value) {
         mName = name;
         mType = type;
         mValue = value;
     }
 
+    /**
+     * Returns the name of the resource.
+     * @return the name.
+     */
+    @NonNull
     public String getName() {
         return mName;
     }
 
+    /**
+     * Returns the type of the resource.
+     * @return the type.
+     */
+    @NonNull
     public ResourceType getType() {
         return mType;
     }
 
+    /**
+     * Returns the ResourceFile the resource is coming from. Can be null.
+     * @return the resource file.
+     */
     public ResourceFile getSource() {
         return mSource;
     }
 
+    /**
+     * Returns the optional value of the resource. Can be null
+     * @return the value or null.
+     */
     public Node getValue() {
         return mValue;
     }
 
+    /**
+     * Sets the value of the resource and set its state to TOUCHED.
+     * @param from the resource to copy the value from.
+     */
+    void setValue(Resource from) {
+        mValue = from.mValue;
+        setTouched();
+    }
+
+
+    /**
+     * Sets the ResourceFile
+     * @param sourceFile the ResourceFile
+     */
     void setSource(ResourceFile sourceFile) {
         mSource = sourceFile;
     }
 
     /**
+     * Resets the state of the resource be WRITTEN. All other states are removed.
+     * @return this
+     *
+     * @see #isWritten()
+     */
+    Resource resetStatusToWritten() {
+        mStatus = MASK_WRITTEN;
+        return this;
+    }
+
+    /**
+     * Sets the resource be WRITTEN. Other states are kept.
+     * @return this
+     *
+     * @see #isWritten()
+     */
+    Resource setWritten() {
+        mStatus |= MASK_WRITTEN;
+        return this;
+    }
+
+    /**
+     * Sets the resource be REMOVED. Other states are kept.
+     * @return this
+     *
+     * @see #isRemoved()
+     */
+    Resource setRemoved() {
+        mStatus |= MASK_REMOVED;
+        return this;
+    }
+
+    /**
+     * Sets the resource be TOUCHED. Other states are kept.
+     * @return this
+     *
+     * @see #isTouched()
+     */
+    Resource setTouched() {
+        mStatus |= MASK_TOUCHED;
+        return this;
+    }
+
+    /**
+     * Returns whether the resource is REMOVED
+     * @return true if removed
+     */
+    boolean isRemoved() {
+        return (mStatus & MASK_REMOVED) != 0;
+    }
+
+    /**
+     * Returns whether the resource is TOUCHED
+     * @return true if touched
+     */
+    boolean isTouched() {
+        return (mStatus & MASK_TOUCHED) != 0;
+    }
+
+    /**
+     * Returns whether the resource is WRITTEN
+     * @return true if written
+     */
+    boolean isWritten() {
+        return (mStatus & MASK_WRITTEN) != 0;
+    }
+
+    /**
      * Returns a key for this resource. They key uniquely identifies this resource by combining
      * resource type, qualifiers, and name.
+     *
+     * If the resource has not been added to a {@link ResourceFile}, this will throw an
+     * {@link IllegalStateException}.
+     *
      * @return the key for this resource.
+     *
+     * @throws IllegalStateException if the resource is not added to a ResourceFile
      */
     String getKey() {
-        String qualifiers = mSource != null ? mSource.getQualifiers() : null;
-        if (qualifiers != null) {
+        if (mSource == null) {
+            throw new IllegalStateException(
+                    "Resource.getKey called on object with no ResourceFile: " + this);
+        }
+        String qualifiers = mSource.getQualifiers();
+        if (qualifiers != null && qualifiers.length() > 0) {
             return mType.getName() + "-" + qualifiers + "/" + mName;
         }
 
         return mType.getName() + "/" + mName;
     }
 
+    /**
+     * Compares the Resource {@link #getValue()} together and returns true if they are the same.
+     * @param resource The Resource object to compare to.
+     * @return true if equal
+     */
+    public boolean compareValueWith(Resource resource) {
+        if (mValue != null && resource.mValue != null) {
+            return NodeUtils.compareElementNode(mValue, resource.mValue);
+        }
+
+        return mValue == resource.mValue;
+    }
+
     @Override
     public String toString() {
         return "Resource{" +
                 "mName='" + mName + '\'' +
                 ", mType=" + mType +
+                ", mStatus=" + mStatus +
                 '}';
     }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        Resource resource = (Resource) o;
+
+        return mName.equals(resource.mName) && mType == resource.mType;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = mName.hashCode();
+        result = 31 * result + mType.hashCode();
+        return result;
+    }
 }
diff --git a/builder/src/main/java/com/android/builder/resources/ResourceFile.java b/builder/src/main/java/com/android/builder/resources/ResourceFile.java
index 7c335ee..42ba9bc 100644
--- a/builder/src/main/java/com/android/builder/resources/ResourceFile.java
+++ b/builder/src/main/java/com/android/builder/resources/ResourceFile.java
@@ -18,43 +18,111 @@
 
 import com.android.annotations.NonNull;
 import com.android.annotations.Nullable;
-import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 
 import java.io.File;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 
 /**
+ * Represents a file in a resource folders.
+ *
+ * It contains a link to the {@link File}, the qualifier string (which is the name of the folder
+ * after the first '-' character), a list of {@link Resource} and a type.
+ *
+ * The type of the file is based on whether the file is located in a values folder (FileType.MULTI)
+ * or in another folder (FileType.SINGLE).
  */
 class ResourceFile {
 
+    static enum FileType {
+        SINGLE, MULTI
+    }
+
+    private final FileType mType;
     private final File mFile;
-    private final List<Resource> mItems;
+    private final Map<String, Resource> mItems;
     private final String mQualifiers;
 
-    ResourceFile(File file, Resource item, String qualifiers) {
+    /**
+     * Creates a resource file with a single resource item.
+     *
+     * The source file is set on the item with {@link Resource#setSource(ResourceFile)}
+     *
+     * The type of the ResourceFile will by {@link FileType#SINGLE}.
+     *
+     * @param file the File
+     * @param item the resource item
+     * @param qualifiers the qualifiers.
+     */
+    ResourceFile(@NonNull File file, @NonNull Resource item, @Nullable String qualifiers) {
+        mType = FileType.SINGLE;
         mFile = file;
-        mItems = Collections.singletonList(item);
         mQualifiers = qualifiers;
 
         item.setSource(this);
+        mItems = Collections.singletonMap(item.getKey(), item);
     }
 
+    /**
+     * Creates a resource file with a list of resource items.
+     *
+     * The source file is set on the items with {@link Resource#setSource(ResourceFile)}
+     *
+     * The type of the ResourceFile will by {@link FileType#MULTI}.
+     *
+     * @param file the File
+     * @param items the resource items
+     * @param qualifiers the qualifiers.
+     */
     ResourceFile(@NonNull File file, @NonNull List<Resource> items, @Nullable String qualifiers) {
+        mType = FileType.MULTI;
         mFile = file;
-        mItems = Lists.newArrayList(items);
         mQualifiers = qualifiers;
 
+        mItems = Maps.newHashMapWithExpectedSize(items.size());
         for (Resource item : items) {
             item.setSource(this);
+            mItems.put(item.getKey(), item);
         }
     }
 
-    @NonNull File getFile() {
+    @NonNull
+    FileType getType() {
+        return mType;
+    }
+
+    @NonNull
+    File getFile() {
         return mFile;
     }
 
-    @Nullable String getQualifiers() {
+    @Nullable
+    String getQualifiers() {
         return mQualifiers;
     }
+
+    Resource getItem() {
+        assert mItems.size() == 1;
+        return mItems.values().iterator().next();
+    }
+
+    @NonNull
+    Collection<Resource> getItems() {
+        return mItems.values();
+    }
+
+    @NonNull
+    Map<String, Resource> getItemMap() {
+        return mItems;
+    }
+
+    void addItems(Collection<Resource> items) {
+        for (Resource item : items) {
+            mItems.put(item.getKey(), item);
+            item.setSource(this);
+        }
+    }
 }
diff --git a/builder/src/main/java/com/android/builder/resources/ResourceMap.java b/builder/src/main/java/com/android/builder/resources/ResourceMap.java
new file mode 100644
index 0000000..cd1f5d9
--- /dev/null
+++ b/builder/src/main/java/com/android/builder/resources/ResourceMap.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 com.android.annotations.NonNull;
+import com.google.common.collect.ListMultimap;
+
+/**
+ * A Resource Map able to provide a {@link ListMultimap} of Resources where the keys are
+ * the value returned by {@link Resource#getKey()}
+ */
+interface ResourceMap {
+
+    /**
+     * Returns the number of resources.
+     * @return the number of resources.
+     */
+    int size();
+
+    /**
+     * a Multi map of (key, resource) where key is the result of
+     * {@link com.android.builder.resources.Resource#getKey()}
+     * @return a non null map
+     */
+    @NonNull
+    ListMultimap<String, Resource> getResourceMap();
+}
diff --git a/builder/src/main/java/com/android/builder/resources/ResourceMerger.java b/builder/src/main/java/com/android/builder/resources/ResourceMerger.java
index a059ec2..88dab2f 100644
--- a/builder/src/main/java/com/android/builder/resources/ResourceMerger.java
+++ b/builder/src/main/java/com/android/builder/resources/ResourceMerger.java
@@ -16,33 +16,584 @@
 
 package com.android.builder.resources;
 
-import com.google.common.collect.Maps;
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.common.xml.XmlPrettyPrinter;
+import com.android.resources.ResourceFolderType;
+import com.android.utils.Pair;
+import com.google.common.base.Charsets;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.common.io.Files;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
 
-import java.util.Map;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.*;
 
 /**
+ * Merges {@link ResourceSet}s and writes a resource folder that can be fed to aapt.
+ *
+ * This is able to save its post work state and reload this for incremental update.
  */
-public class ResourceMerger {
+public class ResourceMerger implements ResourceMap {
+
+    static final String FN_MERGER_XML = "merger.xml";
+    private static final String FN_VALUES_XML = "values.xml";
+    private static final String NODE_MERGER = "merger";
+    private static final String NODE_RESOURCE_SET = "resourceSet";
 
     /**
-     * The merged resources
+     * All the resources. The merged version will be the last item in the list.
      */
-    private final Map<String, Resource> mItems = Maps.newHashMap();
+    private final List<ResourceSet> mResourceSets = Lists.newArrayList();
 
-    public ResourceMerger() {
-
-    }
+    public ResourceMerger() { }
 
     /**
-     * adds a new sourceset and overlays it on top of the existing resource items
+     * adds a new {@link ResourceSet} and overlays it on top of the existing ResourceSets.
+     *
      * @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());
+        mResourceSets.add(resourceSet);
     }
 
-    public ResourceSet getMergedSet() {
-        return new ResourceSet(mItems);
+    /**
+     * Returns the list of ResourceSet objects.
+     * @return the resource sets.
+     */
+    @VisibleForTesting
+    List<ResourceSet> getResourceSets() {
+        return mResourceSets;
+    }
+
+    @VisibleForTesting
+    void validateResourceSets() throws DuplicateResourceException {
+        for (ResourceSet resourceSet : mResourceSets) {
+            resourceSet.checkItems();
+        }
+    }
+
+    /**
+     * Returns the number of resources.
+     * @return the number of resources.
+     *
+     * @see ResourceMap
+     */
+    @Override
+    public int size() {
+        // put all the resource keys in a set.
+        Set<String> keys = Sets.newHashSet();
+
+        for (ResourceSet resourceSet : mResourceSets) {
+            ListMultimap<String, Resource> map = resourceSet.getResourceMap();
+            keys.addAll(map.keySet());
+        }
+
+        return keys.size();
+    }
+
+    /**
+     * Returns a map of the resources.
+     * @return a map of items.
+     *
+     * @see ResourceMap
+     */
+    @NonNull
+    @Override
+    public ListMultimap<String, Resource> getResourceMap() {
+        // put all the sets in a multimap. The result is that for each key,
+        // there is a sorted list of items from all the layers, including removed ones.
+        ListMultimap<String, Resource> fullItemMultimap = ArrayListMultimap.create();
+
+        for (ResourceSet resourceSet : mResourceSets) {
+            ListMultimap<String, Resource> map = resourceSet.getResourceMap();
+            for (Map.Entry<String, Collection<Resource>> entry : map.asMap().entrySet()) {
+                fullItemMultimap.putAll(entry.getKey(), entry.getValue());
+            }
+        }
+
+        return fullItemMultimap;
+    }
+
+    /**
+     * Writes the result of the merge to a destination resource folder.
+     *
+     * The output is an Android style resource folder than can be fed to aapt.
+     *
+     * @param rootFolder the folder to write the resources in.
+     * @throws IOException
+     * @throws DuplicateResourceException
+     */
+    public void writeResourceFolder(File rootFolder)
+            throws IOException, DuplicateResourceException {
+        // get all the resource keys.
+        Set<String> resourceKeys = Sets.newHashSet();
+
+        for (ResourceSet resourceSet : mResourceSets) {
+            // quick check on duplicates in the resource set.
+            resourceSet.checkItems();
+            ListMultimap<String, Resource> map = resourceSet.getResourceMap();
+            resourceKeys.addAll(map.keySet());
+        }
+
+        // map of XML values files to write after parsing all the files.
+        // the key is the qualifier.
+        ListMultimap<String, Resource> valuesResMap = ArrayListMultimap.create();
+        // set of qualifier that was a previously written resource disappear. This is to keep track
+        // of which file to write if no other resources are touched.
+        Set<String> qualifierWithDeletedValues = Sets.newHashSet();
+
+        // loop on all the resources.
+        for (String resourceKey : resourceKeys) {
+            // for each resource, look in the resource sets, starting from the end of the list.
+
+            Resource previouslyWritten = null;
+            Resource toWrite = null;
+
+            /*
+             * We are looking for what to write/delete: the last non deleted item, and the
+             * previously written one.
+             */
+
+            setLoop: for (int i = mResourceSets.size() - 1 ; i >= 0 ; i--) {
+                ResourceSet resourceSet = mResourceSets.get(i);
+
+                // look for the resource key in the set
+                ListMultimap<String, Resource> resourceMap = resourceSet.getResourceMap();
+
+                List<Resource> resources = resourceMap.get(resourceKey);
+                if (resources.isEmpty()) {
+                    continue;
+                }
+
+                // The list can contain at max 2 items. One touched and one deleted.
+                // More than one deleted means there was more than one which isn't possible
+                // More than one touched means there is more than one and this isn't possible.
+                for (int ii = resources.size() - 1 ; ii >= 0 ; ii--) {
+                    Resource resource = resources.get(ii);
+
+                    if (resource.isWritten()) {
+                        assert previouslyWritten == null;
+                        previouslyWritten = resource;
+                    }
+
+                    if (toWrite == null && !resource.isRemoved()) {
+                        toWrite = resource;
+                    }
+
+                    if (toWrite != null && previouslyWritten != null) {
+                        break setLoop;
+                    }
+                }
+            }
+
+            // done searching, we should at least have something.
+            assert previouslyWritten != null || toWrite != null;
+
+            // now need to handle, the type of each (single res file, multi res file), whether
+            // they are the same object or not, whether the previously written object was deleted.
+
+            if (toWrite == null) {
+                // nothing to write? delete only then.
+                assert previouslyWritten.isRemoved();
+
+                ResourceFile.FileType type = previouslyWritten.getSource().getType();
+
+                if (type == ResourceFile.FileType.SINGLE) {
+                    removeOutFile(rootFolder, previouslyWritten.getSource());
+                } else {
+                    qualifierWithDeletedValues.add(previouslyWritten.getSource().getQualifiers());
+                }
+
+            } else if (previouslyWritten == null || previouslyWritten == toWrite) {
+                // easy one: new or updated res
+
+                writeResource(rootFolder, valuesResMap, toWrite);
+            } else {
+                // replacement of a resource by another.
+
+                // first force the writing of the new one.
+                toWrite.setTouched();
+
+                // write the new value
+                writeResource(rootFolder, valuesResMap, toWrite);
+
+                ResourceFile.FileType previousType = previouslyWritten.getSource().getType();
+                ResourceFile.FileType newType = toWrite.getSource().getType();
+
+                if (previousType == newType) {
+                    // if the type is multi, then we make sure to flag the
+                    // qualifier as deleted.
+                    if (previousType == ResourceFile.FileType.MULTI) {
+                        qualifierWithDeletedValues.add(
+                                previouslyWritten.getSource().getQualifiers());
+                    }
+                } else if (newType == ResourceFile.FileType.SINGLE) {
+                    // new type is single, so old type is multi.
+                    // ensure the previous one is deleted by forcing rewrite of its associated
+                    // qualifiers.
+                    qualifierWithDeletedValues.add(previouslyWritten.getSource().getQualifiers());
+                } else {
+                    // new type is values, and old is single res file.
+                    // delete the old single res file
+                    removeOutFile(rootFolder, previouslyWritten.getSource());
+                }
+            }
+        }
+
+        // now write the values files.
+        for (String key : valuesResMap.keySet()) {
+            // the key is the qualifier.
+
+            // check if we have to write the file due to deleted values.
+            // also remove it from that list anyway (to detect empty qualifiers later).
+            boolean mustWriteFile = qualifierWithDeletedValues.remove(key);
+
+            // get the list of items to write
+            Collection<Resource> items = valuesResMap.get(key);
+
+            // now check if we really have to write it
+            if (!mustWriteFile) {
+                for (Resource item : items) {
+                    if (item.isTouched()) {
+                        mustWriteFile = true;
+                        break;
+                    }
+                }
+            }
+
+            if (mustWriteFile) {
+                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, FN_VALUES_XML);
+
+                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 = NodeUtils.adoptNode(document, item.getValue());
+                        rootNode.appendChild(adoptedNode);
+                    }
+
+                    String content = XmlPrettyPrinter.prettyPrint(document);
+
+                    Files.write(content, outFile, Charsets.UTF_8);
+                } catch (ParserConfigurationException e) {
+                    throw new IOException(e);
+                }
+            }
+        }
+
+        // now remove empty values files.
+        for (String key : qualifierWithDeletedValues) {
+            String folderName = key != null && key.length() > 0 ?
+                    ResourceFolderType.VALUES.getName() + SdkConstants.RES_QUALIFIER_SEP + key :
+                    ResourceFolderType.VALUES.getName();
+
+            removeOutFile(rootFolder, folderName, FN_VALUES_XML);
+        }
+    }
+
+    /**
+     * Removes a file that already exists in the out res folder.
+     * @param outFolder the out res folder
+     * @param sourceFile the source file that created the file to remove.
+     * @return true if sucess.
+     */
+    private static boolean removeOutFile(File outFolder, ResourceFile sourceFile) {
+        if (sourceFile.getType() == ResourceFile.FileType.MULTI) {
+            throw new IllegalArgumentException("SourceFile cannot be a FileType.MULTI");
+        }
+
+        File file = sourceFile.getFile();
+        String fileName = file.getName();
+        String folderName = file.getParentFile().getName();
+
+        return removeOutFile(outFolder, folderName, fileName);
+    }
+
+    /**
+     * Removes a file from a folder based on a sub folder name and a filename
+     *
+     * @param outFolder the root folder to remove the file from
+     * @param folderName the sub folder name
+     * @param fileName the file name.
+     * @return true if success.
+     */
+    private static boolean removeOutFile(File outFolder, String folderName, String fileName) {
+        File valuesFolder = new File(outFolder, folderName);
+        File outFile = new File(valuesFolder, fileName);
+        return outFile.delete();
+    }
+
+    /**
+     * Writes a given Resource to a given root res folder.
+     * If the Resource is to be written in a "Values" folder, then it is added to a map instead.
+     *
+     * @param rootFolder the root res folder
+     * @param valuesResMap a map of existing values-type resources where the key is the qualifiers
+     *                     of the values folder.
+     * @param resource the resource to add.
+     * @throws IOException
+     */
+    private void writeResource(File rootFolder, ListMultimap<String, Resource> valuesResMap,
+                               Resource resource) throws IOException {
+        ResourceFile.FileType type = resource.getSource().getType();
+
+        if (type == ResourceFile.FileType.MULTI) {
+            // this is a resource for the values files
+
+            // just add the node to write to the map based on the qualifier.
+            // We'll figure out later if the files needs to be written or (not)
+
+            String qualifier = resource.getSource().getQualifiers();
+            if (qualifier == null) {
+                qualifier = "";
+            }
+
+            valuesResMap.put(qualifier, resource);
+        } else {
+            // This is a single value file.
+            // Only write it if the state is TOUCHED.
+            if (resource.isTouched()) {
+                ResourceFile resourceFile = resource.getSource();
+                File file = resourceFile.getFile();
+
+                String filename = file.getName();
+                String folderName = resource.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);
+            }
+        }
+    }
+
+    /**
+     * Writes a single blog file to store all that the ResourceMerger knows about.
+     *
+     * @param blobRootFolder the root folder where blobs are store.
+     * @throws java.io.IOException
+     *
+     * @see #loadFromBlob(java.io.File)
+     */
+    public void writeBlobTo(File blobRootFolder) throws IOException {
+        // write "compact" blob
+        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(NODE_MERGER);
+            document.appendChild(rootNode);
+
+            for (ResourceSet resourceSet : mResourceSets) {
+                Node resourceSetNode = document.createElement(NODE_RESOURCE_SET);
+                rootNode.appendChild(resourceSetNode);
+
+                resourceSet.appendToXml(resourceSetNode, document);
+            }
+
+            String content = XmlPrettyPrinter.prettyPrint(document);
+
+            blobRootFolder.mkdirs();
+            Files.write(content, new File(blobRootFolder, FN_MERGER_XML), Charsets.UTF_8);
+        } catch (ParserConfigurationException e) {
+            throw new IOException(e);
+        }
+    }
+
+    /**
+     * Loads the merger state from a blob file.
+     *
+     * @param blobRootFolder the folder containing the blob.
+     * @return
+     * @throws IOException
+     *
+     * @see #writeBlobTo(java.io.File)
+     */
+    public boolean loadFromBlob(File blobRootFolder) throws IOException {
+        File file = new File(blobRootFolder, FN_MERGER_XML);
+        if (!file.isFile()) {
+            return false;
+        }
+
+        BufferedInputStream stream = null;
+        try {
+            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+            stream = new BufferedInputStream(new FileInputStream(file));
+            InputSource is = new InputSource(stream);
+            factory.setNamespaceAware(true);
+            factory.setValidating(false);
+
+            DocumentBuilder builder = factory.newDocumentBuilder();
+            Document document = builder.parse(is);
+
+            // get the root node
+            Node rootNode = document.getDocumentElement();
+            if (rootNode == null || !NODE_MERGER.equals(rootNode.getLocalName())) {
+                return false;
+            }
+
+            NodeList nodes = rootNode.getChildNodes();
+
+            for (int i = 0, n = nodes.getLength(); i < n; i++) {
+                Node node = nodes.item(i);
+
+                if (node.getNodeType() != Node.ELEMENT_NODE ||
+                        !NODE_RESOURCE_SET.equals(node.getLocalName())) {
+                    continue;
+                }
+
+                ResourceSet resourceSet = ResourceSet.createFromXml(node);
+                if (resourceSet != null) {
+                    mResourceSets.add(resourceSet);
+                }
+            }
+
+            setResourcesToWritten();
+
+            return true;
+        } catch (FileNotFoundException e) {
+            throw new IOException(e);
+        } catch (ParserConfigurationException e) {
+            throw new IOException(e);
+        } catch (SAXException e) {
+            throw new IOException(e);
+        } finally {
+            try {
+                if (stream != null) {
+                    stream.close();
+                }
+            } catch (IOException e) {
+                // ignore
+            }
+        }
+    }
+
+    /**
+     * Sets all existing resources to have their state be WRITTEN.
+     *
+     * @see com.android.builder.resources.Resource#isWritten()
+     */
+    private void setResourcesToWritten() {
+        ListMultimap<String, Resource> resources = ArrayListMultimap.create();
+
+        for (ResourceSet resourceSet : mResourceSets) {
+            ListMultimap<String, Resource> map = resourceSet.getResourceMap();
+            for (Map.Entry<String, Collection<Resource>> entry : map.asMap().entrySet()) {
+                resources.putAll(entry.getKey(), entry.getValue());
+            }
+        }
+
+        for (String key : resources.keySet()) {
+            List<Resource> resourceList = resources.get(key);
+            resourceList.get(resourceList.size() - 1).resetStatusToWritten();
+        }
+    }
+
+    /**
+     * Checks that a loaded merger can be updated with a given list of ResourceSet.
+     *
+     * For now this means the sets haven't changed.
+     *
+     * @param resourceSets the resource sets.
+     * @return true if the update can be performed. false if a full merge should be done.
+     */
+    public boolean checkValidUpdate(List<ResourceSet> resourceSets) {
+        if (resourceSets.size() != mResourceSets.size()) {
+            return false;
+        }
+
+        for (int i = 0, n = resourceSets.size(); i < n; i++) {
+            ResourceSet localSet = mResourceSets.get(i);
+            ResourceSet newSet = resourceSets.get(i);
+
+            List<File> localSourceFiles = localSet.getSourceFiles();
+            List<File> newSourceFiles = newSet.getSourceFiles();
+
+            // compare the config name and source files sizes.
+            if (!newSet.getConfigName().equals(localSet.getConfigName()) ||
+                    localSourceFiles.size() != newSourceFiles.size()) {
+                return false;
+            }
+
+            // compare the source files. The order is not important so it should be normalized
+            // before it's compared.
+            // make copies to sort.
+            localSourceFiles = Lists.newArrayList(localSourceFiles);
+            Collections.sort(localSourceFiles);
+            newSourceFiles = Lists.newArrayList(newSourceFiles);
+            Collections.sort(newSourceFiles);
+
+            if (!localSourceFiles.equals(newSourceFiles)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns a ResourceSet that contains a given file.
+     *
+     * "contains" means that the ResourceSet has a source file/folder that is the root folder
+     * of this file. The folder and/or file doesn't have to exist.
+     *
+     * @param file the file to check
+     * @return a pair containing the ResourceSet and its source file that contains the file.
+     */
+    public Pair<ResourceSet, File> getResourceSetContaining(File file) {
+        for (ResourceSet resourceSet : mResourceSets) {
+            File sourceFile = resourceSet.findMatchingSourceFile(file);
+            if (file != null) {
+                return Pair.of(resourceSet, sourceFile);
+            }
+        }
+
+        return null;
     }
 }
diff --git a/builder/src/main/java/com/android/builder/resources/ResourceSet.java b/builder/src/main/java/com/android/builder/resources/ResourceSet.java
index dcc2a93..20a08b4 100644
--- a/builder/src/main/java/com/android/builder/resources/ResourceSet.java
+++ b/builder/src/main/java/com/android/builder/resources/ResourceSet.java
@@ -16,236 +16,141 @@
 
 package com.android.builder.resources;
 
-import com.android.SdkConstants;
+import com.android.annotations.NonNull;
 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.ListMultimap;
+import com.google.common.collect.Lists;
 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)
+ * Represents a set of resources.
+ *
+ * The resources can be coming from multiple source folders. Duplicates are detected (either
+ * from the same source folder -- same resource in values files -- or across the source folders.
+ *
+ * Each source folders is considered to be at the same level. To use overlays, a
+ * {@link ResourceMerger} must be used.
+ *
+ * Creating the set and adding folders does not load the data.
+ * The data can be loaded from the files, or from a blob which is generated by the set itself.
+ *
+ * Upon loading the data from the blob, the data can be updated with fresher files. Each resource
+ * that is updated is flagged as such, in order to manage incremental update.
+ *
+ * Writing/Loading the blob is not done through this class directly, but instead through the
+ * {@link ResourceMerger} which contains ResourceSet objects.
  */
-public class ResourceSet {
+public class ResourceSet implements SourceSet, ResourceMap {
+
+    private static final String NODE_SOURCE = "source";
+    private static final String ATTR_CONFIG = "config";
+    private static final String ATTR_PATH = "path";
+    private static final String NODE_FILE = "file";
+    private static final String ATTR_QUALIFIER = "qualifiers";
+    private static final String ATTR_TYPE = "type";
+    private static final String ATTR_NAME = "name";
+
+    private final String mConfigName;
+
+    /**
+     * List of source files. The may not have been loaded yet.
+     */
+    private final List<File> mSourceFiles = Lists.newArrayList();
 
     /**
      * The key is the {@link com.android.builder.resources.Resource#getKey()}.
+     * This is a multimap to support moving a resource from one file to another (values file)
+     * during incremental update.
      */
-    private final Map<String, Resource> mItems = Maps.newHashMap();
+    private final ListMultimap<String, Resource> mItems = ArrayListMultimap.create();
 
-    public ResourceSet() {
-        // nothing done here
-    }
+    /**
+     * Map of source files to ResourceFiles. This is a multimap because the key is the source
+     * file/folder, not the
+     * File for the resource file itself.
+     */
+    private final ListMultimap<File, ResourceFile> mSourceFileToResourceFilesMap = ArrayListMultimap.create();
+    /**
+     * Map from a File to its ResourceFile.
+     */
+    private final Map<File, ResourceFile> mResourceFileMap = Maps.newHashMap();
 
-    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 boolean isEmpty() {
-        return mItems.isEmpty();
-    }
-
-    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);
-            }
-        }
+    /**
+     * Creates a resource set with a given configName. The name is used to identify the set
+     * across sessions.
+     *
+     * @param configName the name of the config this set is associated with.
+     */
+    public ResourceSet(String configName) {
+        mConfigName = configName;
     }
 
     /**
-     * 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.
+     * Adds a collection of source files.
+     * @param files the source files to add.
      */
-    private Node adoptNode(Document document, Node node) {
-        Node newNode = document.adoptNode(node);
-
-        updateNamespace(newNode, document);
-
-        return newNode;
+    public void addSources(Collection<File> files) {
+        mSourceFiles.addAll(files);
     }
 
     /**
-     * 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
+     * Adds a new source file
+     * @param file the source file.
      */
-    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);
-                }
-            }
-        }
+    public void addSource(File file) {
+        mSourceFiles.add(file);
     }
 
     /**
-     * Update the namespace of a given node to work with a given document.
-     * @param node the node to update
-     * @param document the new document
+     * Get the list of source files.
+     * @return the source files.
      */
-    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);
-        }
+    @NonNull
+    @Override
+    public List<File> getSourceFiles() {
+        return mSourceFiles;
     }
 
     /**
-     * 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.
+     * Returns the config name.
+     * @return the config name.
      */
-    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();
+    public String getConfigName() {
+        return mConfigName;
+    }
+
+    /**
+     * Returns a matching Source file that contains a given file.
+     *
+     * "contains" means that the source file/folder is the root folder
+     * of this file. The folder and/or file doesn't have to exist.
+     *
+     * @param file the file to search for
+     * @return the Source file or null if no match is found.
+     */
+    @Override
+    public File findMatchingSourceFile(File file) {
+        for (File sourceFile : mSourceFiles) {
+            if (sourceFile.equals(file)) {
+                return sourceFile;
+            } else if (sourceFile.isDirectory()) {
+                String sourcePath = sourceFile.getAbsolutePath() + File.separator;
+                if (file.getAbsolutePath().startsWith(sourcePath)) {
+                    return sourceFile;
                 }
             }
         }
@@ -253,87 +158,482 @@
         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;
-            }
-        }
+    /**
+     * Returns the number of resources.
+     * @return the number of resources.
+     *
+     * @see ResourceMap
+     */
+    @Override
+    public int size() {
+        // returns the number of keys, not the size of the multimap which would include duplicate
+        // Resource objects.
+        return mItems.keySet().size();
     }
 
-    private void readFolder(File rootFolder) throws DuplicateResourceException {
-        File[] folders = rootFolder.listFiles();
-        if (folders != null) {
-            for (File folder : folders) {
-                if (folder.isDirectory()) {
-                    parseFolder(folder);
+    /**
+     * Returns whether the set is empty of resources.
+     * @return true if the set contains no resources.
+     */
+    public boolean isEmpty() {
+        return mItems.isEmpty();
+    }
+
+    /**
+     * Returns a map of the resources.
+     * @return a map of items.
+     *
+     * @see ResourceMap
+     */
+    @NonNull
+    @Override
+    public ListMultimap<String, Resource> getResourceMap() {
+        return mItems;
+    }
+
+    /**
+     * Loads the resource set from the file its source folder contains.
+     *
+     * All loaded resources are set to TOUCHED. This is so that after loading the resources from
+     * the files, they can be written directly (since touched force them to be written).
+     *
+     * This also checks for duplicates resources.
+     *
+     * @throws DuplicateResourceException
+     * @throws IOException
+     */
+    public void loadFromFiles() throws DuplicateResourceException, IOException {
+        for (File file : mSourceFiles) {
+            if (file.isDirectory()) {
+                readSourceFolder(file);
+
+            } else if (file.isFile()) {
+                // TODO support resource bundle
+            }
+        }
+        checkItems();
+    }
+
+    /**
+     * Appends the resourceSet to a given DOM object.
+     *
+     * @param resourceSetNode the root node for this resource set.
+     * @param document The root XML document
+     */
+    void appendToXml(Node resourceSetNode, Document document) {
+        // add the config name attribute
+        NodeUtils.addAttribute(document, resourceSetNode, null, ATTR_CONFIG, mConfigName);
+
+        // add the source files.
+        // we need to loop on the source files themselves and not the map to ensure we
+        // write empty resourceSets
+        for (File sourceFile : mSourceFiles) {
+
+            // the node for the source and its path attribute
+            Node sourceNode = document.createElement(NODE_SOURCE);
+            resourceSetNode.appendChild(sourceNode);
+            NodeUtils.addAttribute(document, sourceNode, null, ATTR_PATH,
+                    sourceFile.getAbsolutePath());
+
+            Collection<ResourceFile> resourceFiles = mSourceFileToResourceFilesMap.get(sourceFile);
+
+            for (ResourceFile resourceFile : resourceFiles) {
+                // the node for the file and its path and qualifiers attribute
+                Node fileNode = document.createElement(NODE_FILE);
+                sourceNode.appendChild(fileNode);
+                NodeUtils.addAttribute(document, fileNode, null, ATTR_PATH,
+                        resourceFile.getFile().getAbsolutePath());
+                NodeUtils.addAttribute(document, fileNode, null, ATTR_QUALIFIER,
+                        resourceFile.getQualifiers());
+
+                if (resourceFile.getType() == ResourceFile.FileType.MULTI) {
+                    for (Resource item : resourceFile.getItems()) {
+                        Node adoptedNode = NodeUtils.adoptNode(document, item.getValue());
+                        fileNode.appendChild(adoptedNode);
+                    }
+                } else {
+                    Resource item = resourceFile.getItem();
+                    NodeUtils.addAttribute(document, fileNode, null, ATTR_TYPE,
+                            item.getType().getName());
+                    NodeUtils.addAttribute(document, fileNode, null, ATTR_NAME, item.getName());
                 }
             }
         }
     }
 
-    private void parseFolder(File folder) throws DuplicateResourceException {
+    /**
+     * Creates a new ResourceSet from an XML node that was created with
+     * {@link #appendToXml(org.w3c.dom.Node, org.w3c.dom.Document)}
+     *
+     * @param resourceSetNode the node to read from.
+     * @return a new ResourceSet object or null.
+     */
+    static ResourceSet createFromXml(Node resourceSetNode) {
+        // get the config name
+        Attr configNameAttr = (Attr) resourceSetNode.getAttributes().getNamedItem(ATTR_CONFIG);
+        if (configNameAttr == null) {
+            return null;
+        }
 
-        // get the type.
+        // create the ResourceSet that will be filled with the content of the XML.
+        ResourceSet resourceSet = new ResourceSet(configNameAttr.getValue());
+
+        // loop on the source nodes
+        NodeList sourceNodes = resourceSetNode.getChildNodes();
+        for (int i = 0, n = sourceNodes.getLength(); i < n; i++) {
+            Node sourceNode = sourceNodes.item(i);
+
+            if (sourceNode.getNodeType() != Node.ELEMENT_NODE ||
+                    !NODE_SOURCE.equals(sourceNode.getLocalName())) {
+                continue;
+            }
+
+            Attr pathAttr = (Attr) sourceNode.getAttributes().getNamedItem(ATTR_PATH);
+            if (pathAttr == null) {
+                continue;
+            }
+
+            File sourceFolder = new File(pathAttr.getValue());
+            resourceSet.mSourceFiles.add(sourceFolder);
+
+            // now loop on the files inside the source folder.
+            NodeList fileNodes = sourceNode.getChildNodes();
+            for (int j = 0, m = fileNodes.getLength(); j < m; j++) {
+                Node fileNode = fileNodes.item(j);
+
+                if (fileNode.getNodeType() != Node.ELEMENT_NODE ||
+                        !NODE_FILE.equals(fileNode.getLocalName())) {
+                    continue;
+                }
+
+                pathAttr = (Attr) fileNode.getAttributes().getNamedItem(ATTR_PATH);
+                if (pathAttr == null) {
+                    continue;
+                }
+
+                Attr qualifierAttr = (Attr) fileNode.getAttributes().getNamedItem(ATTR_QUALIFIER);
+                String qualifier = qualifierAttr != null ? qualifierAttr.getValue() : null;
+
+                Attr typeAttr = (Attr) fileNode.getAttributes().getNamedItem(ATTR_TYPE);
+                if (typeAttr == null) {
+                    // multi res file
+                    List<Resource> resourceList = Lists.newArrayList();
+
+                    // loop on each node that represent a resource
+                    NodeList resNodes = fileNode.getChildNodes();
+                    for (int iii = 0, nnn = resNodes.getLength(); iii < nnn; iii++) {
+                        Node resNode = resNodes.item(iii);
+
+                        if (resNode.getNodeType() != Node.ELEMENT_NODE) {
+                            continue;
+                        }
+
+                        Resource r = ValueResourceParser.getResource(resNode);
+                        if (r != null) {
+                            resourceList.add(r);
+                        }
+                    }
+
+                    ResourceFile resourceFile = new ResourceFile(new File(pathAttr.getValue()),
+                            resourceList, qualifier);
+                    resourceSet.addResourceFile(sourceFolder, resourceFile);
+
+                    for (Resource item : resourceList) {
+                        resourceSet.mItems.put(item.getKey(), item);
+                    }
+
+                } else {
+                    // single res file
+                    ResourceType type = ResourceType.getEnum(typeAttr.getValue());
+                    if (type == null) {
+                        continue;
+                    }
+
+                    Attr nameAttr = (Attr) fileNode.getAttributes().getNamedItem(ATTR_NAME);
+                    if (nameAttr == null) {
+                        continue;
+                    }
+
+                    Resource item = new Resource(nameAttr.getValue(), type, null);
+                    ResourceFile resourceFile = new ResourceFile(new File(pathAttr.getValue()),
+                            item, qualifier);
+
+                    resourceSet.addResourceFile(sourceFolder, resourceFile);
+                    resourceSet.mItems.put(item.getKey(), item);
+                }
+            }
+        }
+
+        return resourceSet;
+    }
+
+    /**
+     * Reads the content of a resource folders and loads the resources.
+     * @param sourceFolder the source folder to load the resources from.
+     *
+     * @throws DuplicateResourceException
+     * @throws IOException
+     */
+    private void readSourceFolder(File sourceFolder)
+            throws DuplicateResourceException, IOException {
+        File[] folders = sourceFolder.listFiles();
+        if (folders != null) {
+            for (File folder : folders) {
+                if (folder.isDirectory()) {
+                    parseFolder(sourceFolder, folder);
+                }
+            }
+        }
+    }
+
+    /**
+     * temp structure containing a qualifier string and a {@link ResourceType}.
+     */
+    private static class FolderData {
+        String qualifiers = null;
+        ResourceType type = null;
+    }
+
+    /**
+     * Returns a FolderData for the given folder
+     * @param folder the folder.
+     * @return the FolderData object.
+     */
+    @NonNull
+    private static FolderData getFolderData(File folder) {
+        FolderData fd = new FolderData();
+
         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);
+            fd.qualifiers = folderName.substring(pos + 1);
         } else {
             folderType = ResourceFolderType.getTypeByName(folderName);
         }
 
-        boolean singleResourceFile = folderType != ResourceFolderType.VALUES;
-        List<ResourceType> types = FolderTypeRelationship.getRelatedResourceTypes(folderType);
+        if (folderType != ResourceFolderType.VALUES) {
+            fd.type = FolderTypeRelationship.getRelatedResourceTypes(folderType).get(0);
+        }
+
+        return fd;
+    }
+
+    /**
+     * Reads the content of a typed resource folder (sub folder to the root of res folder), and
+     * loads the resources from it.
+     *
+     * @param sourceFolder the main res folder
+     * @param folder the folder to read.
+     *
+     * @throws IOException
+     */
+    private void parseFolder(File sourceFolder, File folder)
+            throws IOException {
+        // get the type.
+        FolderData folderData = getFolderData(folder);
 
         // 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);
+                if (folderData.type != null) {
+                    Resource item = handleSingleResFile(sourceFolder,
+                            folderData.qualifiers, folderData.type, file);
+                    item.setTouched();
                 } 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.
+                    Collection<Resource> items = handleMultiResFile(sourceFolder,
+                            folderData.qualifiers, file);
+                    for (Resource item : items) {
+                        item.setTouched();
                     }
                 }
             }
         }
     }
 
-    private void checkItem(Resource item) throws DuplicateResourceException {
-        Resource otherItem = mItems.get(item.getKey());
-        if (otherItem != null) {
-            throw new DuplicateResourceException(item, otherItem);
+    /**
+     * Handles a single resource file (ie not located in "values") and create a Resource from it.
+     *
+     * @param sourceFolder the top res folder for the file
+     * @param qualifiers the qualifiers associated with the file
+     * @param type the ResourceType read from the parent folder name
+     * @param file the single resource file
+     * @return a Resource object
+     */
+    @NonNull
+    private Resource handleSingleResFile(File sourceFolder, String qualifiers,
+                                         ResourceType type, File file) {
+        int pos;// 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, type, null);
+        ResourceFile resourceFile = new ResourceFile(file, item, qualifiers);
+        addResourceFile(sourceFolder, resourceFile);
+
+        mItems.put(item.getKey(), item);
+
+        return item;
+    }
+
+    /**
+     * Handles a multi res file (in a "values" folder) and create Resource object from it.
+     *
+     * @param sourceFolder the top res folder for the file
+     * @param qualifiers the qualifiers associated with the file
+     * @param file the single resource file
+     * @return a list of created Resource objects.
+     *
+     * @throws IOException
+     */
+    @NonNull
+    private Collection<Resource> handleMultiResFile(File sourceFolder, String qualifiers, File file)
+            throws IOException {
+        ValueResourceParser parser = new ValueResourceParser(file);
+        List<Resource> items = parser.parseFile();
+
+        ResourceFile resourceFile = new ResourceFile(file, items, qualifiers);
+        addResourceFile(sourceFolder, resourceFile);
+
+        for (Resource item : items) {
+            mItems.put(item.getKey(), item);
         }
+
+        return items;
+    }
+
+    /**
+     * Adds a new ResourceFile to this.
+     *
+     * @param sourceFile the parent source file.
+     * @param resourceFile the ResourceFile
+     */
+    private void addResourceFile(File sourceFile, ResourceFile resourceFile) {
+        mSourceFileToResourceFilesMap.put(sourceFile, resourceFile);
+        mResourceFileMap.put(resourceFile.getFile(), resourceFile);
+    }
+
+    /**
+     * Checks for duplicate resources across all source files.
+     *
+     * @throws DuplicateResourceException if a duplicated item is found.
+     */
+    void checkItems() throws DuplicateResourceException {
+        // check a list for duplicate, ignoring removed items.
+        for (Map.Entry<String, Collection<Resource>> entry : mItems.asMap().entrySet()) {
+            Collection<Resource> items = entry.getValue();
+
+            // there can be several version of the same key if some are "removed"
+            Resource lastItem = null;
+            for (Resource item : items) {
+                if (!item.isRemoved()) {
+                    if (lastItem == null) {
+                        lastItem = item;
+                    } else {
+                        throw new DuplicateResourceException(item, lastItem);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Update the ResourceSet with a given file.
+     *
+     * @param sourceFolder the sourceFile containing the changedFile
+     * @param changedFile The changed file
+     * @param fileStatus the change state
+     * @return true if the set was properly updated, false otherwise
+     */
+    public boolean updateWith(File sourceFolder, File changedFile, FileStatus fileStatus)
+            throws IOException {
+        switch (fileStatus) {
+            case CHANGED:
+                FolderData folderData = getFolderData(changedFile.getParentFile());
+                ResourceFile resourceFile = mResourceFileMap.get(changedFile);
+
+                if (folderData.type != null) {
+                    // single res file
+                    resourceFile.getItem().setTouched();
+                } else {
+                    // multi res. Need to parse the file and compare the items one by one.
+                    ValueResourceParser parser = new ValueResourceParser(changedFile);
+                    List<Resource> parsedItems = parser.parseFile();
+
+                    Map<String, Resource> oldItems = Maps.newHashMap(resourceFile.getItemMap());
+
+                    Map<String, Resource> newItems  = Maps.newHashMap();
+
+                    // create a fake ResourceFile to be able to call resource.getKey();
+                    // It's ok because we never use this instance anyway.
+                    ResourceFile fakeResourceFile = new ResourceFile(changedFile, parsedItems,
+                            resourceFile.getQualifiers());
+
+                    for (Resource newItem : parsedItems) {
+                        String newKey = newItem.getKey();
+                        Resource oldItem = oldItems.get(newKey);
+
+                        if (oldItem == null) {
+                            // this is a new item
+                            newItems.put(newKey, newItem.setTouched());
+                        } else {
+                            // remove it from the list of oldItems (this is to detect deletion)
+                            //noinspection SuspiciousMethodCalls
+                            oldItems.remove(oldItem.getKey());
+
+                            // now compare the items
+                            if (!oldItem.compareValueWith(newItem)) {
+                                // if the values are different, take the values from the newItems
+                                // and update the old item status.
+
+                                oldItem.setValue(newItem);
+                            }
+                        }
+                    }
+
+                    // at this point oldItems is left with the deleted items.
+                    // just update their status to removed.
+                    for (Resource deletedItem : oldItems.values()) {
+                        deletedItem.setRemoved();
+                    }
+
+                    // Now we need to add the new items to the resource file and the main map
+                    resourceFile.addItems(newItems.values());
+                    for (Map.Entry<String, Resource> entry : newItems.entrySet()) {
+                        mItems.put(entry.getKey(), entry.getValue());
+                    }
+                }
+
+                return true;
+            case NEW:
+                folderData = getFolderData(changedFile.getParentFile());
+
+                if (folderData.type != null) {
+                    Resource item = handleSingleResFile(sourceFolder, folderData.qualifiers,
+                            folderData.type, changedFile);
+                    item.setTouched();
+                } else {
+                    Collection<Resource> items = handleMultiResFile(sourceFolder,
+                            folderData.qualifiers, changedFile);
+                    for (Resource item : items) {
+                        item.setTouched();
+                    }
+                }
+
+                return true;
+            case REMOVED:
+                resourceFile = mResourceFileMap.get(changedFile);
+
+                // flag all resource items are removed
+                for (Resource item : resourceFile.getItems()) {
+                    item.setRemoved();
+                }
+                return true;
+        }
+
+        return false;
     }
 }
diff --git a/builder/src/main/java/com/android/builder/resources/SourceSet.java b/builder/src/main/java/com/android/builder/resources/SourceSet.java
new file mode 100644
index 0000000..f7e6fff
--- /dev/null
+++ b/builder/src/main/java/com/android/builder/resources/SourceSet.java
@@ -0,0 +1,45 @@
+/*
+ * 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 java.io.File;
+import java.util.List;
+
+/**
+ * A Source sets that contains a list of source files/folders
+ */
+public interface SourceSet {
+
+    /**
+     * Returns a list of Source files or folders.
+     * @return a non null list.
+     */
+    @NonNull
+    List<File> getSourceFiles();
+
+    /**
+     * Finds and returns a Source file/folder containing a given file.
+     *
+     * It doesn't actually check if the file exists, instead just cares about the file path.
+     *
+     * @param file the file to search for
+     * @return the source file containing the file or null if none are found.
+     */
+    File findMatchingSourceFile(File file);
+}
diff --git a/builder/src/main/java/com/android/builder/resources/ValueResourceParser.java b/builder/src/main/java/com/android/builder/resources/ValueResourceParser.java
index 99b8c7d..fd1b0ec 100644
--- a/builder/src/main/java/com/android/builder/resources/ValueResourceParser.java
+++ b/builder/src/main/java/com/android/builder/resources/ValueResourceParser.java
@@ -16,41 +16,54 @@
 
 package com.android.builder.resources;
 
+import com.android.annotations.NonNull;
 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 org.xml.sax.SAXException;
 
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
 import java.io.BufferedInputStream;
 import java.io.File;
 import java.io.FileInputStream;
-import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.util.Collections;
 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;
+import static com.android.SdkConstants.*;
 
 /**
+ * Parser for "values" files.
+ *
+ * This parses the file and returns a list of {@link Resource} object.
  */
 class ValueResourceParser {
 
     private final File mFile;
 
+    /**
+     * Creates the parser for a given file.
+     * @param file the file to parse.
+     */
     ValueResourceParser(File file) {
         mFile = file;
     }
 
-    List<Resource> parseFile() throws FileNotFoundException {
-        Document document = parseDocument();
+    /**
+     * Parses the file and returns a list of {@link Resource} objects.
+     * @return a list of resources.
+     *
+     * @throws IOException
+     */
+    @NonNull
+    List<Resource> parseFile() throws IOException {
+        Document document = parseDocument(mFile);
 
         // get the root node
         Node rootNode = document.getDocumentElement();
@@ -68,19 +81,37 @@
                 continue;
             }
 
-            ResourceType type = getType(node);
-            String name = getName(node);
-
-            if (type != null && name != null) {
-                Resource r = new Resource(name, type, node);
-                resources.add(r);
+            Resource resource = getResource(node);
+            if (resource != null) {
+                resources.add(resource);
             }
         }
 
         return resources;
     }
 
-    private ResourceType getType(Node node) {
+    /**
+     * Returns a new Resource object for a given node.
+     * @param node the node representing the resource.
+     * @return a Resource object or null.
+     */
+    static Resource getResource(Node node) {
+        ResourceType type = getType(node);
+        String name = getName(node);
+
+        if (type != null && name != null) {
+            return new Resource(name, type, node);
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns the type of the Resource based on a node's attributes.
+     * @param node the node
+     * @return the ResourceType or null if it could not be inferred.
+     */
+    static ResourceType getType(Node node) {
         String nodeName = node.getLocalName();
         String typeString = null;
 
@@ -101,7 +132,12 @@
         return null;
     }
 
-    private String getName(Node node) {
+    /**
+     * Returns the name of the resource based a node's attributes.
+     * @param node the node.
+     * @return the name or null if it could not be inferred.
+     */
+    static String getName(Node node) {
         Attr attribute = (Attr) node.getAttributes().getNamedItemNS(null, ATTR_NAME);
 
         if (attribute != null) {
@@ -111,21 +147,31 @@
         return null;
     }
 
-    private Document parseDocument() throws FileNotFoundException {
+    /**
+     * Loads the DOM for a given file and returns a {@link Document} object.
+     * @param file the file to parse
+     * @return a Document object.
+     * @throws IOException
+     */
+    @NonNull
+    static Document parseDocument(File file) throws IOException {
         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
-        BufferedInputStream stream = new BufferedInputStream(new FileInputStream(mFile));
+        BufferedInputStream stream = new BufferedInputStream(new FileInputStream(file));
         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);
+        } catch (ParserConfigurationException e) {
+            throw new IOException(e);
+        } catch (SAXException e) {
+            throw new IOException(e);
         } finally {
             try {
                 stream.close();
             } catch (IOException e) {
+                // ignore
             }
         }
     }
diff --git a/builder/src/test/java/com/android/builder/VariantConfigurationTest.java b/builder/src/test/java/com/android/builder/VariantConfigurationTest.java
index 5ee482a..47dd863 100644
--- a/builder/src/test/java/com/android/builder/VariantConfigurationTest.java
+++ b/builder/src/test/java/com/android/builder/VariantConfigurationTest.java
@@ -35,6 +35,7 @@
             mPackageName = packageName;
         }
 
+        @Override
         public String getPackage(File manifestFile) {
             return mPackageName;
         }
@@ -44,6 +45,7 @@
             return 0;
         }
 
+        @Override
         public String getVersionName(File manifestFile) {
             return "1.0";
         }
diff --git a/builder/src/test/java/com/android/builder/resources/BaseTestCase.java b/builder/src/test/java/com/android/builder/resources/BaseTestCase.java
index ac9f788..b536ca7 100644
--- a/builder/src/test/java/com/android/builder/resources/BaseTestCase.java
+++ b/builder/src/test/java/com/android/builder/resources/BaseTestCase.java
@@ -16,10 +16,12 @@
 
 package com.android.builder.resources;
 
+import com.google.common.collect.ListMultimap;
 import junit.framework.TestCase;
 
 import java.io.File;
-import java.util.Map;
+import java.io.IOException;
+import java.util.List;
 
 public abstract class BaseTestCase extends TestCase {
 
@@ -31,11 +33,48 @@
         return root;
     }
 
-    protected void verifyResources(ResourceSet resourceSet, String... resourceKeys) {
-        Map<String, Resource> map = resourceSet.getResourceMap();
+    protected static File getCanonicalRoot(String name) throws IOException {
+        File root = getRoot(name);
+        return root.getCanonicalFile();
+    }
 
-        for (String res : resourceKeys) {
-            assertNotNull("resource '" + res + "' is missing!", map.get(res));
+    protected void verifyResourceExists(ResourceMap resourceMap, String... resourceKeys) {
+        ListMultimap<String, Resource> map = resourceMap.getResourceMap();
+
+        for (String resKey : resourceKeys) {
+            List<Resource> resources = map.get(resKey);
+            assertTrue("resource '" + resKey + "' is missing!", resources.size() > 0);
+        }
+    }
+
+    /**
+     * Compares two resource maps.
+     *
+     * if <var>fullCompare</var> is <code>true</code> then it'll make sure the multimaps contains
+     * the same number of items, otherwise it'll only checks that each resource key is present
+     * in both maps.
+     *
+     * @param resourceMap1
+     * @param resourceMap2
+     * @param fullCompare
+     */
+    protected void compareResourceMaps(ResourceMap resourceMap1, ResourceMap resourceMap2,
+                                       boolean fullCompare) {
+        assertEquals(resourceMap1.size(), resourceMap2.size());
+
+        // compare the resources are all the same
+        ListMultimap<String, Resource> map1 = resourceMap1.getResourceMap();
+        ListMultimap<String, Resource> map2 = resourceMap2.getResourceMap();
+        for (String key : map1.keySet()) {
+            List<Resource> items1 = map1.get(key);
+            List<Resource> items2 = map2.get(key);
+            if (fullCompare) {
+                assertEquals("Wrong size for " + key, items1.size(), items2.size());
+            } else {
+                boolean map1HasItem = items1.size() > 0;
+                boolean map2HasItem = items2.size() > 0;
+                assertEquals("resource " + key + " missing from one map", map1HasItem, map2HasItem);
+            }
         }
     }
 }
diff --git a/builder/src/test/java/com/android/builder/resources/NoteUtilsTest.java b/builder/src/test/java/com/android/builder/resources/NoteUtilsTest.java
new file mode 100644
index 0000000..1bcd083
--- /dev/null
+++ b/builder/src/test/java/com/android/builder/resources/NoteUtilsTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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 org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+public class NoteUtilsTest extends TestCase {
+
+    public void testBasicAttributes() throws Exception {
+        Document document = createDocument();
+
+        // create two nodes
+        Node node1 = document.createElement("N1");
+        Node node2 = document.createElement("N2");
+
+        NodeUtils.addAttribute(document, node1, null, "foo", "bar");
+        NodeUtils.addAttribute(document, node2, null, "foo", "bar");
+        assertTrue(NodeUtils.compareAttributes(node1.getAttributes(), node2.getAttributes()));
+
+        NodeUtils.addAttribute(document, node1, null, "foo2", "bar2");
+        assertFalse(NodeUtils.compareAttributes(node1.getAttributes(), node2.getAttributes()));
+
+        NodeUtils.addAttribute(document, node2, null, "foo2", "bar");
+        assertFalse(NodeUtils.compareAttributes(node1.getAttributes(), node2.getAttributes()));
+    }
+
+    public void testNamespaceAttributes() throws Exception {
+        Document document = createDocument();
+
+        // create two nodes
+        Node node1 = document.createElement("N1");
+        Node node2 = document.createElement("N2");
+
+        NodeUtils.addAttribute(document, node1, "http://some.uri/", "foo", "bar");
+        NodeUtils.addAttribute(document, node2, "http://some.uri/", "foo", "bar");
+        assertTrue(NodeUtils.compareAttributes(node1.getAttributes(), node2.getAttributes()));
+
+        NodeUtils.addAttribute(document, node1, "http://some.uri/", "foo2", "bar2");
+        NodeUtils.addAttribute(document, node2, "http://some.other.uri/", "foo2", "bar2");
+        assertFalse(NodeUtils.compareAttributes(node1.getAttributes(), node2.getAttributes()));
+    }
+
+    public void testNodesWithChildrenNodes() throws Exception {
+        Document document = createDocument();
+
+        // create two nodes
+        Node node1 = document.createElement("some-node");
+        Node node2 = document.createElement("some-node");
+
+        Node child1a = document.createElement("child1");
+        Node child1b = document.createElement("child2");
+        node1.appendChild(child1a).appendChild(child1b);
+
+        Node child2a = document.createElement("child1");
+        Node child2b = document.createElement("child2");
+        node2.appendChild(child2a).appendChild(child2b);
+
+        assertTrue(NodeUtils.compareElementNode(node1, node2));
+    }
+
+    public void testAdoptNode() throws Exception {
+        Document document = createDocument();
+        Node rootNode = document.createElement("root");
+        document.appendChild(rootNode);
+
+        // create a single s
+        Node node = document.createElement("some-node");
+
+        // add some children
+        Node child1 = document.createElement("child1");
+        Node child2 = document.createElement("child2");
+        node.appendChild(child1).appendChild(child2);
+
+        // add some attributes
+        NodeUtils.addAttribute(document, node, null, "foo", "bar");
+        NodeUtils.addAttribute(document, node, "http://some.uri", "foo2", "bar2");
+        NodeUtils.addAttribute(document, child1, "http://some.other.uri", "blah", "test");
+        NodeUtils.addAttribute(document, child2, "http://another.uri", "blah", "test");
+
+        // create the other document to receive the adopted node. It must have a root node.
+        Document document2 = createDocument();
+        rootNode = document2.createElement("root");
+        document2.appendChild(rootNode);
+
+        assertTrue(NodeUtils.compareElementNode(node, NodeUtils.adoptNode(document2, node)));
+    }
+
+    private static Document createDocument() throws ParserConfigurationException {
+        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+        factory.setNamespaceAware(true);
+        factory.setValidating(false);
+        factory.setIgnoringComments(true);
+        DocumentBuilder builder;
+
+        builder = factory.newDocumentBuilder();
+        return builder.newDocument();
+    }
+}
diff --git a/builder/src/test/java/com/android/builder/resources/ResourceMergerTest.java b/builder/src/test/java/com/android/builder/resources/ResourceMergerTest.java
index 6b856e2..c26f47d 100644
--- a/builder/src/test/java/com/android/builder/resources/ResourceMergerTest.java
+++ b/builder/src/test/java/com/android/builder/resources/ResourceMergerTest.java
@@ -16,26 +16,38 @@
 
 package com.android.builder.resources;
 
+import com.android.resources.ResourceFolderType;
+import com.android.resources.ResourceType;
+import com.google.common.base.Charsets;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Maps;
 import com.google.common.io.Files;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
 
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
 import java.io.File;
 import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 
 public class ResourceMergerTest extends BaseTestCase {
 
-    private static ResourceSet sMergedSet = null;
+    private static ResourceMerger sResourceMerger = null;
 
     public void testMergeByCount() throws Exception {
-        ResourceSet mergedSet = getMergedSet();
+        ResourceMerger merger = getResourceMerger();
 
-        assertEquals(25, mergedSet.getSize());
+        assertEquals(25, merger.size());
     }
 
     public void testMergedResourcesByName() throws Exception {
-        ResourceSet mergedSet = getMergedSet();
+        ResourceMerger merger = getResourceMerger();
 
-        verifyResources(mergedSet,
+        verifyResourceExists(merger,
                 "drawable/icon",
                 "drawable-ldpi/icon",
                 "drawable/icon2",
@@ -64,73 +76,712 @@
     }
 
     public void testReplacedLayout() throws Exception {
-        ResourceSet mergedSet = getMergedSet();
+        ResourceMerger merger = getResourceMerger();
+        ListMultimap<String, Resource> mergedMap = merger.getResourceMap();
 
-        Resource mainLayout = mergedSet.getResourceMap().get("layout/main");
+        List<Resource> values = mergedMap.get("layout/main");
+
+        // the overlay means there's 2 versions of this resource.
+        assertEquals(2, values.size());
+        Resource mainLayout = values.get(1);
+
         ResourceFile sourceFile = mainLayout.getSource();
         assertTrue(sourceFile.getFile().getAbsolutePath().endsWith("overlay/layout/main.xml"));
     }
 
     public void testReplacedAlias() throws Exception {
-        ResourceSet mergedSet = getMergedSet();
+        ResourceMerger merger = getResourceMerger();
+        ListMultimap<String, Resource> mergedMap = merger.getResourceMap();
 
-        Resource layout = mergedSet.getResourceMap().get("layout/alias_replaced_by_file");
+
+        List<Resource> values = mergedMap.get("layout/alias_replaced_by_file");
+
+        // the overlay means there's 2 versions of this resource.
+        assertEquals(2, values.size());
+        Resource layout = values.get(1);
+
         // since it's replaced by a file, there's no node.
         assertNull(layout.getValue());
     }
 
     public void testReplacedFile() throws Exception {
-        ResourceSet mergedSet = getMergedSet();
+        ResourceMerger merger = getResourceMerger();
+        ListMultimap<String, Resource> mergedMap = merger.getResourceMap();
 
-        Resource layout = mergedSet.getResourceMap().get("layout/file_replaced_by_alias");
+        List<Resource> values = mergedMap.get("layout/file_replaced_by_alias");
+
+        // the overlay means there's 2 versions of this resource.
+        assertEquals(2, values.size());
+        Resource layout = values.get(1);
+
         // since it's replaced by a file, there's no node.
         assertNotNull(layout.getValue());
     }
 
     public void testMergeWrite() throws Exception {
-        ResourceSet mergedSet = getMergedSet();
+        ResourceMerger merger = getResourceMerger();
 
-        File folder = getWrittenSet();
+        File folder = getWrittenResources();
 
-        ResourceSet writtenSet = new ResourceSet();
+        ResourceSet writtenSet = new ResourceSet("unused");
         writtenSet.addSource(folder);
+        writtenSet.loadFromFiles();
 
-        // compare the two sets.
-        assertEquals(mergedSet.getSize(), writtenSet.getSize());
+        // compare the two maps, but not using the full map as the set loaded from the output
+        // won't contains all versions of each Resource item.
+        compareResourceMaps(merger, writtenSet, false /*full compare*/);
+    }
 
-        // compare the resources are all the same
-        Map<String, Resource> map = writtenSet.getResourceMap();
-        for (Resource item : mergedSet.getResources()) {
-            assertNotNull(map.get(item.getKey()));
+    public void testMergeBlob() throws Exception {
+        ResourceMerger merger = getResourceMerger();
+
+        File folder = Files.createTempDir();
+        merger.writeBlobTo(folder);
+
+        ResourceMerger loadedMerger = new ResourceMerger();
+        loadedMerger.loadFromBlob(folder);
+
+        compareResourceMaps(merger, loadedMerger, true /*full compare*/);
+    }
+
+    /**
+     * Tests the path replacement in the merger.xml file loaded from testData/
+     * @throws Exception
+     */
+    public void testLoadingTestPathReplacement() throws Exception {
+        File root = getRoot("baseMerge");
+        File fakeRoot = getMergedBlobFolder(root);
+
+        ResourceMerger resourceMerger = new ResourceMerger();
+        resourceMerger.loadFromBlob(fakeRoot);
+
+        List<ResourceSet> sets = resourceMerger.getResourceSets();
+        for (ResourceSet set : sets) {
+            List<File> sourceFiles = set.getSourceFiles();
+
+            // there should only be one
+            assertEquals(1, sourceFiles.size());
+
+            File sourceFile = sourceFiles.get(0);
+            assertTrue(String.format("File %s is located in %s", sourceFile, root),
+                    sourceFile.getAbsolutePath().startsWith(root.getAbsolutePath()));
         }
     }
 
-    private static ResourceSet getMergedSet() throws DuplicateResourceException {
-        if (sMergedSet == null) {
+    public void testUpdateWithBasicFiles() throws Exception {
+        File root = getIncMergeRoot("basicFiles");
+        File fakeRoot = getMergedBlobFolder(root);
+        ResourceMerger resourceMerger = new ResourceMerger();
+        resourceMerger.loadFromBlob(fakeRoot);
+
+        List<ResourceSet> sets = resourceMerger.getResourceSets();
+        assertEquals(2, sets.size());
+
+        // ----------------
+        // first set is the main one, no change here
+        ResourceSet mainSet = sets.get(0);
+        File mainBase = new File(root, "main");
+        File mainDrawable = new File(mainBase, "drawable");
+        File mainDrawableLdpi = new File(mainBase, "drawable-ldpi");
+
+        // touched/removed files:
+        File mainDrawableTouched = new File(mainDrawable, "touched.png");
+        mainSet.updateWith(mainBase, mainDrawableTouched, FileStatus.CHANGED);
+
+        File mainDrawableRemoved = new File(mainDrawable, "removed.png");
+        mainSet.updateWith(mainBase, mainDrawableRemoved, FileStatus.REMOVED);
+
+        File mainDrawableLdpiRemoved = new File(mainDrawableLdpi, "removed.png");
+        mainSet.updateWith(mainBase, mainDrawableLdpiRemoved, FileStatus.REMOVED);
+
+        // ----------------
+        // second set is the overlay one
+        ResourceSet overlaySet = sets.get(1);
+        File overlayBase = new File(root, "overlay");
+        File overlayDrawable = new File(overlayBase, "drawable");
+        File overlayDrawableHdpi = new File(overlayBase, "drawable-hdpi");
+
+        // new/removed files:
+        File overlayDrawableNewOverlay = new File(overlayDrawable, "new_overlay.png");
+        overlaySet.updateWith(overlayBase, overlayDrawableNewOverlay, FileStatus.NEW);
+
+        File overlayDrawableRemovedOverlay = new File(overlayDrawable, "removed_overlay.png");
+        overlaySet.updateWith(overlayBase, overlayDrawableRemovedOverlay, FileStatus.REMOVED);
+
+        File overlayDrawableHdpiNewAlternate = new File(overlayDrawableHdpi, "new_alternate.png");
+        overlaySet.updateWith(overlayBase, overlayDrawableHdpiNewAlternate, FileStatus.NEW);
+
+        // validate for duplicates
+        resourceMerger.validateResourceSets();
+
+        // check the content.
+        ListMultimap<String, Resource> mergedMap = resourceMerger.getResourceMap();
+
+        // check unchanged file is WRITTEN
+        List<Resource> drawableUntouched = mergedMap.get("drawable/untouched");
+        assertEquals(1, drawableUntouched.size());
+        assertTrue(drawableUntouched.get(0).isWritten());
+        assertFalse(drawableUntouched.get(0).isTouched());
+        assertFalse(drawableUntouched.get(0).isRemoved());
+
+        // check replaced file is TOUCHED
+        List<Resource> drawableTouched = mergedMap.get("drawable/touched");
+        assertEquals(1, drawableTouched.size());
+        assertTrue(drawableTouched.get(0).isWritten());
+        assertTrue(drawableTouched.get(0).isTouched());
+        assertFalse(drawableTouched.get(0).isRemoved());
+
+        // check removed file is REMOVED
+        List<Resource> drawableRemoved = mergedMap.get("drawable/removed");
+        assertEquals(1, drawableRemoved.size());
+        assertTrue(drawableRemoved.get(0).isWritten());
+        assertTrue(drawableRemoved.get(0).isRemoved());
+
+        // check new overlay: two objects, last one is TOUCHED
+        List<Resource> drawableNewOverlay = mergedMap.get("drawable/new_overlay");
+        assertEquals(2, drawableNewOverlay.size());
+        Resource newOverlay = drawableNewOverlay.get(1);
+        assertEquals(overlayDrawableNewOverlay, newOverlay.getSource().getFile());
+        assertFalse(newOverlay.isWritten());
+        assertTrue(newOverlay.isTouched());
+
+        // check new alternate: one objects, last one is TOUCHED
+        List<Resource> drawableHdpiNewAlternate = mergedMap.get("drawable-hdpi/new_alternate");
+        assertEquals(1, drawableHdpiNewAlternate.size());
+        Resource newAlternate = drawableHdpiNewAlternate.get(0);
+        assertEquals(overlayDrawableHdpiNewAlternate, newAlternate.getSource().getFile());
+        assertFalse(newAlternate.isWritten());
+        assertTrue(newAlternate.isTouched());
+
+        // write and check the result of writeResourceFolder
+        // copy the current resOut which serves as pre incremental update state.
+        File resFolder = getFolderCopy(new File(root, "resOut"));
+
+        // write the content of the resource merger.
+        resourceMerger.writeResourceFolder(resFolder);
+
+        // Check the content.
+        checkImageColor(new File(resFolder, "drawable" + File.separator + "touched.png"),
+                (int) 0xFF00FF00);
+        checkImageColor(new File(resFolder, "drawable" + File.separator + "untouched.png"),
+                (int) 0xFF00FF00);
+        checkImageColor(new File(resFolder, "drawable" + File.separator + "new_overlay.png"),
+                (int) 0xFF00FF00);
+        checkImageColor(new File(resFolder, "drawable" + File.separator + "removed_overlay.png"),
+                (int) 0xFF00FF00);
+        checkImageColor(new File(resFolder, "drawable-hdpi" + File.separator + "new_alternate.png"),
+                (int) 0xFF00FF00);
+        assertFalse(new File(resFolder, "drawable-ldpi" + File.separator + "removed.png").isFile());
+    }
+
+    public void testUpdateWithBasicValues() throws Exception {
+        File root = getIncMergeRoot("basicValues");
+        File fakeRoot = getMergedBlobFolder(root);
+        ResourceMerger resourceMerger = new ResourceMerger();
+        resourceMerger.loadFromBlob(fakeRoot);
+
+        List<ResourceSet> sets = resourceMerger.getResourceSets();
+        assertEquals(2, sets.size());
+
+        // ----------------
+        // first set is the main one, no change here
+        ResourceSet mainSet = sets.get(0);
+        File mainBase = new File(root, "main");
+        File mainValues = new File(mainBase, "values");
+        File mainValuesEn = new File(mainBase, "values-en");
+
+        // touched file:
+        File mainValuesTouched = new File(mainValues, "values.xml");
+        mainSet.updateWith(mainBase, mainValuesTouched, FileStatus.CHANGED);
+
+        // removed files
+        File mainValuesEnRemoved = new File(mainValuesEn, "values.xml");
+        mainSet.updateWith(mainBase, mainValuesEnRemoved, FileStatus.REMOVED);
+
+        // ----------------
+        // second set is the overlay one
+        ResourceSet overlaySet = sets.get(1);
+        File overlayBase = new File(root, "overlay");
+        File overlayValues = new File(overlayBase, "values");
+        File overlayValuesFr = new File(overlayBase, "values-fr");
+
+        // new files:
+        File overlayValuesNew = new File(overlayValues, "values.xml");
+        overlaySet.updateWith(overlayBase, overlayValuesNew, FileStatus.NEW);
+        File overlayValuesFrNew = new File(overlayValuesFr, "values.xml");
+        overlaySet.updateWith(overlayBase, overlayValuesFrNew, FileStatus.NEW);
+
+        // validate for duplicates
+        resourceMerger.validateResourceSets();
+
+        // check the content.
+        ListMultimap<String, Resource> mergedMap = resourceMerger.getResourceMap();
+
+        // check unchanged string is WRITTEN
+        List<Resource> valuesUntouched = mergedMap.get("string/untouched");
+        assertEquals(1, valuesUntouched.size());
+        assertTrue(valuesUntouched.get(0).isWritten());
+        assertFalse(valuesUntouched.get(0).isTouched());
+        assertFalse(valuesUntouched.get(0).isRemoved());
+
+        // check replaced file is TOUCHED
+        List<Resource> valuesTouched = mergedMap.get("string/touched");
+        assertEquals(1, valuesTouched.size());
+        assertTrue(valuesTouched.get(0).isWritten());
+        assertTrue(valuesTouched.get(0).isTouched());
+        assertFalse(valuesTouched.get(0).isRemoved());
+
+        // check removed file is REMOVED
+        List<Resource> valuesRemoved = mergedMap.get("string/removed");
+        assertEquals(1, valuesRemoved.size());
+        assertTrue(valuesRemoved.get(0).isWritten());
+        assertTrue(valuesRemoved.get(0).isRemoved());
+
+        valuesRemoved = mergedMap.get("string-en/removed");
+        assertEquals(1, valuesRemoved.size());
+        assertTrue(valuesRemoved.get(0).isWritten());
+        assertTrue(valuesRemoved.get(0).isRemoved());
+
+        // check new overlay: two objects, last one is TOUCHED
+        List<Resource> valuesNewOverlay = mergedMap.get("string/new_overlay");
+        assertEquals(2, valuesNewOverlay.size());
+        Resource newOverlay = valuesNewOverlay.get(1);
+        assertFalse(newOverlay.isWritten());
+        assertTrue(newOverlay.isTouched());
+
+        // check new alternate: one objects, last one is TOUCHED
+        List<Resource> valuesFrNewAlternate = mergedMap.get("string-fr/new_alternate");
+        assertEquals(1, valuesFrNewAlternate.size());
+        Resource newAlternate = valuesFrNewAlternate.get(0);
+        assertFalse(newAlternate.isWritten());
+        assertTrue(newAlternate.isTouched());
+
+        // write and check the result of writeResourceFolder
+        // copy the current resOut which serves as pre incremental update state.
+        File resFolder = getFolderCopy(new File(root, "resOut"));
+
+        // write the content of the resource merger.
+        resourceMerger.writeResourceFolder(resFolder);
+
+        // Check the content.
+        // values/values.xml
+        Map<String, String> map = quickStringOnlyValueFileParser(
+                new File(resFolder, "values" + File.separator + "values.xml"));
+        assertEquals("untouched", map.get("untouched"));
+        assertEquals("touched", map.get("touched"));
+        assertEquals("new_overlay", map.get("new_overlay"));
+
+        // values/values-fr.xml
+        map = quickStringOnlyValueFileParser(
+                new File(resFolder, "values-fr" + File.separator + "values.xml"));
+        assertEquals("new_alternate", map.get("new_alternate"));
+
+        // deleted values-en/values.xml
+        assertFalse(new File(resFolder, "values-en" + File.separator + "values.xml").isFile());
+    }
+
+    public void testUpdateWithBasicValues2() throws Exception {
+        File root = getIncMergeRoot("basicValues2");
+        File fakeRoot = getMergedBlobFolder(root);
+        ResourceMerger resourceMerger = new ResourceMerger();
+        resourceMerger.loadFromBlob(fakeRoot);
+
+        List<ResourceSet> sets = resourceMerger.getResourceSets();
+        assertEquals(2, sets.size());
+
+        // ----------------
+        // first set is the main one, no change here
+        ResourceSet mainSet = sets.get(0);
+        File mainBase = new File(root, "main");
+        File mainValues = new File(mainBase, "values");
+
+        // ----------------
+        // second set is the overlay one
+        ResourceSet overlaySet = sets.get(1);
+        File overlayBase = new File(root, "overlay");
+        File overlayValues = new File(overlayBase, "values");
+
+        // new files:
+        File overlayValuesNew = new File(overlayValues, "values.xml");
+        overlaySet.updateWith(overlayBase, overlayValuesNew, FileStatus.REMOVED);
+
+        // validate for duplicates
+        resourceMerger.validateResourceSets();
+
+        // check the content.
+        ListMultimap<String, Resource> mergedMap = resourceMerger.getResourceMap();
+
+        // check unchanged string is WRITTEN
+        List<Resource> valuesUntouched = mergedMap.get("string/untouched");
+        assertEquals(1, valuesUntouched.size());
+        assertTrue(valuesUntouched.get(0).isWritten());
+        assertFalse(valuesUntouched.get(0).isTouched());
+        assertFalse(valuesUntouched.get(0).isRemoved());
+
+        // check removed_overlay is present twice.
+        List<Resource> valuesRemovedOverlay = mergedMap.get("string/removed_overlay");
+        assertEquals(2, valuesRemovedOverlay.size());
+        // first is untouched
+        assertFalse(valuesRemovedOverlay.get(0).isWritten());
+        assertFalse(valuesRemovedOverlay.get(0).isTouched());
+        assertFalse(valuesRemovedOverlay.get(0).isRemoved());
+        // other is removed
+        assertTrue(valuesRemovedOverlay.get(1).isWritten());
+        assertFalse(valuesRemovedOverlay.get(1).isTouched());
+        assertTrue(valuesRemovedOverlay.get(1).isRemoved());
+
+        // write and check the result of writeResourceFolder
+        // copy the current resOut which serves as pre incremental update state.
+        File resFolder = getFolderCopy(new File(root, "resOut"));
+
+        // write the content of the resource merger.
+        resourceMerger.writeResourceFolder(resFolder);
+
+        // Check the content.
+        // values/values.xml
+        Map<String, String> map = quickStringOnlyValueFileParser(
+                new File(resFolder, "values" + File.separator + "values.xml"));
+        assertEquals("untouched", map.get("untouched"));
+        assertEquals("untouched", map.get("removed_overlay"));
+    }
+
+    public void testUpdateWithFilesVsValues() throws Exception {
+        File root = getIncMergeRoot("filesVsValues");
+        File fakeRoot = getMergedBlobFolder(root);
+        ResourceMerger resourceMerger = new ResourceMerger();
+        resourceMerger.loadFromBlob(fakeRoot);
+
+        List<ResourceSet> sets = resourceMerger.getResourceSets();
+        assertEquals(1, sets.size());
+
+        // ----------------
+        // Load the main set
+        ResourceSet mainSet = sets.get(0);
+        File mainBase = new File(root, "main");
+        File mainValues = new File(mainBase, ResourceFolderType.VALUES.getName());
+        File mainLayout = new File(mainBase, ResourceFolderType.LAYOUT.getName());
+
+        // touched file:
+        File mainValuesTouched = new File(mainValues, "values.xml");
+        mainSet.updateWith(mainBase, mainValuesTouched, FileStatus.CHANGED);
+
+        // new file:
+        File mainLayoutNew = new File(mainLayout, "alias_replaced_by_file.xml");
+        mainSet.updateWith(mainBase, mainLayoutNew, FileStatus.NEW);
+
+        // removed file
+        File mainLayoutRemoved = new File(mainLayout, "file_replaced_by_alias.xml");
+        mainSet.updateWith(mainBase, mainLayoutRemoved, FileStatus.REMOVED);
+
+        // validate for duplicates
+        resourceMerger.validateResourceSets();
+
+        // check the content.
+        ListMultimap<String, Resource> mergedMap = resourceMerger.getResourceMap();
+
+        // check layout/main is unchanged
+        List<Resource> layoutMain = mergedMap.get("layout/main");
+        assertEquals(1, layoutMain.size());
+        assertTrue(layoutMain.get(0).isWritten());
+        assertFalse(layoutMain.get(0).isTouched());
+        assertFalse(layoutMain.get(0).isRemoved());
+
+        // check file_replaced_by_alias has 2 version, 2nd is TOUCHED, and contains a Node
+        List<Resource> layoutReplacedByAlias = mergedMap.get("layout/file_replaced_by_alias");
+        assertEquals(2, layoutReplacedByAlias.size());
+        // 1st one is removed version, as it already existed in the item multimap
+        Resource replacedByAlias = layoutReplacedByAlias.get(0);
+        assertTrue(replacedByAlias.isWritten());
+        assertFalse(replacedByAlias.isTouched());
+        assertTrue(replacedByAlias.isRemoved());
+        assertNull(replacedByAlias.getValue());
+        assertEquals("file_replaced_by_alias.xml", replacedByAlias.getSource().getFile().getName());
+        // 2nd version is the new one
+        replacedByAlias = layoutReplacedByAlias.get(1);
+        assertFalse(replacedByAlias.isWritten());
+        assertTrue(replacedByAlias.isTouched());
+        assertFalse(replacedByAlias.isRemoved());
+        assertNotNull(replacedByAlias.getValue());
+        assertEquals("values.xml", replacedByAlias.getSource().getFile().getName());
+
+        // check alias_replaced_by_file has 2 version, 2nd is TOUCHED, and contains a Node
+        List<Resource> layoutReplacedByFile = mergedMap.get("layout/alias_replaced_by_file");
+        // 1st one is removed version, as it already existed in the item multimap
+        assertEquals(2, layoutReplacedByFile.size());
+        Resource replacedByFile = layoutReplacedByFile.get(0);
+        assertTrue(replacedByFile.isWritten());
+        assertFalse(replacedByFile.isTouched());
+        assertTrue(replacedByFile.isRemoved());
+        assertNotNull(replacedByFile.getValue());
+        assertEquals("values.xml", replacedByFile.getSource().getFile().getName());
+        // 2nd version is the new one
+        replacedByFile = layoutReplacedByFile.get(1);
+        assertFalse(replacedByFile.isWritten());
+        assertTrue(replacedByFile.isTouched());
+        assertFalse(replacedByFile.isRemoved());
+        assertNull(replacedByFile.getValue());
+        assertEquals("alias_replaced_by_file.xml", replacedByFile.getSource().getFile().getName());
+
+        // write and check the result of writeResourceFolder
+        // copy the current resOut which serves as pre incremental update state.
+        File resFolder = getFolderCopy(new File(root, "resOut"));
+
+        // write the content of the resource merger.
+        resourceMerger.writeResourceFolder(resFolder);
+
+        // deleted layout/file_replaced_by_alias.xml
+        assertFalse(new File(resFolder, "layout" + File.separator + "file_replaced_by_alias.xml")
+                .isFile());
+        // new file layout/alias_replaced_by_file.xml
+        assertTrue(new File(resFolder, "layout" + File.separator + "alias_replaced_by_file.xml")
+                .isFile());
+
+        // quick load of the values file
+        File valuesFile = new File(resFolder, "values" + File.separator + "values.xml");
+        assertTrue(valuesFile.isFile());
+        String content = Files.toString(valuesFile, Charsets.UTF_8);
+        assertTrue(content.contains("name=\"file_replaced_by_alias\""));
+        assertFalse(content.contains("name=\"alias_replaced_by_file\""));
+    }
+
+    public void testCheckValidUpdate() throws Exception {
+        // first merger
+        ResourceMerger merger1 = createMerger(new String[][] {
+                new String[] { "main",    "/main/res1", "/main/res2" },
+                new String[] { "overlay", "/overlay/res1", "/overlay/res2" },
+        });
+
+        // 2nd merger with different order source files in sets.
+        ResourceMerger merger2 = createMerger(new String[][] {
+                new String[] { "main",    "/main/res2", "/main/res1" },
+                new String[] { "overlay", "/overlay/res1", "/overlay/res2" },
+        });
+
+        assertTrue(merger1.checkValidUpdate(merger2.getResourceSets()));
+
+        // write merger1 on disk to test writing empty ResourceSets.
+        File folder = Files.createTempDir();
+        merger1.writeBlobTo(folder);
+
+        // reload it
+        ResourceMerger loadedMerger = new ResourceMerger();
+        loadedMerger.loadFromBlob(folder);
+
+        assertTrue(loadedMerger.checkValidUpdate(merger1.getResourceSets()));
+    }
+
+    public void testCheckValidUpdateFail() throws Exception {
+        // Test with removed overlay
+        ResourceMerger merger1 = createMerger(new String[][] {
+                new String[] { "main",    "/main/res1", "/main/res2" },
+                new String[] { "overlay", "/overlay/res1", "/overlay/res2" },
+        });
+
+        // 2nd merger with different order source files in sets.
+        ResourceMerger merger2 = createMerger(new String[][] {
+                new String[] { "main",    "/main/res2", "/main/res1" },
+        });
+
+        assertFalse(merger1.checkValidUpdate(merger2.getResourceSets()));
+
+        // Test with different overlays
+        merger1 = createMerger(new String[][] {
+                new String[] { "main",    "/main/res1", "/main/res2" },
+                new String[] { "overlay", "/overlay/res1", "/overlay/res2" },
+        });
+
+        // 2nd merger with different order source files in sets.
+        merger2 = createMerger(new String[][] {
+                new String[] { "main",    "/main/res2", "/main/res1" },
+                new String[] { "overlay2", "/overlay2/res1", "/overlay2/res2" },
+        });
+
+        assertFalse(merger1.checkValidUpdate(merger2.getResourceSets()));
+
+        // Test with different overlays
+        merger1 = createMerger(new String[][] {
+                new String[] { "main",    "/main/res1", "/main/res2" },
+                new String[] { "overlay1", "/overlay1/res1", "/overlay1/res2" },
+                new String[] { "overlay2", "/overlay2/res1", "/overlay2/res2" },
+        });
+
+        // 2nd merger with different order source files in sets.
+        merger2 = createMerger(new String[][] {
+                new String[] { "main",    "/main/res2", "/main/res1" },
+                new String[] { "overlay2", "/overlay2/res1", "/overlay2/res2" },
+                new String[] { "overlay1", "/overlay1/res1", "/overlay1/res2" },
+        });
+
+        assertFalse(merger1.checkValidUpdate(merger2.getResourceSets()));
+
+        // Test with different source files
+        merger1 = createMerger(new String[][] {
+                new String[] { "main",    "/main/res1", "/main/res2" },
+        });
+
+        // 2nd merger with different order source files in sets.
+        merger2 = createMerger(new String[][] {
+                new String[] { "main",    "/main/res1" },
+        });
+
+        assertFalse(merger1.checkValidUpdate(merger2.getResourceSets()));
+    }
+
+    private static ResourceMerger createMerger(String[][] data) {
+        ResourceMerger merger = new ResourceMerger();
+        for (String[] setData : data) {
+            ResourceSet set = new ResourceSet(setData[0]);
+            merger.addResourceSet(set);
+            for (int i = 1, n = setData.length; i < n; i++) {
+                set.addSource(new File(setData[i]));
+            }
+        }
+
+        return merger;
+    }
+
+    private static ResourceMerger getResourceMerger()
+            throws DuplicateResourceException, IOException {
+        if (sResourceMerger == null) {
             File root = getRoot("baseMerge");
 
             ResourceSet res = ResourceSetTest.getBaseResourceSet();
 
-            ResourceSet overlay = new ResourceSet();
+            ResourceSet overlay = new ResourceSet("overlay");
             overlay.addSource(new File(root, "overlay"));
+            overlay.loadFromFiles();
 
-            ResourceMerger merger = new ResourceMerger();
-            merger.addResourceSet(res);
-            merger.addResourceSet(overlay);
-
-            sMergedSet = merger.getMergedSet();
+            sResourceMerger = new ResourceMerger();
+            sResourceMerger.addResourceSet(res);
+            sResourceMerger.addResourceSet(overlay);
         }
 
-        return sMergedSet;
+        return sResourceMerger;
     }
 
-    private static File getWrittenSet() throws DuplicateResourceException, IOException {
-        ResourceSet mergedSet = getMergedSet();
+    private static File getWrittenResources() throws DuplicateResourceException, IOException {
+        ResourceMerger resourceMerger = getResourceMerger();
 
         File folder = Files.createTempDir();
 
-        mergedSet.writeTo(folder);
+        resourceMerger.writeResourceFolder(folder);
 
         return folder;
     }
+
+    /**
+     * Returns a folder containing a merger blob data for the given test data folder.
+     *
+     * This is to work around the fact that the merger blob data contains full path, but we don't
+     * know where this project is located on the drive. This rewrites the blob to contain the
+     * actual folder.
+     * (The blobs written in the test data contains placeholders for the path root and path
+     * separators)
+     *
+     * @param folder
+     * @return
+     * @throws IOException
+     */
+    private static File getMergedBlobFolder(File folder) throws IOException {
+        File originalMerger = new File(folder, ResourceMerger.FN_MERGER_XML);
+
+        String content = Files.toString(originalMerger, Charsets.UTF_8);
+
+        // search and replace $TOP$ with the root
+        content = content.replaceAll(
+                "\\$TOP\\$", folder.getAbsolutePath()).replaceAll("\\$SEP\\$", File.separator);
+
+        File tempFolder = Files.createTempDir();
+        Files.write(content, new File(tempFolder, ResourceMerger.FN_MERGER_XML), Charsets.UTF_8);
+
+        return tempFolder;
+    }
+
+    private File getIncMergeRoot(String name) throws IOException {
+        File root = getCanonicalRoot("incMergeData");
+        return new File(root, name);
+    }
+
+    private static File getFolderCopy(File folder) throws IOException {
+        File dest = Files.createTempDir();
+        copyFolder(folder, dest);
+        return dest;
+    }
+
+    private static void copyFolder(File from, File to) throws IOException {
+        if (from.isFile()) {
+            Files.copy(from, to);
+        } else if (from.isDirectory()) {
+            if (!to.exists()) {
+                to.mkdirs();
+            }
+
+            File[] children = from.listFiles();
+            if (children != null) {
+                for (File f : children) {
+                    copyFolder(f, new File(to, f.getName()));
+                }
+            }
+        }
+    }
+
+    private static void checkImageColor(File file, int expectedColor) throws IOException {
+        assertTrue("File '" + file.getAbsolutePath() + "' does not exist.", file.isFile());
+
+        BufferedImage image = ImageIO.read(file);
+        int rgb = image.getRGB(0, 0);
+        assertEquals(String.format("Expected: 0x%08X, actual: 0x%08X for file %s",
+                expectedColor, rgb, file),
+                expectedColor, rgb);
+    }
+
+    private static Map<String, String> quickStringOnlyValueFileParser(File file)
+            throws IOException {
+        Map<String, String> result = Maps.newHashMap();
+
+        Document document = ValueResourceParser.parseDocument(file);
+
+        // get the root node
+        Node rootNode = document.getDocumentElement();
+        if (rootNode == null) {
+            return Collections.emptyMap();
+        }
+
+        NodeList nodes = rootNode.getChildNodes();
+
+        for (int i = 0, n = nodes.getLength(); i < n; i++) {
+            Node node = nodes.item(i);
+
+            if (node.getNodeType() != Node.ELEMENT_NODE) {
+                continue;
+            }
+
+            ResourceType type = ValueResourceParser.getType(node);
+            if (type != ResourceType.STRING) {
+                throw new IllegalArgumentException("Only String resources supported.");
+            }
+            String name = ValueResourceParser.getName(node);
+
+            String value = null;
+
+            NodeList nodeList = node.getChildNodes();
+            nodeLoop: for (int ii = 0, nn = nodes.getLength(); ii < nn; ii++) {
+                Node subNode = nodeList.item(ii);
+
+                switch (subNode.getNodeType()) {
+                    case Node.COMMENT_NODE:
+                        break;
+                    case Node.TEXT_NODE:
+                        value = subNode.getNodeValue().trim(); // TODO: remove trim.
+                        break nodeLoop;
+                    case Node.ELEMENT_NODE:
+                        break;
+                }
+            }
+
+            result.put(name, value != null ? value : "");
+        }
+
+        return result;
+    }
 }
diff --git a/builder/src/test/java/com/android/builder/resources/ResourceSetTest.java b/builder/src/test/java/com/android/builder/resources/ResourceSetTest.java
index bcc7b88..9c0efa1 100644
--- a/builder/src/test/java/com/android/builder/resources/ResourceSetTest.java
+++ b/builder/src/test/java/com/android/builder/resources/ResourceSetTest.java
@@ -17,6 +17,7 @@
 package com.android.builder.resources;
 
 import java.io.File;
+import java.io.IOException;
 
 public class ResourceSetTest extends BaseTestCase {
 
@@ -24,13 +25,13 @@
 
     public void testBaseResourceSetByCount() throws Exception {
         ResourceSet resourceSet = getBaseResourceSet();
-        assertEquals(23, resourceSet.getSize());
+        assertEquals(23, resourceSet.size());
     }
 
     public void testBaseResourceSetByName() throws Exception {
         ResourceSet resourceSet = getBaseResourceSet();
 
-        verifyResources(resourceSet,
+        verifyResourceExists(resourceSet,
                 "drawable/icon",
                 "drawable/patch",
                 "raw/foo",
@@ -60,11 +61,12 @@
     public void testDupResourceSet() throws Exception {
         File root = getRoot("dupResourceSet");
 
-        ResourceSet set = new ResourceSet();
+        ResourceSet set = new ResourceSet("main");
         set.addSource(new File(root, "res1"));
+        set.addSource(new File(root, "res2"));
         boolean gotException = false;
         try {
-            set.addSource(new File(root, "res2"));
+            set.loadFromFiles();
         } catch (DuplicateResourceException e) {
             gotException = true;
         }
@@ -72,12 +74,13 @@
         assertTrue(gotException);
     }
 
-    static ResourceSet getBaseResourceSet() throws DuplicateResourceException {
+    static ResourceSet getBaseResourceSet() throws DuplicateResourceException, IOException {
         if (sBaseResourceSet == null) {
             File root = getRoot("baseResourceSet");
 
-            sBaseResourceSet = new ResourceSet();
+            sBaseResourceSet = new ResourceSet("main");
             sBaseResourceSet.addSource(root);
+            sBaseResourceSet.loadFromFiles();
         }
 
         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
index dbd31a5..022b5d7 100644
--- a/builder/src/test/java/com/android/builder/resources/ValueResourceParserTest.java
+++ b/builder/src/test/java/com/android/builder/resources/ValueResourceParserTest.java
@@ -19,7 +19,7 @@
 import com.google.common.collect.Maps;
 
 import java.io.File;
-import java.io.FileNotFoundException;
+import java.io.IOException;
 import java.util.List;
 import java.util.Map;
 
@@ -37,11 +37,9 @@
 
     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);
+        Map<String, Resource> resourceMap = Maps.newHashMapWithExpectedSize(resources.size());
+        for (Resource item : resources) {
+            resourceMap.put(item.getKey(), item);
         }
 
         String[] resourceNames = new String[] {
@@ -65,11 +63,11 @@
         };
 
         for (String name : resourceNames) {
-            assertNotNull(name, map.get(name));
+            assertNotNull(name, resourceMap.get(name));
         }
     }
 
-    private static List<Resource> getParsedResources() throws FileNotFoundException {
+    private static List<Resource> getParsedResources() throws IOException {
         if (sResources == null) {
             File root = getRoot("baseResourceSet");
             File values = new File(root, "values");
@@ -77,6 +75,9 @@
 
             ValueResourceParser parser = new ValueResourceParser(valuesXml);
             sResources = parser.parseFile();
+
+            // create a fake resource file to allow calling Resource.getKey()
+            new ResourceFile(valuesXml, sResources, "");
         }
 
         return sResources;
diff --git a/builder/src/test/resources/testData/baseMerge/merger.xml b/builder/src/test/resources/testData/baseMerge/merger.xml
new file mode 100644
index 0000000..18613c4
--- /dev/null
+++ b/builder/src/test/resources/testData/baseMerge/merger.xml
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="utf-8"?>
+<merger xmlns:ns1="urn:oasis:names:tc:xliff:document:1.2">
+    <resourceSet config="main">
+        <source path="$TOP$$SEP$baseResourceSet">
+            <file name="icon" path="$TOP$$SEP$baseResourceSet$SEP$drawable$SEP$icon.png" qualifiers="" type="drawable"/>
+            <file name="patch" path="$TOP$$SEP$baseResourceSet$SEP$drawable$SEP$patch.9.png" qualifiers="" type="drawable"/>
+            <file name="file_replaced_by_alias" path="$TOP$$SEP$baseResourceSet$SEP$layout$SEP$file_replaced_by_alias.xml" qualifiers="" type="layout"/>
+            <file name="main" path="$TOP$$SEP$baseResourceSet$SEP$layout$SEP$main.xml" qualifiers="" type="layout"/>
+            <file name="foo" path="$TOP$$SEP$baseResourceSet$SEP$raw$SEP$foo.dat" qualifiers="" type="raw"/>
+            <file path="$TOP$$SEP$baseResourceSet$SEP$values$SEP$values.xml" qualifiers="">
+                <dimen name="dimen">164dp</dimen>
+                <integer name="integer">75</integer>
+                <color name="color">#00000000</color>
+                <drawable name="color_drawable">#ffffffff</drawable>
+                <declare-styleable name="declare_styleable">
+
+                    <!-- ============== -->
+                    <!-- Generic styles -->
+                    <!-- ============== -->
+                    <eat-comment/>
+
+                    <!-- Default color of foreground imagery. -->
+                    <attr format="color" name="blah"/>
+
+                    <!-- Default color of foreground imagery on an inverted background. -->
+                    <attr name="android:colorForegroundInverse"/>
+
+                </declare-styleable>
+                <style name="style">
+                    <item name="android:singleLine">true</item>
+                    <item name="android:textAppearance">@style$SEP$TextAppearance.WindowTitle</item>
+                    <item name="android:shadowColor">#BB000000</item>
+                    <item name="android:shadowRadius">2.75</item>
+                </style>
+                <string name="styled_string">Forgot your username or password\?\nVisit<b>google.com$SEP$accounts$SEP$recovery</b>.</string>
+                <attr format="dimension" name="dimen_attr"/>
+                <string-array name="string_array" translatable="false">
+                    <item/>
+                    <!-- 0 -->
+                    <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>
+                <item name="alias_replaced_by_file" type="layout">@layout$SEP$ref </item>
+                <string name="basic_string">basic_string</string>
+                <string name="xliff_string"><ns1:g example="123" id="number">%1$s</ns1:g><ns1:g example="KB" id="unit">%2$s</ns1:g></string>
+                <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>
+                <item name="item_id" type="id"/>
+                <item name="layout_ref" type="layout">@layout$SEP$ref</item>
+                <drawable name="drawable_ref">@drawable$SEP$stat_notify_sync_anim0</drawable>
+                <attr format="string" name="string_attr"/>
+                <attr name="flag_attr">
+                    <flag name="normal" value="0"/>
+                    <flag name="bold" value="1"/>
+                    <flag name="italic" value="2"/>
+                </attr>
+            </file>
+        </source>
+    </resourceSet>
+    <resourceSet config="overlay">
+        <source path="$TOP$$SEP$baseMerge$SEP$overlay">
+            <file name="icon2" path="$TOP$$SEP$baseMerge$SEP$overlay$SEP$drawable$SEP$icon2.png" qualifiers="" type="drawable"/>
+            <file name="icon" path="$TOP$$SEP$baseMerge$SEP$overlay$SEP$drawable-ldpi$SEP$icon.png" qualifiers="ldpi" type="drawable"/>
+            <file name="alias_replaced_by_file" path="$TOP$$SEP$baseMerge$SEP$overlay$SEP$layout$SEP$alias_replaced_by_file.xml" qualifiers="" type="layout"/>
+            <file name="main" path="$TOP$$SEP$baseMerge$SEP$overlay$SEP$layout$SEP$main.xml" qualifiers="" type="layout"/>
+            <file path="$TOP$$SEP$baseMerge$SEP$overlay$SEP$values$SEP$values.xml" qualifiers="">
+                <color name="color">#FFFFFFFF</color>
+                <item name="file_replaced_by_alias" type="layout">@layout$SEP$ref</item>
+                <string name="basic_string">overlay_string</string>
+            </file>
+        </source>
+    </resourceSet>
+</merger>
diff --git a/builder/src/test/resources/testData/incMergeData/basicFiles/main/drawable/new_overlay.png b/builder/src/test/resources/testData/incMergeData/basicFiles/main/drawable/new_overlay.png
new file mode 100644
index 0000000..b55e544
--- /dev/null
+++ b/builder/src/test/resources/testData/incMergeData/basicFiles/main/drawable/new_overlay.png
Binary files differ
diff --git a/builder/src/test/resources/testData/incMergeData/basicFiles/main/drawable/removed_overlay.png b/builder/src/test/resources/testData/incMergeData/basicFiles/main/drawable/removed_overlay.png
new file mode 100644
index 0000000..47e1adf
--- /dev/null
+++ b/builder/src/test/resources/testData/incMergeData/basicFiles/main/drawable/removed_overlay.png
Binary files differ
diff --git a/builder/src/test/resources/testData/incMergeData/basicFiles/main/drawable/touched.png b/builder/src/test/resources/testData/incMergeData/basicFiles/main/drawable/touched.png
new file mode 100644
index 0000000..47e1adf
--- /dev/null
+++ b/builder/src/test/resources/testData/incMergeData/basicFiles/main/drawable/touched.png
Binary files differ
diff --git a/builder/src/test/resources/testData/incMergeData/basicFiles/main/drawable/untouched.png b/builder/src/test/resources/testData/incMergeData/basicFiles/main/drawable/untouched.png
new file mode 100644
index 0000000..47e1adf
--- /dev/null
+++ b/builder/src/test/resources/testData/incMergeData/basicFiles/main/drawable/untouched.png
Binary files differ
diff --git a/builder/src/test/resources/testData/incMergeData/basicFiles/merger.xml b/builder/src/test/resources/testData/incMergeData/basicFiles/merger.xml
new file mode 100644
index 0000000..6f4a3cb
--- /dev/null
+++ b/builder/src/test/resources/testData/incMergeData/basicFiles/merger.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<merger>
+    <resourceSet config="main">
+        <source path="$TOP$$SEP$main">
+            <file name="untouched" path="$TOP$$SEP$main$SEP$drawable$SEP$untouched.png" qualifiers="" type="drawable"/>
+            <file name="touched" path="$TOP$$SEP$main$SEP$drawable$SEP$touched.png" qualifiers="" type="drawable"/>
+            <file name="new_overlay" path="$TOP$$SEP$main$SEP$drawable$SEP$new_overlay.png" qualifiers="" type="drawable"/>
+            <file name="removed" path="$TOP$$SEP$main$SEP$drawable$SEP$removed.png" qualifiers="" type="drawable"/>
+            <file name="removed" path="$TOP$$SEP$main$SEP$drawable-ldpi$SEP$removed.png" qualifiers="ldpi" type="drawable"/>
+            <file name="removed_overlay" path="$TOP$$SEP$main$SEP$drawable$SEP$removed_overlay.png" qualifiers="" type="drawable"/>
+        </source>
+    </resourceSet>
+    <resourceSet config="overlay">
+        <source path="$TOP$$SEP$overlay">
+            <file name="removed_overlay" path="$TOP$$SEP$overlay$SEP$drawable$SEP$removed_overlay.png" qualifiers="" type="drawable"/>
+        </source>
+    </resourceSet>
+</merger>
diff --git a/builder/src/test/resources/testData/incMergeData/basicFiles/new_overlay.png b/builder/src/test/resources/testData/incMergeData/basicFiles/new_overlay.png
new file mode 100644
index 0000000..b55e544
--- /dev/null
+++ b/builder/src/test/resources/testData/incMergeData/basicFiles/new_overlay.png
Binary files differ
diff --git a/builder/src/test/resources/testData/incMergeData/basicFiles/overlay/drawable-hdpi/new_alternate.png b/builder/src/test/resources/testData/incMergeData/basicFiles/overlay/drawable-hdpi/new_alternate.png
new file mode 100644
index 0000000..47e1adf
--- /dev/null
+++ b/builder/src/test/resources/testData/incMergeData/basicFiles/overlay/drawable-hdpi/new_alternate.png
Binary files differ
diff --git a/builder/src/test/resources/testData/incMergeData/basicFiles/overlay/drawable/new_overlay.png b/builder/src/test/resources/testData/incMergeData/basicFiles/overlay/drawable/new_overlay.png
new file mode 100644
index 0000000..47e1adf
--- /dev/null
+++ b/builder/src/test/resources/testData/incMergeData/basicFiles/overlay/drawable/new_overlay.png
Binary files differ
diff --git a/builder/src/test/resources/testData/incMergeData/basicFiles/resOut/drawable-ldpi/removed.png b/builder/src/test/resources/testData/incMergeData/basicFiles/resOut/drawable-ldpi/removed.png
new file mode 100644
index 0000000..b55e544
--- /dev/null
+++ b/builder/src/test/resources/testData/incMergeData/basicFiles/resOut/drawable-ldpi/removed.png
Binary files differ
diff --git a/builder/src/test/resources/testData/incMergeData/basicFiles/resOut/drawable/new_overlay.png b/builder/src/test/resources/testData/incMergeData/basicFiles/resOut/drawable/new_overlay.png
new file mode 100644
index 0000000..b55e544
--- /dev/null
+++ b/builder/src/test/resources/testData/incMergeData/basicFiles/resOut/drawable/new_overlay.png
Binary files differ
diff --git a/builder/src/test/resources/testData/incMergeData/basicFiles/resOut/drawable/removed_overlay.png b/builder/src/test/resources/testData/incMergeData/basicFiles/resOut/drawable/removed_overlay.png
new file mode 100644
index 0000000..b55e544
--- /dev/null
+++ b/builder/src/test/resources/testData/incMergeData/basicFiles/resOut/drawable/removed_overlay.png
Binary files differ
diff --git a/builder/src/test/resources/testData/incMergeData/basicFiles/resOut/drawable/touched.png b/builder/src/test/resources/testData/incMergeData/basicFiles/resOut/drawable/touched.png
new file mode 100644
index 0000000..b55e544
--- /dev/null
+++ b/builder/src/test/resources/testData/incMergeData/basicFiles/resOut/drawable/touched.png
Binary files differ
diff --git a/builder/src/test/resources/testData/incMergeData/basicFiles/resOut/drawable/untouched.png b/builder/src/test/resources/testData/incMergeData/basicFiles/resOut/drawable/untouched.png
new file mode 100644
index 0000000..47e1adf
--- /dev/null
+++ b/builder/src/test/resources/testData/incMergeData/basicFiles/resOut/drawable/untouched.png
Binary files differ
diff --git a/builder/src/test/resources/testData/incMergeData/basicValues/main/values/values.xml b/builder/src/test/resources/testData/incMergeData/basicValues/main/values/values.xml
new file mode 100644
index 0000000..32c8031
--- /dev/null
+++ b/builder/src/test/resources/testData/incMergeData/basicValues/main/values/values.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="untouched">untouched</string>
+    <string name="touched">touched</string>
+    <string name="new_overlay">old_overlay</string>
+</resources>
diff --git a/builder/src/test/resources/testData/incMergeData/basicValues/merger.xml b/builder/src/test/resources/testData/incMergeData/basicValues/merger.xml
new file mode 100644
index 0000000..79980b2
--- /dev/null
+++ b/builder/src/test/resources/testData/incMergeData/basicValues/merger.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<merger>
+    <resourceSet config="main">
+        <source path="$TOP$$SEP$main">
+            <file path="$TOP$$SEP$main$SEP$values$SEP$values.xml" qualifiers="">
+                <string name="untouched">untouched</string>
+                <string name="touched">untouched</string>
+                <string name="removed">removed</string>
+                <string name="new_overlay">old_overlay</string>
+            </file>
+            <file path="$TOP$$SEP$main$SEP$values-en$SEP$values.xml" qualifiers="en">
+                <string name="removed">removed</string>
+            </file>
+        </source>
+    </resourceSet>
+    <resourceSet config="overlay">
+        <source path="$TOP$$SEP$overlay" />
+    </resourceSet>
+</merger>
diff --git a/builder/src/test/resources/testData/incMergeData/basicValues/overlay/values-fr/values.xml b/builder/src/test/resources/testData/incMergeData/basicValues/overlay/values-fr/values.xml
new file mode 100644
index 0000000..dcfcbdd
--- /dev/null
+++ b/builder/src/test/resources/testData/incMergeData/basicValues/overlay/values-fr/values.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="new_alternate">new_alternate</string>
+</resources>
diff --git a/builder/src/test/resources/testData/incMergeData/basicValues/overlay/values/values.xml b/builder/src/test/resources/testData/incMergeData/basicValues/overlay/values/values.xml
new file mode 100644
index 0000000..78e296c
--- /dev/null
+++ b/builder/src/test/resources/testData/incMergeData/basicValues/overlay/values/values.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="new_overlay">new_overlay</string>
+</resources>
diff --git a/builder/src/test/resources/testData/incMergeData/basicValues/resOut/values-en/values.xml b/builder/src/test/resources/testData/incMergeData/basicValues/resOut/values-en/values.xml
new file mode 100644
index 0000000..b6be59a
--- /dev/null
+++ b/builder/src/test/resources/testData/incMergeData/basicValues/resOut/values-en/values.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="removed">removed</string>
+</resources>
diff --git a/builder/src/test/resources/testData/incMergeData/basicValues/resOut/values/values.xml b/builder/src/test/resources/testData/incMergeData/basicValues/resOut/values/values.xml
new file mode 100644
index 0000000..a3ccf6b
--- /dev/null
+++ b/builder/src/test/resources/testData/incMergeData/basicValues/resOut/values/values.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="untouched">untouched</string>
+    <string name="touched">untouched</string>
+    <string name="removed">removed</string>
+    <string name="new_overlay">old_overlay</string>
+</resources>
diff --git a/builder/src/test/resources/testData/incMergeData/basicValues2/main/values/values.xml b/builder/src/test/resources/testData/incMergeData/basicValues2/main/values/values.xml
new file mode 100644
index 0000000..7a0a153
--- /dev/null
+++ b/builder/src/test/resources/testData/incMergeData/basicValues2/main/values/values.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="untouched">untouched</string>
+    <string name="removed_overlay">untouched</string>
+</resources>
diff --git a/builder/src/test/resources/testData/incMergeData/basicValues2/merger.xml b/builder/src/test/resources/testData/incMergeData/basicValues2/merger.xml
new file mode 100644
index 0000000..33f4356
--- /dev/null
+++ b/builder/src/test/resources/testData/incMergeData/basicValues2/merger.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<merger>
+    <resourceSet config="main">
+        <source path="$TOP$$SEP$main">
+            <file path="$TOP$$SEP$main$SEP$values$SEP$values.xml" qualifiers="">
+                <string name="untouched">untouched</string>
+                <string name="removed_overlay">untouched</string>
+            </file>
+        </source>
+    </resourceSet>
+    <resourceSet config="overlay">
+        <source path="$TOP$$SEP$overlay">
+            <file path="$TOP$$SEP$overlay$SEP$values$SEP$values.xml" qualifiers="">
+                <string name="removed_overlay">overlay</string>
+            </file>
+        </source>
+    </resourceSet>
+</merger>
diff --git a/builder/src/test/resources/testData/incMergeData/basicValues2/resOut/values/values.xml b/builder/src/test/resources/testData/incMergeData/basicValues2/resOut/values/values.xml
new file mode 100644
index 0000000..a0e41d1
--- /dev/null
+++ b/builder/src/test/resources/testData/incMergeData/basicValues2/resOut/values/values.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="untouched">untouched</string>
+    <string name="removed_overlay">overlay</string>
+</resources>
diff --git a/builder/src/test/resources/testData/incMergeData/filesVsValues/main/layout/alias_replaced_by_file.xml b/builder/src/test/resources/testData/incMergeData/filesVsValues/main/layout/alias_replaced_by_file.xml
new file mode 100644
index 0000000..b199751
--- /dev/null
+++ b/builder/src/test/resources/testData/incMergeData/filesVsValues/main/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/incMergeData/filesVsValues/main/layout/main.xml b/builder/src/test/resources/testData/incMergeData/filesVsValues/main/layout/main.xml
new file mode 100644
index 0000000..b199751
--- /dev/null
+++ b/builder/src/test/resources/testData/incMergeData/filesVsValues/main/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/incMergeData/filesVsValues/main/values/values.xml b/builder/src/test/resources/testData/incMergeData/filesVsValues/main/values/values.xml
new file mode 100644
index 0000000..028b828
--- /dev/null
+++ b/builder/src/test/resources/testData/incMergeData/filesVsValues/main/values/values.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <item type="layout" name="file_replaced_by_alias">@layout/main</item>
+</resources>
+
diff --git a/builder/src/test/resources/testData/incMergeData/filesVsValues/merger.xml b/builder/src/test/resources/testData/incMergeData/filesVsValues/merger.xml
new file mode 100644
index 0000000..c2cd240
--- /dev/null
+++ b/builder/src/test/resources/testData/incMergeData/filesVsValues/merger.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<merger>
+    <resourceSet config="main">
+        <source path="$TOP$$SEP$main">
+            <file name="main" path="$TOP$$SEP$main$SEP$layout$SEP$main.xml" qualifiers="" type="layout"/>
+            <file name="file_replaced_by_alias" path="$TOP$$SEP$main$SEP$layout$SEP$file_replaced_by_alias.xml" qualifiers="" type="layout"/>
+            <file path="$TOP$$SEP$main$SEP$values$SEP$values.xml" qualifiers="">
+                <item type="layout" name="alias_replaced_by_file">@layout/main</item>
+            </file>
+        </source>
+    </resourceSet>
+</merger>
diff --git a/builder/src/test/resources/testData/incMergeData/filesVsValues/resOut/layout/file_replaced_by_alias.xml b/builder/src/test/resources/testData/incMergeData/filesVsValues/resOut/layout/file_replaced_by_alias.xml
new file mode 100644
index 0000000..b199751
--- /dev/null
+++ b/builder/src/test/resources/testData/incMergeData/filesVsValues/resOut/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/incMergeData/filesVsValues/resOut/layout/main.xml b/builder/src/test/resources/testData/incMergeData/filesVsValues/resOut/layout/main.xml
new file mode 100644
index 0000000..b199751
--- /dev/null
+++ b/builder/src/test/resources/testData/incMergeData/filesVsValues/resOut/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/incMergeData/filesVsValues/resOut/values/values.xml b/builder/src/test/resources/testData/incMergeData/filesVsValues/resOut/values/values.xml
new file mode 100644
index 0000000..1fbc46e
--- /dev/null
+++ b/builder/src/test/resources/testData/incMergeData/filesVsValues/resOut/values/values.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <item type="layout" name="alias_replaced_by_file">@layout/main</item>
+</resources>
+
diff --git a/gradle/src/integration-test/groovy/com/android/build/gradle/ProjectTest.groovy b/gradle/src/integration-test/groovy/com/android/build/gradle/ProjectTest.groovy
index 20d6d22..abe1848 100644
--- a/gradle/src/integration-test/groovy/com/android/build/gradle/ProjectTest.groovy
+++ b/gradle/src/integration-test/groovy/com/android/build/gradle/ProjectTest.groovy
@@ -72,7 +72,7 @@
         buildProject("flavors")
     }
 
-    void testlibsTest() {
+    void testLibsTest() {
         buildProject("libsTest")
     }
 
@@ -84,7 +84,7 @@
         buildProject("multiproject")
     }
 
-    void testMultires() {
+    void testMultiRes() {
         buildProject("multires")
     }
 
@@ -120,8 +120,8 @@
             buildProject(new File(repo, "app"), "clean", "assemble")
         } finally {
             // clean up the test repository.
-            File testrepo = new File(repo, "testrepo")
-            testrepo.deleteDir()
+            File testRepo = new File(repo, "testrepo")
+            testRepo.deleteDir()
         }
     }
 
@@ -133,6 +133,9 @@
         File[] projects = testDir.listFiles()
         for (File project : projects) {
             String name = project.name
+            if (name.startsWith(".")) {
+                continue
+            }
             if (!builtProjects.contains(name)) {
                 buildProject(name)
             }
@@ -143,7 +146,7 @@
         File project = new File(testDir, name)
         builtProjects.add(name)
 
-        buildProject(project, "clean", "assemble")
+        buildProject(project, "clean", "assembleDebug")
 
         return project;
     }
diff --git a/gradle/src/main/groovy/com/android/build/gradle/BasePlugin.groovy b/gradle/src/main/groovy/com/android/build/gradle/BasePlugin.groovy
index f964a05..5740251 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/BasePlugin.groovy
+++ b/gradle/src/main/groovy/com/android/build/gradle/BasePlugin.groovy
@@ -16,56 +16,15 @@
 package com.android.build.gradle
 
 import com.android.SdkConstants
-import com.android.build.gradle.internal.AndroidDependencyImpl
-import com.android.build.gradle.internal.ApplicationVariant
-import com.android.build.gradle.internal.ConfigurationDependencies
-import com.android.build.gradle.internal.DependencyChecker
-import com.android.build.gradle.internal.LoggerWrapper
-import com.android.build.gradle.internal.ManifestDependencyImpl
-import com.android.build.gradle.internal.ProductFlavorData
-import com.android.build.gradle.internal.ProductionAppVariant
-import com.android.build.gradle.internal.SymbolFileProviderImpl
-import com.android.build.gradle.internal.TestAppVariant
-import com.android.build.gradle.internal.tasks.AidlCompileTask
-import com.android.build.gradle.internal.tasks.AndroidDependencyTask
-import com.android.build.gradle.internal.tasks.DexTask
-import com.android.build.gradle.internal.tasks.GenerateBuildConfigTask
-import com.android.build.gradle.internal.tasks.InstallTask
-import com.android.build.gradle.internal.tasks.PackageApplicationTask
-import com.android.build.gradle.internal.tasks.PrepareDependenciesTask
-import com.android.build.gradle.internal.tasks.PrepareLibraryTask
-import com.android.build.gradle.internal.tasks.MergeResourcesTask
-import com.android.build.gradle.internal.tasks.ProcessManifestTask
-import com.android.build.gradle.internal.tasks.ProcessResourcesTask
-import com.android.build.gradle.internal.tasks.ProcessTestManifestTask
-import com.android.build.gradle.internal.tasks.RunTestsTask
-import com.android.build.gradle.internal.tasks.UninstallTask
-import com.android.build.gradle.internal.tasks.ZipAlignTask
-import com.android.builder.AndroidBuilder
-import com.android.builder.AndroidDependency
-import com.android.builder.BuilderConstants
-import com.android.builder.DefaultSdkParser
-import com.android.builder.JarDependency
-import com.android.builder.ManifestDependency
-import com.android.builder.ProductFlavor
-import com.android.builder.SdkParser
-import com.android.builder.SourceProvider
-import com.android.builder.SymbolFileProvider
-import com.android.builder.VariantConfiguration
+import com.android.build.gradle.internal.*
+import com.android.build.gradle.internal.tasks.*
+import com.android.builder.*
 import com.android.utils.ILogger
 import com.google.common.collect.ArrayListMultimap
 import com.google.common.collect.Lists
 import com.google.common.collect.Multimap
-import org.gradle.api.DefaultTask
-import org.gradle.api.GradleException
-import org.gradle.api.NamedDomainObjectContainer
-import org.gradle.api.Project
-import org.gradle.api.Task
-import org.gradle.api.artifacts.Configuration
-import org.gradle.api.artifacts.ModuleVersionIdentifier
-import org.gradle.api.artifacts.ProjectDependency
-import org.gradle.api.artifacts.ResolvedArtifact
-import org.gradle.api.artifacts.SelfResolvingDependency
+import org.gradle.api.*
+import org.gradle.api.artifacts.*
 import org.gradle.api.artifacts.result.ResolvedDependencyResult
 import org.gradle.api.artifacts.result.ResolvedModuleVersionResult
 import org.gradle.api.internal.plugins.ProcessResources
@@ -326,10 +285,11 @@
 
         mergeResourcesTask.plugin = this
         mergeResourcesTask.variant = variant
+        mergeResourcesTask.incrementalFolder = project.file("$project.buildDir/incremental/$variant.dirName")
 
         mergeResourcesTask.conventionMapping.inputResourceSets = { variant.config.resourceSets }
         mergeResourcesTask.conventionMapping.rawInputFolders = {
-            MergeResourcesTask.inlineInputs(variant.config.resourceSets)
+            BaseTask.flattenSourceSets(variant.config.resourceSets)
         }
 
         mergeResourcesTask.conventionMapping.outputDir = {
diff --git a/gradle/src/main/groovy/com/android/build/gradle/internal/AaptOptionsImpl.groovy b/gradle/src/main/groovy/com/android/build/gradle/internal/AaptOptionsImpl.groovy
index 55773e0..1b65438 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/internal/AaptOptionsImpl.groovy
+++ b/gradle/src/main/groovy/com/android/build/gradle/internal/AaptOptionsImpl.groovy
@@ -29,7 +29,7 @@
     private List<String> noCompressList
 
     public void setIgnoreAssetsPattern(String ignoreAssetsPattern) {
-        ignoreAssetsPattern = ignoreAssetsPattern
+        this.ignoreAssetsPattern = ignoreAssetsPattern
     }
 
     @Override
diff --git a/gradle/src/main/groovy/com/android/build/gradle/internal/DefaultAndroidSourceSet.java b/gradle/src/main/groovy/com/android/build/gradle/internal/DefaultAndroidSourceSet.java
index 6d52086..77a6760 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/internal/DefaultAndroidSourceSet.java
+++ b/gradle/src/main/groovy/com/android/build/gradle/internal/DefaultAndroidSourceSet.java
@@ -65,6 +65,7 @@
         String javaResourcesDisplayName = String.format("%s Java resources", displayName);
         javaResources = new DefaultSourceDirectorySet(javaResourcesDisplayName, fileResolver);
         javaResources.getFilter().exclude(new Spec<FileTreeElement>() {
+            @Override
             public boolean isSatisfiedBy(FileTreeElement element) {
                 return javaSource.contains(element.getFile());
             }
diff --git a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/AidlCompileTask.groovy b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/AidlCompileTask.groovy
index f65f9ba..f34db20 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/AidlCompileTask.groovy
+++ b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/AidlCompileTask.groovy
@@ -15,11 +15,8 @@
  */
 
 package com.android.build.gradle.internal.tasks
-
 import com.android.build.gradle.tasks.AidlCompile
 import org.gradle.api.tasks.InputFiles
-import org.gradle.api.tasks.TaskAction
-
 /**
  */
 public class AidlCompileTask extends AidlCompile {
@@ -30,8 +27,8 @@
     @InputFiles
     List<File> importDirs
 
-    @TaskAction
-    void generate() {
+    @Override
+    protected void doFullTaskAction() {
         getBuilder().compileAidl(getSourceDirs(), getSourceOutputDir(), getImportDirs())
     }
 }
diff --git a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/BaseTask.groovy b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/BaseTask.groovy
index 13df81e..d6146c4 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/BaseTask.groovy
+++ b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/BaseTask.groovy
@@ -18,14 +18,136 @@
 import com.android.build.gradle.BasePlugin
 import com.android.build.gradle.internal.ApplicationVariant
 import com.android.builder.AndroidBuilder
+import com.android.builder.internal.incremental.ChangeManager
+import com.android.builder.resources.FileStatus
+import com.android.builder.resources.SourceSet
+import com.google.common.collect.Lists
 import org.gradle.api.DefaultTask
+import org.gradle.api.file.FileCollection
+import org.gradle.api.tasks.Optional
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.TaskAction
+import org.gradle.api.tasks.TaskInputs
 
 public abstract class BaseTask extends DefaultTask {
 
     BasePlugin plugin
     ApplicationVariant variant
 
+    @OutputDirectory @Optional
+    File incrementalFolder
+
+    private Map<File, FileStatus> sEmptyMap = Collections.emptyMap()
+
     protected AndroidBuilder getBuilder() {
-        return plugin.getAndroidBuilder(variant);
+        return plugin.getAndroidBuilder(variant)
+    }
+
+    /**
+     * Whether this task can support incremental update using the {@link ChangeManager}
+     *
+     * @return whether this task can support incremental update.
+     */
+    protected boolean isIncremental() {
+        return false
+    }
+
+    /**
+     * Actual task action. This is called when a full run is needed, which is always the case if
+     * {@link #isIncremental()} returns false.
+     *
+     */
+    protected abstract void doFullTaskAction();
+
+    /**
+     * Optional incremental task action.
+     * Only used if {@link #isIncremental()} returns true.
+     *
+     * @param changedInputs the changed input files.
+     * @param changedOutputs the changed output files.
+     */
+    protected void doIncrementalTaskAction(Map<File, FileStatus> changedInputs,
+                                           Map<File, FileStatus> changedOutputs) {
+        // do nothing.
+    }
+
+    protected Collection<File> getOutputForIncrementalBuild() {
+        return Collections.emptyList();
+    }
+
+    /**
+     * Actual entry point for the action.
+     * Calls out to the doTaskAction as needed.
+     */
+    @TaskAction
+    void taskAction() {
+        if (!isIncremental() || incrementalFolder == null) {
+            doFullTaskAction()
+            return;
+        }
+
+        // load known state.
+        ChangeManager changeManager = new ChangeManager()
+        boolean fullBuild = !changeManager.load(incrementalFolder)
+
+        // update with current files.
+        TaskInputs inputs = getInputs()
+        FileCollection inputCollection = inputs.getFiles()
+
+        for (File f : inputCollection.files) {
+            changeManager.addInput(f)
+        }
+
+        for (File f : getOutputForIncrementalBuild()) {
+            changeManager.addOutput(f);
+        }
+
+        try {
+            // force full build if output changed somehow.
+            Map<File, FileStatus> changedOutputs = changeManager.getChangedOutputs()
+            Map<File, FileStatus> changedInputs = changeManager.getChangedInputs()
+            if (fullBuild) {
+                project.logger.info("No incremental data: full task run")
+                doFullTaskAction();
+            } else if (!changedOutputs.isEmpty()) {
+                for (File f : changedOutputs.keySet()) {
+                    project.logger.info(">> " + f)
+                }
+                project.logger.info("Changed output: full task run")
+
+                doFullTaskAction();
+            } else if (changedInputs.isEmpty() && changedOutputs.isEmpty()) {
+                // both input and output are empty, this is something we don't control
+                // through files, just do a full run
+                project.logger.info("Changed non file input/output: full task run")
+                doFullTaskAction()
+            } else {
+                doIncrementalTaskAction(
+                        changeManager.getChangedInputs(), changeManager.getChangedOutputs())
+            }
+
+            // update the outputs post task-action, to record their state
+            // for the next run
+            changeManager.updateOutputs(getOutputForIncrementalBuild())
+
+            // write the result down to be used next time the task is run.
+            changeManager.write(incrementalFolder)
+        } catch (Exception e) {
+            // Easiest to do here, is to delete the incremental Data so that
+            // next run is full.
+            ChangeManager.delete(incrementalFolder)
+
+            throw e
+        }
+    }
+
+    public static List<File> flattenSourceSets(List<? extends SourceSet> resourceSets) {
+        List<File> list = Lists.newArrayList();
+
+        for (SourceSet sourceSet : resourceSets) {
+            list.addAll(sourceSet.sourceFiles)
+        }
+
+        return list;
     }
 }
diff --git a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/DexTask.groovy b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/DexTask.groovy
index f62979a..a46e890 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/DexTask.groovy
+++ b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/DexTask.groovy
@@ -14,12 +14,10 @@
  * limitations under the License.
  */
 package com.android.build.gradle.internal.tasks
-
 import com.android.build.gradle.tasks.Dex
 import com.android.builder.DexOptions
 import org.gradle.api.tasks.InputFiles
 import org.gradle.api.tasks.Nested
-import org.gradle.api.tasks.TaskAction
 
 public class DexTask extends Dex {
 
@@ -32,8 +30,8 @@
     @Nested
     DexOptions dexOptions
 
-    @TaskAction
-    void generate() {
+    @Override
+    protected void doFullTaskAction() {
         getBuilder().convertByteCode(
                 getSourceFiles(),
                 getLibraries(),
diff --git a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/GenerateBuildConfigTask.groovy b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/GenerateBuildConfigTask.groovy
index e7bf2e6..df8b9f9 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/GenerateBuildConfigTask.groovy
+++ b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/GenerateBuildConfigTask.groovy
@@ -14,10 +14,8 @@
  * limitations under the License.
  */
 package com.android.build.gradle.internal.tasks
-
 import com.android.build.gradle.tasks.GenerateBuildConfig
 import org.gradle.api.tasks.Input
-import org.gradle.api.tasks.TaskAction
 
 public class GenerateBuildConfigTask extends GenerateBuildConfig {
 
@@ -30,8 +28,8 @@
     @Input
     List<String> javaLines;
 
-    @TaskAction
-    void generate() {
+    @Override
+    protected void doFullTaskAction() {
         getBuilder().generateBuildConfig(
                 getPackageName(),
                 isDebuggable(),
diff --git a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/MergeResourcesTask.groovy b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/MergeResourcesTask.groovy
index c51cb77..ac43974 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/MergeResourcesTask.groovy
+++ b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/MergeResourcesTask.groovy
@@ -1,7 +1,7 @@
 /*
  * Copyright (C) 2012 The Android Open Source Project
  *
- * Licensed under the Apache License, Version 2.0 (the "License");
+ * 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
  *
@@ -14,11 +14,12 @@
  * limitations under the License.
  */
 package com.android.build.gradle.internal.tasks
-
 import com.android.build.gradle.tasks.MergeResources
-import com.google.common.collect.Lists
+import com.android.builder.resources.FileStatus
+import com.android.builder.resources.ResourceMerger
+import com.android.builder.resources.ResourceSet
+import com.android.utils.Pair
 import org.gradle.api.tasks.InputFiles
-import org.gradle.api.tasks.TaskAction
 
 class MergeResourcesTask extends MergeResources {
 
@@ -27,29 +28,92 @@
     Iterable<File> rawInputFolders
 
     // actual inputs
-    List<List<File>> inputResourceSets
+    List<ResourceSet> inputResourceSets
 
-    @TaskAction
-    void generate() {
-        // this is not yet incremental. Need to clean up the previous merge result.
-        File destinationDir = getOutputDir()
-
-        deleteFolder(destinationDir)
-        destinationDir.mkdir()
-
-        getBuilder().mergeResources(destinationDir.absolutePath, getInputResourceSets())
+    @Override
+    protected boolean isIncremental() {
+        return true
     }
 
-    static Iterable<File> inlineInputs(List<List<File>> inputs) {
-        List<File> list = Lists.newArrayList();
+    @Override
+    protected Collection<File> getOutputForIncrementalBuild() {
+        return Collections.singletonList(getOutputDir())
+    }
 
-        for (List<File> folders : inputs) {
-            for (File folder : folders) {
-                list.add(folder);
+    @Override
+    protected void doFullTaskAction() {
+        // this is full run, clean the previous output
+        File destinationDir = getOutputDir()
+        deleteFolder(destinationDir)
+        destinationDir.mkdirs()
+
+        List<ResourceSet> resourceSets = getInputResourceSets()
+
+        // create a new merger and populate it with the sets.
+        ResourceMerger merger = new ResourceMerger()
+
+        for (ResourceSet resourceSet : resourceSets) {
+            // set needs to be loaded.
+            resourceSet.loadFromFiles()
+            merger.addResourceSet(resourceSet)
+        }
+
+        // get the merged set and write it down.
+        merger.writeResourceFolder(destinationDir)
+
+        // No exception? Write the known state.
+        merger.writeBlobTo(getIncrementalFolder())
+    }
+
+    @Override
+    protected void doIncrementalTaskAction(Map<File, FileStatus> changedInputs,
+                                           Map<File, FileStatus> changedOutputs) {
+        // create a merger and load the known state.
+        ResourceMerger merger = new ResourceMerger()
+        if (!merger.loadFromBlob(getIncrementalFolder())) {
+            doFullTaskAction()
+            return
+        }
+
+        // compare the known state to the current sets to detect incompatibility.
+        // This is in case there's a change that's too hard to do incrementally. In this case
+        // we'll simply revert to full build.
+        List<ResourceSet> resourceSets = getInputResourceSets()
+
+        if (!merger.checkValidUpdate(resourceSets)) {
+            project.logger.info("Changed Resource sets: full task run!")
+            doFullTaskAction()
+            return
+        }
+
+        // The incremental process is the following:
+        // Loop on all the changed files, find which ResourceSet it belongs to, then ask
+        // the resource set to update itself with the new file.
+        for (Map.Entry<File, FileStatus> entry : changedInputs.entrySet()) {
+            File changedFile = entry.getKey()
+
+            Pair<ResourceSet, File> matchSet = merger.getResourceSetContaining(changedFile)
+            assert matchSet != null
+            if (matchSet == null) {
+                doFullTaskAction()
+                return
+            }
+
+            // do something?
+            if (!matchSet.getFirst().updateWith(
+                    matchSet.getSecond(), changedFile, entry.getValue())) {
+                project.logger.info(
+                        String.format("Failed to process %s event! Full task run",
+                                entry.getValue()))
+                doFullTaskAction()
+                return
             }
         }
 
-        return list;
+        merger.writeResourceFolder(getOutputDir())
+
+        // No exception? Write the known state.
+        merger.writeBlobTo(getIncrementalFolder())
     }
 
     private static void deleteFolder(File folder) {
@@ -59,7 +123,7 @@
                 if (file.isDirectory()) {
                     deleteFolder(file)
                 }
-                file.delete();
+                file.delete()
             }
         }
 
diff --git a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/PackageApplicationTask.groovy b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/PackageApplicationTask.groovy
index 101f111..e1de634 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/PackageApplicationTask.groovy
+++ b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/PackageApplicationTask.groovy
@@ -14,13 +14,11 @@
  * limitations under the License.
  */
 package com.android.build.gradle.internal.tasks
-
 import com.android.build.gradle.tasks.PackageApplication
 import com.android.builder.packaging.DuplicateFileException
 import org.gradle.api.tasks.Input
 import org.gradle.api.tasks.InputFiles
 import org.gradle.api.tasks.Optional
-import org.gradle.api.tasks.TaskAction
 
 public class PackageApplicationTask extends PackageApplication {
 
@@ -45,9 +43,8 @@
     @Input @Optional
     String signingKeyPassword
 
-    @TaskAction
-    void generate() {
-
+    @Override
+    protected void doFullTaskAction() {
         try {
             getBuilder().packageApk(
                     getResourceFile().absolutePath,
diff --git a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/PrepareDependenciesTask.groovy b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/PrepareDependenciesTask.groovy
index 77f5182..7d0f7e4 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/PrepareDependenciesTask.groovy
+++ b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/PrepareDependenciesTask.groovy
@@ -14,10 +14,8 @@
  * limitations under the License.
  */
 package com.android.build.gradle.internal.tasks
-
 import com.android.build.gradle.internal.DependencyChecker
 import com.android.utils.Pair
-import org.gradle.api.tasks.TaskAction
 
 public class PrepareDependenciesTask extends BaseTask {
     final List<DependencyChecker> checkers = []
@@ -27,8 +25,8 @@
         androidDependencies.add(api)
     }
 
-    @TaskAction
-    def prepare() {
+    @Override
+    protected void doFullTaskAction() {
         def minSdkVersion = variant.config.minSdkVersion
 
         for (DependencyChecker checker : checkers) {
diff --git a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/ProcessManifestTask.groovy b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/ProcessManifestTask.groovy
index c829a18..3543bd9 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/ProcessManifestTask.groovy
+++ b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/ProcessManifestTask.groovy
@@ -22,7 +22,6 @@
 import org.gradle.api.tasks.InputFiles
 import org.gradle.api.tasks.Nested
 import org.gradle.api.tasks.Optional
-import org.gradle.api.tasks.TaskAction
 
 /**
  */
@@ -49,8 +48,8 @@
     @Input
     int targetSdkVersion
 
-    @TaskAction
-    void generate() {
+    @Override
+    protected void doFullTaskAction() {
         getBuilder().processManifest(
                 getMainManifest(),
                 getManifestOverlays(),
diff --git a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/ProcessResourcesTask.groovy b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/ProcessResourcesTask.groovy
index af53b65..993562c 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/ProcessResourcesTask.groovy
+++ b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/ProcessResourcesTask.groovy
@@ -18,11 +18,11 @@
 import com.android.build.gradle.tasks.ProcessResources
 import com.android.builder.SymbolFileProvider
 import com.android.builder.VariantConfiguration
+import com.android.builder.resources.FileStatus
 import org.gradle.api.tasks.Input
 import org.gradle.api.tasks.InputDirectory
 import org.gradle.api.tasks.Nested
 import org.gradle.api.tasks.Optional
-import org.gradle.api.tasks.TaskAction
 
 public class ProcessResourcesTask extends ProcessResources {
 
@@ -44,9 +44,8 @@
     @Nested
     AaptOptionsImpl aaptOptions
 
-    @TaskAction
-    void generate() {
-
+    @Override
+    protected void doFullTaskAction() {
         getBuilder().processResources(
                 getManifestFile(),
                 getMergedResFolder(),
diff --git a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/ProcessTestManifestTask.groovy b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/ProcessTestManifestTask.groovy
index 1edbe21..f140ec6 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/ProcessTestManifestTask.groovy
+++ b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/ProcessTestManifestTask.groovy
@@ -14,13 +14,10 @@
  * limitations under the License.
  */
 package com.android.build.gradle.internal.tasks
-
 import com.android.build.gradle.tasks.ProcessManifest
 import com.android.builder.ManifestDependency
 import org.gradle.api.tasks.Input
 import org.gradle.api.tasks.Nested
-import org.gradle.api.tasks.TaskAction
-
 /**
  */
 class ProcessTestManifestTask extends ProcessManifest {
@@ -40,8 +37,8 @@
     @Nested
     List<ManifestDependency> libraries
 
-    @TaskAction
-    void generate() {
+    @Override
+    protected void doFullTaskAction() {
         getBuilder().processTestManifest(
                 getTestPackageName(),
                 getMinSdkVersion(),
diff --git a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/RunTestsTask.groovy b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/RunTestsTask.groovy
index e7f605a..ec0873c 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/RunTestsTask.groovy
+++ b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/RunTestsTask.groovy
@@ -16,8 +16,6 @@
 package com.android.build.gradle.internal.tasks
 
 import org.gradle.api.tasks.Input
-import org.gradle.api.tasks.TaskAction
-
 /**
  * Run tests for a given variant
  */
@@ -26,8 +24,8 @@
     @Input
     File sdkDir
 
-    @TaskAction
-    void generate() {
+    @Override
+    protected void doFullTaskAction() {
         List<String> command = variant.runCommand
 
         logger.info("Running tests with command: " + command)
diff --git a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/UninstallTask.groovy b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/UninstallTask.groovy
index 42bb30a..995eda0 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/UninstallTask.groovy
+++ b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/UninstallTask.groovy
@@ -15,15 +15,15 @@
  */
 package com.android.build.gradle.internal.tasks
 
+import com.android.builder.resources.FileStatus
 import org.gradle.api.tasks.Input
-import org.gradle.api.tasks.TaskAction
 
 public class UninstallTask extends BaseTask {
     @Input
     File sdkDir
 
-    @TaskAction
-    void generate() {
+    @Override
+    protected void doFullTaskAction() {
         String packageName = variant.package
         logger.info("Uninstalling app: " + packageName)
         project.exec {
diff --git a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/ZipAlignTask.groovy b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/ZipAlignTask.groovy
index e69ce14..37884cb 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/ZipAlignTask.groovy
+++ b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/ZipAlignTask.groovy
@@ -25,7 +25,7 @@
     File sdkDir
 
     @TaskAction
-    void generate() {
+    void zipAlign() {
         project.exec {
             executable = new File(getSdkDir(), "tools${File.separator}zipalign")
             args '-f', '4'
diff --git a/gradle/src/main/groovy/com/android/build/gradle/tasks/AidlCompile.groovy b/gradle/src/main/groovy/com/android/build/gradle/tasks/AidlCompile.groovy
index 50f938b..8e597d2 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/tasks/AidlCompile.groovy
+++ b/gradle/src/main/groovy/com/android/build/gradle/tasks/AidlCompile.groovy
@@ -21,7 +21,7 @@
 
 /**
  */
-public class AidlCompile extends BaseTask {
+public abstract class AidlCompile extends BaseTask {
 
     @OutputDirectory
     File sourceOutputDir
diff --git a/gradle/src/main/groovy/com/android/build/gradle/tasks/Dex.groovy b/gradle/src/main/groovy/com/android/build/gradle/tasks/Dex.groovy
index f5a6540..c1b1daf 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/tasks/Dex.groovy
+++ b/gradle/src/main/groovy/com/android/build/gradle/tasks/Dex.groovy
@@ -18,7 +18,7 @@
 import com.android.build.gradle.internal.tasks.BaseTask
 import org.gradle.api.tasks.OutputFile
 
-public class Dex extends BaseTask {
+public abstract class Dex extends BaseTask {
 
     @OutputFile
     File outputFile
diff --git a/gradle/src/main/groovy/com/android/build/gradle/tasks/GenerateBuildConfig.groovy b/gradle/src/main/groovy/com/android/build/gradle/tasks/GenerateBuildConfig.groovy
index 7f30c8b..9c75214 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/tasks/GenerateBuildConfig.groovy
+++ b/gradle/src/main/groovy/com/android/build/gradle/tasks/GenerateBuildConfig.groovy
@@ -18,7 +18,7 @@
 import com.android.build.gradle.internal.tasks.BaseTask
 import org.gradle.api.tasks.OutputDirectory
 
-public class GenerateBuildConfig extends BaseTask {
+public abstract class GenerateBuildConfig extends BaseTask {
 
     @OutputDirectory
     File sourceOutputDir
diff --git a/gradle/src/main/groovy/com/android/build/gradle/tasks/MergeResources.groovy b/gradle/src/main/groovy/com/android/build/gradle/tasks/MergeResources.groovy
index 0ed64ea..c824d3d 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/tasks/MergeResources.groovy
+++ b/gradle/src/main/groovy/com/android/build/gradle/tasks/MergeResources.groovy
@@ -18,7 +18,7 @@
 import com.android.build.gradle.internal.tasks.BaseTask
 import org.gradle.api.tasks.OutputDirectory
 
-class MergeResources extends BaseTask {
+abstract class MergeResources extends BaseTask {
 
     @OutputDirectory
     File outputDir
diff --git a/gradle/src/main/groovy/com/android/build/gradle/tasks/PackageApplication.groovy b/gradle/src/main/groovy/com/android/build/gradle/tasks/PackageApplication.groovy
index 1ebe367..07ee2db 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/tasks/PackageApplication.groovy
+++ b/gradle/src/main/groovy/com/android/build/gradle/tasks/PackageApplication.groovy
@@ -21,7 +21,7 @@
 import org.gradle.api.tasks.Optional
 import org.gradle.api.tasks.OutputFile
 
-public class PackageApplication extends BaseTask {
+public abstract class PackageApplication extends BaseTask {
     @InputFile
     File resourceFile
 
diff --git a/gradle/src/main/groovy/com/android/build/gradle/tasks/ProcessResources.groovy b/gradle/src/main/groovy/com/android/build/gradle/tasks/ProcessResources.groovy
index da4ab76..c2355aa 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/tasks/ProcessResources.groovy
+++ b/gradle/src/main/groovy/com/android/build/gradle/tasks/ProcessResources.groovy
@@ -22,7 +22,7 @@
 import org.gradle.api.tasks.OutputDirectory
 import org.gradle.api.tasks.OutputFile
 
-public class ProcessResources extends BaseTask {
+public abstract class ProcessResources extends BaseTask {
 
     @InputFile
     File manifestFile
diff --git a/gradle/src/main/groovy/com/android/build/gradle/tasks/ZipAlign.groovy b/gradle/src/main/groovy/com/android/build/gradle/tasks/ZipAlign.groovy
index d41a981..a71d65d 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/tasks/ZipAlign.groovy
+++ b/gradle/src/main/groovy/com/android/build/gradle/tasks/ZipAlign.groovy
@@ -19,7 +19,7 @@
 import org.gradle.api.tasks.InputFile
 import org.gradle.api.tasks.OutputFile
 
-public class ZipAlign extends DefaultTask {
+public abstract class ZipAlign extends DefaultTask {
     @OutputFile
     File outputFile