Merge "More APIs for encryption-aware apps."
diff --git a/core/java/android/security/net/config/ApplicationConfig.java b/core/java/android/security/net/config/ApplicationConfig.java
index c675352..9bf344a 100644
--- a/core/java/android/security/net/config/ApplicationConfig.java
+++ b/core/java/android/security/net/config/ApplicationConfig.java
@@ -48,7 +48,7 @@
      */
     public boolean hasPerDomainConfigs() {
         ensureInitialized();
-        return mConfigs == null || !mConfigs.isEmpty();
+        return mConfigs != null && !mConfigs.isEmpty();
     }
 
     /**
diff --git a/core/java/android/security/net/config/NetworkSecurityConfig.java b/core/java/android/security/net/config/NetworkSecurityConfig.java
index 915fbef..1c787b4 100644
--- a/core/java/android/security/net/config/NetworkSecurityConfig.java
+++ b/core/java/android/security/net/config/NetworkSecurityConfig.java
@@ -17,6 +17,9 @@
 package android.security.net.config;
 
 import android.util.ArraySet;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 
@@ -26,6 +29,12 @@
  * @hide
  */
 public final class NetworkSecurityConfig {
+    /** @hide */
+    public static final boolean DEFAULT_CLEARTEXT_TRAFFIC_PERMITTED = true;
+    /** @hide */
+    public static final boolean DEFAULT_HSTS_ENFORCED = false;
+    public static final NetworkSecurityConfig DEFAULT = getDefaultBuilder().build();
+
     private final boolean mCleartextTrafficPermitted;
     private final boolean mHstsEnforced;
     private final PinSet mPins;
@@ -35,7 +44,7 @@
     private X509TrustManager mTrustManager;
     private final Object mTrustManagerLock = new Object();
 
-    public NetworkSecurityConfig(boolean cleartextTrafficPermitted, boolean hstsEnforced,
+    private NetworkSecurityConfig(boolean cleartextTrafficPermitted, boolean hstsEnforced,
             PinSet pins, List<CertificatesEntryRef> certificatesEntryRefs) {
         mCleartextTrafficPermitted = cleartextTrafficPermitted;
         mHstsEnforced = hstsEnforced;
@@ -83,4 +92,151 @@
             mAnchors = null;
         }
     }
+
+    /**
+     * Return a {@link Builder} for the default {@code NetworkSecurityConfig}.
+     *
+     * <p>
+     * The default configuration has the following properties:
+     * <ol>
+     * <li>Cleartext traffic is permitted.</li>
+     * <li>HSTS is not enforced.</li>
+     * <li>No certificate pinning is used.</li>
+     * <li>The system and user added trusted certificate stores are trusted for connections.</li>
+     * </ol>
+     *
+     * @hide
+     */
+    public static final Builder getDefaultBuilder() {
+        return new Builder()
+                .setCleartextTrafficPermitted(DEFAULT_CLEARTEXT_TRAFFIC_PERMITTED)
+                .setHstsEnforced(DEFAULT_HSTS_ENFORCED)
+                // System certificate store, does not bypass static pins.
+                .addCertificatesEntryRef(
+                        new CertificatesEntryRef(SystemCertificateSource.getInstance(), false))
+                // User certificate store, does not bypass static pins.
+                .addCertificatesEntryRef(
+                        new CertificatesEntryRef(UserCertificateSource.getInstance(), false));
+    }
+
+    /**
+     * Builder for creating {@code NetworkSecurityConfig} objects.
+     * @hide
+     */
+    public static final class Builder {
+        private List<CertificatesEntryRef> mCertificatesEntryRefs;
+        private PinSet mPinSet;
+        private boolean mCleartextTrafficPermitted = DEFAULT_CLEARTEXT_TRAFFIC_PERMITTED;
+        private boolean mHstsEnforced = DEFAULT_HSTS_ENFORCED;
+        private boolean mCleartextTrafficPermittedSet = false;
+        private boolean mHstsEnforcedSet = false;
+        private Builder mParentBuilder;
+
+        /**
+         * Sets the parent {@code Builder} for this {@code Builder}.
+         * The parent will be used to determine values not configured in this {@code Builder}
+         * in {@link Builder#build()}, recursively if needed.
+         */
+        public Builder setParent(Builder parent) {
+            // Sanity check to avoid adding loops.
+            Builder current = parent;
+            while (current != null) {
+                if (current == this) {
+                    throw new IllegalArgumentException("Loops are not allowed in Builder parents");
+                }
+                current = current.getParent();
+            }
+            mParentBuilder = parent;
+            return this;
+        }
+
+        public Builder getParent() {
+            return mParentBuilder;
+        }
+
+        public Builder setPinSet(PinSet pinSet) {
+            mPinSet = pinSet;
+            return this;
+        }
+
+        private PinSet getEffectivePinSet() {
+            if (mPinSet != null) {
+                return mPinSet;
+            }
+            if (mParentBuilder != null) {
+                return mParentBuilder.getEffectivePinSet();
+            }
+            return PinSet.EMPTY_PINSET;
+        }
+
+        public Builder setCleartextTrafficPermitted(boolean cleartextTrafficPermitted) {
+            mCleartextTrafficPermitted = cleartextTrafficPermitted;
+            mCleartextTrafficPermittedSet = true;
+            return this;
+        }
+
+        private boolean getEffectiveCleartextTrafficPermitted() {
+            if (mCleartextTrafficPermittedSet) {
+                return mCleartextTrafficPermitted;
+            }
+            if (mParentBuilder != null) {
+                return mParentBuilder.getEffectiveCleartextTrafficPermitted();
+            }
+            return DEFAULT_CLEARTEXT_TRAFFIC_PERMITTED;
+        }
+
+        public Builder setHstsEnforced(boolean hstsEnforced) {
+            mHstsEnforced = hstsEnforced;
+            mHstsEnforcedSet = true;
+            return this;
+        }
+
+        private boolean getEffectiveHstsEnforced() {
+            if (mHstsEnforcedSet) {
+                return mHstsEnforced;
+            }
+            if (mParentBuilder != null) {
+                return mParentBuilder.getEffectiveHstsEnforced();
+            }
+            return DEFAULT_HSTS_ENFORCED;
+        }
+
+        public Builder addCertificatesEntryRef(CertificatesEntryRef ref) {
+            if (mCertificatesEntryRefs == null) {
+                mCertificatesEntryRefs = new ArrayList<CertificatesEntryRef>();
+            }
+            mCertificatesEntryRefs.add(ref);
+            return this;
+        }
+
+        public Builder addCertificatesEntryRefs(Collection<? extends CertificatesEntryRef> refs) {
+            if (mCertificatesEntryRefs == null) {
+                mCertificatesEntryRefs = new ArrayList<CertificatesEntryRef>();
+            }
+            mCertificatesEntryRefs.addAll(refs);
+            return this;
+        }
+
+        private List<CertificatesEntryRef> getEffectiveCertificatesEntryRefs() {
+            if (mCertificatesEntryRefs != null) {
+                return mCertificatesEntryRefs;
+            }
+            if (mParentBuilder != null) {
+                return mParentBuilder.getEffectiveCertificatesEntryRefs();
+            }
+            return Collections.<CertificatesEntryRef>emptyList();
+        }
+
+        public boolean hasCertificateEntryRefs() {
+            return mCertificatesEntryRefs != null;
+        }
+
+        public NetworkSecurityConfig build() {
+            boolean cleartextPermitted = getEffectiveCleartextTrafficPermitted();
+            boolean hstsEnforced = getEffectiveHstsEnforced();
+            PinSet pinSet = getEffectivePinSet();
+            List<CertificatesEntryRef> entryRefs = getEffectiveCertificatesEntryRefs();
+            return new NetworkSecurityConfig(cleartextPermitted, hstsEnforced, pinSet, entryRefs);
+        }
+    }
 }
diff --git a/core/java/android/security/net/config/Pin.java b/core/java/android/security/net/config/Pin.java
index 8567431..94520e2 100644
--- a/core/java/android/security/net/config/Pin.java
+++ b/core/java/android/security/net/config/Pin.java
@@ -30,6 +30,26 @@
         this.digest = digest;
         mHashCode = Arrays.hashCode(digest) ^ digestAlgorithm.hashCode();
     }
+
+    /**
+     * @hide
+     */
+    public static boolean isSupportedDigestAlgorithm(String algorithm) {
+        // Currently only SHA-256 is supported. SHA-512 if/once Chromium networking stack
+        // supports it.
+        return "SHA-256".equalsIgnoreCase(algorithm);
+    }
+
+    /**
+     * @hide
+     */
+    public static int getDigestLength(String algorithm) {
+        if ("SHA-256".equalsIgnoreCase(algorithm)) {
+            return 32;
+        }
+        throw new IllegalArgumentException("Unsupported digest algorithm: " + algorithm);
+    }
+
     @Override
     public int hashCode() {
         return mHashCode;
diff --git a/core/java/android/security/net/config/PinSet.java b/core/java/android/security/net/config/PinSet.java
index a9ee039..d3c975e 100644
--- a/core/java/android/security/net/config/PinSet.java
+++ b/core/java/android/security/net/config/PinSet.java
@@ -17,10 +17,13 @@
 package android.security.net.config;
 
 import android.util.ArraySet;
+import java.util.Collections;
 import java.util.Set;
 
 /** @hide */
 public final class PinSet {
+    public static final PinSet EMPTY_PINSET =
+            new PinSet(Collections.<Pin>emptySet(), Long.MAX_VALUE);
     public final long expirationTime;
     public final Set<Pin> pins;
 
diff --git a/core/java/android/security/net/config/SystemCertificateSource.java b/core/java/android/security/net/config/SystemCertificateSource.java
index 640ebd9..7649a97 100644
--- a/core/java/android/security/net/config/SystemCertificateSource.java
+++ b/core/java/android/security/net/config/SystemCertificateSource.java
@@ -36,18 +36,23 @@
  * @hide
  */
 public class SystemCertificateSource implements CertificateSource {
-    private static Set<X509Certificate> sSystemCerts = null;
-    private static final Object sLock = new Object();
+    private static final SystemCertificateSource INSTANCE = new SystemCertificateSource();
+    private Set<X509Certificate> mSystemCerts = null;
+    private final Object mLock = new Object();
 
-    public SystemCertificateSource() {
+    private SystemCertificateSource() {
+    }
+
+    public static SystemCertificateSource getInstance() {
+        return INSTANCE;
     }
 
     @Override
     public Set<X509Certificate> getCertificates() {
         // TODO: loading all of these is wasteful, we should instead use a keystore style API.
-        synchronized (sLock) {
-            if (sSystemCerts != null) {
-                return sSystemCerts;
+        synchronized (mLock) {
+            if (mSystemCerts != null) {
+                return mSystemCerts;
             }
             CertificateFactory certFactory;
             try {
@@ -83,14 +88,14 @@
                     IoUtils.closeQuietly(is);
                 }
             }
-            sSystemCerts = systemCerts;
-            return sSystemCerts;
+            mSystemCerts = systemCerts;
+            return mSystemCerts;
         }
     }
 
     public void onCertificateStorageChange() {
-        synchronized (sLock) {
-            sSystemCerts = null;
+        synchronized (mLock) {
+            mSystemCerts = null;
         }
     }
 }
diff --git a/core/java/android/security/net/config/UserCertificateSource.java b/core/java/android/security/net/config/UserCertificateSource.java
index 77e2c88..e9d5aa1 100644
--- a/core/java/android/security/net/config/UserCertificateSource.java
+++ b/core/java/android/security/net/config/UserCertificateSource.java
@@ -36,18 +36,23 @@
  * @hide
  */
 public class UserCertificateSource implements CertificateSource {
-    private static Set<X509Certificate> sUserCerts = null;
-    private static final Object sLock = new Object();
+    private static final UserCertificateSource INSTANCE = new UserCertificateSource();
+    private Set<X509Certificate> mUserCerts = null;
+    private final Object mLock = new Object();
 
-    public UserCertificateSource() {
+    private UserCertificateSource() {
+    }
+
+    public static UserCertificateSource getInstance() {
+        return INSTANCE;
     }
 
     @Override
     public Set<X509Certificate> getCertificates() {
         // TODO: loading all of these is wasteful, we should instead use a keystore style API.
-        synchronized (sLock) {
-            if (sUserCerts != null) {
-                return sUserCerts;
+        synchronized (mLock) {
+            if (mUserCerts != null) {
+                return mUserCerts;
             }
             CertificateFactory certFactory;
             try {
@@ -57,32 +62,31 @@
             }
             final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
             final File userCaDir = new File(configDir, "cacerts-added");
-            if (!userCaDir.isDirectory()) {
-                throw new AssertionError(userCaDir + " is not a directory");
-            }
-
             Set<X509Certificate> userCerts = new ArraySet<X509Certificate>();
-            for (String caFile : userCaDir.list()) {
-                InputStream is = null;
-                try {
-                    is = new BufferedInputStream(
-                            new FileInputStream(new File(userCaDir, caFile)));
-                    userCerts.add((X509Certificate) certFactory.generateCertificate(is));
-                } catch (CertificateException | IOException e) {
-                    // Don't rethrow to be consistent with conscrypt's cert loading code.
-                    continue;
-                } finally {
-                    IoUtils.closeQuietly(is);
+            // If the user hasn't added any certificates the directory may not exist.
+            if (userCaDir.isDirectory()) {
+                for (String caFile : userCaDir.list()) {
+                    InputStream is = null;
+                    try {
+                        is = new BufferedInputStream(
+                                new FileInputStream(new File(userCaDir, caFile)));
+                        userCerts.add((X509Certificate) certFactory.generateCertificate(is));
+                    } catch (CertificateException | IOException e) {
+                        // Don't rethrow to be consistent with conscrypt's cert loading code.
+                        continue;
+                    } finally {
+                        IoUtils.closeQuietly(is);
+                    }
                 }
             }
-            sUserCerts = userCerts;
-            return sUserCerts;
+            mUserCerts = userCerts;
+            return mUserCerts;
         }
     }
 
     public void onCertificateStorageChange() {
-        synchronized (sLock) {
-            sUserCerts = null;
+        synchronized (mLock) {
+            mUserCerts = null;
         }
     }
 }
diff --git a/core/java/android/security/net/config/XmlConfigSource.java b/core/java/android/security/net/config/XmlConfigSource.java
new file mode 100644
index 0000000..6abfb66
--- /dev/null
+++ b/core/java/android/security/net/config/XmlConfigSource.java
@@ -0,0 +1,328 @@
+package android.security.net.config;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.util.ArraySet;
+import android.util.Base64;
+import android.util.Pair;
+import com.android.internal.util.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * {@link ConfigSource} based on an XML configuration file.
+ *
+ * @hide
+ */
+public class XmlConfigSource implements ConfigSource {
+    private final Object mLock = new Object();
+    private final int mResourceId;
+
+    private boolean mInitialized;
+    private NetworkSecurityConfig mDefaultConfig;
+    private Set<Pair<Domain, NetworkSecurityConfig>> mDomainMap;
+    private Context mContext;
+
+    public XmlConfigSource(Context context, int resourceId) {
+        mResourceId = resourceId;
+        mContext = context;
+    }
+
+    public Set<Pair<Domain, NetworkSecurityConfig>> getPerDomainConfigs() {
+        ensureInitialized();
+        return mDomainMap;
+    }
+
+    public NetworkSecurityConfig getDefaultConfig() {
+        ensureInitialized();
+        return mDefaultConfig;
+    }
+
+    private void ensureInitialized() {
+        synchronized (mLock) {
+            if (mInitialized) {
+                return;
+            }
+            try (XmlResourceParser parser = mContext.getResources().getXml(mResourceId)) {
+                parseNetworkSecurityConfig(parser);
+                mContext = null;
+                mInitialized = true;
+            } catch (Resources.NotFoundException | XmlPullParserException | IOException
+                    | ParserException e) {
+                throw new RuntimeException("Failed to parse XML configuration from "
+                        + mContext.getResources().getResourceEntryName(mResourceId), e);
+            }
+        }
+    }
+
+    private Pin parsePin(XmlResourceParser parser)
+            throws IOException, XmlPullParserException, ParserException {
+        String digestAlgorithm = parser.getAttributeValue(null, "digest");
+        if (!Pin.isSupportedDigestAlgorithm(digestAlgorithm)) {
+            throw new ParserException(parser, "Unsupported pin digest algorithm: "
+                    + digestAlgorithm);
+        }
+        if (parser.next() != XmlPullParser.TEXT) {
+            throw new ParserException(parser, "Missing pin digest");
+        }
+        String digest = parser.getText();
+        byte[] decodedDigest = null;
+        try {
+            decodedDigest = Base64.decode(digest, 0);
+        } catch (IllegalArgumentException e) {
+            throw new ParserException(parser, "Invalid pin digest", e);
+        }
+        int expectedLength = Pin.getDigestLength(digestAlgorithm);
+        if (decodedDigest.length != expectedLength) {
+            throw new ParserException(parser, "digest length " + decodedDigest.length
+                    + " does not match expected length for " + digestAlgorithm + " of "
+                    + expectedLength);
+        }
+        if (parser.next() != XmlPullParser.END_TAG) {
+            throw new ParserException(parser, "pin contains additional elements");
+        }
+        return new Pin(digestAlgorithm, decodedDigest);
+    }
+
+    private PinSet parsePinSet(XmlResourceParser parser)
+            throws IOException, XmlPullParserException, ParserException {
+        String expirationDate = parser.getAttributeValue(null, "expiration");
+        long expirationTimestampMilis = Long.MAX_VALUE;
+        if (expirationDate != null) {
+            try {
+                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+                sdf.setLenient(false);
+                Date date = sdf.parse(expirationDate);
+                if (date == null) {
+                    throw new ParserException(parser, "Invalid expiration date in pin-set");
+                }
+                expirationTimestampMilis = date.getTime();
+            } catch (ParseException e) {
+                throw new ParserException(parser, "Invalid expiration date in pin-set", e);
+            }
+        }
+
+        int outerDepth = parser.getDepth();
+        Set<Pin> pins = new ArraySet<>();
+        while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+            String tagName = parser.getName();
+            if (tagName.equals("pin")) {
+                pins.add(parsePin(parser));
+            } else {
+                XmlUtils.skipCurrentTag(parser);
+            }
+        }
+        return new PinSet(pins, expirationTimestampMilis);
+    }
+
+    private Domain parseDomain(XmlResourceParser parser, Set<String> seenDomains)
+            throws IOException, XmlPullParserException, ParserException {
+        boolean includeSubdomains =
+                parser.getAttributeBooleanValue(null, "includeSubdomains", false);
+        if (parser.next() != XmlPullParser.TEXT) {
+            throw new ParserException(parser, "Domain name missing");
+        }
+        String domain = parser.getText().toLowerCase(Locale.US);
+        if (parser.next() != XmlPullParser.END_TAG) {
+            throw new ParserException(parser, "domain contains additional elements");
+        }
+        // Domains are matched using a most specific match, so don't allow duplicates.
+        // includeSubdomains isn't relevant here, both android.com + subdomains and android.com
+        // match for android.com equally. Do not allow any duplicates period.
+        if (!seenDomains.add(domain)) {
+            throw new ParserException(parser, domain + " has already been specified");
+        }
+        return new Domain(domain, includeSubdomains);
+    }
+
+    private CertificatesEntryRef parseCertificatesEntry(XmlResourceParser parser)
+            throws IOException, XmlPullParserException, ParserException {
+        boolean overridePins = parser.getAttributeBooleanValue(null, "overridePins", false);
+        int sourceId = parser.getAttributeResourceValue(null, "src", -1);
+        String sourceString = parser.getAttributeValue(null, "src");
+        CertificateSource source = null;
+        if (sourceString == null) {
+            throw new ParserException(parser, "certificates element missing src attribute");
+        }
+        if (sourceId != -1) {
+            // TODO: Cache ResourceCertificateSources by sourceId
+            source = new ResourceCertificateSource(sourceId, mContext);
+        } else if ("system".equals(sourceString)) {
+            source = SystemCertificateSource.getInstance();
+        } else if ("user".equals(sourceString)) {
+            source = UserCertificateSource.getInstance();
+        } else {
+            throw new ParserException(parser, "Unknown certificates src. "
+                    + "Should be one of system|user|@resourceVal");
+        }
+        XmlUtils.skipCurrentTag(parser);
+        return new CertificatesEntryRef(source, overridePins);
+    }
+
+    private Collection<CertificatesEntryRef> parseTrustAnchors(XmlResourceParser parser)
+            throws IOException, XmlPullParserException, ParserException {
+        int outerDepth = parser.getDepth();
+        List<CertificatesEntryRef> anchors = new ArrayList<>();
+        while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+            String tagName = parser.getName();
+            if (tagName.equals("certificates")) {
+                anchors.add(parseCertificatesEntry(parser));
+            } else {
+                XmlUtils.skipCurrentTag(parser);
+            }
+        }
+        return anchors;
+    }
+
+    private List<Pair<NetworkSecurityConfig.Builder, Set<Domain>>> parseConfigEntry(
+            XmlResourceParser parser, Set<String> seenDomains,
+            NetworkSecurityConfig.Builder parentBuilder, boolean baseConfig)
+            throws IOException, XmlPullParserException, ParserException {
+        List<Pair<NetworkSecurityConfig.Builder, Set<Domain>>> builders = new ArrayList<>();
+        NetworkSecurityConfig.Builder builder = new NetworkSecurityConfig.Builder();
+        builder.setParent(parentBuilder);
+        Set<Domain> domains = new ArraySet<>();
+        boolean seenPinSet = false;
+        boolean seenTrustAnchors = false;
+        String configName = parser.getName();
+        int outerDepth = parser.getDepth();
+        // Add this builder now so that this builder occurs before any of its children. This
+        // makes the final build pass easier.
+        builders.add(new Pair<>(builder, domains));
+        // Parse config attributes. Only set values that are present, config inheritence will
+        // handle the rest.
+        for (int i = 0; i < parser.getAttributeCount(); i++) {
+            String name = parser.getAttributeName(i);
+            if ("hstsEnforced".equals(name)) {
+                builder.setHstsEnforced(
+                        parser.getAttributeBooleanValue(i,
+                                NetworkSecurityConfig.DEFAULT_HSTS_ENFORCED));
+            } else if ("cleartextTrafficPermitted".equals(name)) {
+                builder.setCleartextTrafficPermitted(
+                        parser.getAttributeBooleanValue(i,
+                                NetworkSecurityConfig.DEFAULT_CLEARTEXT_TRAFFIC_PERMITTED));
+            }
+        }
+        // Parse the config elements.
+        while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+            String tagName = parser.getName();
+            if ("domain".equals(tagName)) {
+                if (baseConfig) {
+                    throw new ParserException(parser, "domain element not allowed in base-config");
+                }
+                Domain domain = parseDomain(parser, seenDomains);
+                domains.add(domain);
+            } else if ("trust-anchors".equals(tagName)) {
+                if (seenTrustAnchors) {
+                    throw new ParserException(parser,
+                            "Multiple trust-anchor elements not allowed");
+                }
+                builder.addCertificatesEntryRefs(parseTrustAnchors(parser));
+                seenTrustAnchors = true;
+            } else if ("pin-set".equals(tagName)) {
+                if (baseConfig) {
+                    throw new ParserException(parser,
+                            "pin-set element not allowed in base-config");
+                }
+                if (seenPinSet) {
+                    throw new ParserException(parser, "Multiple pin-set elements not allowed");
+                }
+                builder.setPinSet(parsePinSet(parser));
+                seenPinSet = true;
+            } else if ("domain-config".equals(tagName)) {
+                if (baseConfig) {
+                    throw new ParserException(parser,
+                            "Nested domain-config not allowed in base-config");
+                }
+                builders.addAll(parseConfigEntry(parser, seenDomains, builder, false));
+            } else {
+                XmlUtils.skipCurrentTag(parser);
+            }
+        }
+        if (!baseConfig && domains.isEmpty()) {
+            throw new ParserException(parser, "No domain elements in domain-config");
+        }
+        return builders;
+    }
+
+    private void parseNetworkSecurityConfig(XmlResourceParser parser)
+            throws IOException, XmlPullParserException, ParserException {
+        Set<String> seenDomains = new ArraySet<>();
+        List<Pair<NetworkSecurityConfig.Builder, Set<Domain>>> builders = new ArrayList<>();
+        NetworkSecurityConfig.Builder baseConfigBuilder = null;
+        boolean seenDebugOverrides = false;
+        boolean seenBaseConfig = false;
+
+        XmlUtils.beginDocument(parser, "network-security-config");
+        int outerDepth = parser.getDepth();
+        while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+            // TODO: support debug-override.
+            if ("base-config".equals(parser.getName())) {
+                if (seenBaseConfig) {
+                    throw new ParserException(parser, "Only one base-config allowed");
+                }
+                seenBaseConfig = true;
+                baseConfigBuilder = parseConfigEntry(parser, seenDomains, null, true).get(0).first;
+            } else if ("domain-config".equals(parser.getName())) {
+                builders.addAll(parseConfigEntry(parser, seenDomains, baseConfigBuilder, false));
+            } else {
+                XmlUtils.skipCurrentTag(parser);
+            }
+        }
+
+        // Use the platform default as the parent of the base config for any values not provided
+        // there. If there is no base config use the platform default.
+        NetworkSecurityConfig.Builder platformDefaultBuilder =
+                NetworkSecurityConfig.getDefaultBuilder();
+        if (baseConfigBuilder != null) {
+            baseConfigBuilder.setParent(platformDefaultBuilder);
+        } else {
+            baseConfigBuilder = platformDefaultBuilder;
+        }
+        // Build the per-domain config mapping.
+        Set<Pair<Domain, NetworkSecurityConfig>> configs = new ArraySet<>();
+
+        for (Pair<NetworkSecurityConfig.Builder, Set<Domain>> entry : builders) {
+            NetworkSecurityConfig.Builder builder = entry.first;
+            Set<Domain> domains = entry.second;
+            // Set the parent of configs that do not have a parent to the base-config. This can
+            // happen if the base-config comes after a domain-config in the file.
+            // Note that this is safe with regards to children because of the order that
+            // parseConfigEntry returns builders, the parent is always before the children. The
+            // children builders will not have build called until _after_ their parents have their
+            // parent set so everything is consistent.
+            if (builder.getParent() == null) {
+                builder.setParent(baseConfigBuilder);
+            }
+            NetworkSecurityConfig config = builder.build();
+            for (Domain domain : domains) {
+                configs.add(new Pair<>(domain, config));
+            }
+        }
+        mDefaultConfig = baseConfigBuilder.build();
+        mDomainMap = configs;
+    }
+
+    public static class ParserException extends Exception {
+
+        public ParserException(XmlPullParser parser, String message, Throwable cause) {
+            super(message + " at: " + parser.getPositionDescription(), cause);
+        }
+
+        public ParserException(XmlPullParser parser, String message) {
+            this(parser, message, null);
+        }
+    }
+}
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index 28b22bf7..7b5f5ab 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -219,6 +219,11 @@
      */
     void cancelTaskWindowTransition(int taskId);
 
+    /**
+     * Cancels the thumbnail transitions for the given task.
+     */
+    void cancelTaskThumbnailTransition(int taskId);
+
     // These can only be called with the SET_ORIENTATION permission.
     /**
      * Update the current screen rotation based on the current state of
diff --git a/graphics/java/android/graphics/drawable/DrawableContainer.java b/graphics/java/android/graphics/drawable/DrawableContainer.java
index 10d862e..d05c66a 100644
--- a/graphics/java/android/graphics/drawable/DrawableContainer.java
+++ b/graphics/java/android/graphics/drawable/DrawableContainer.java
@@ -144,7 +144,7 @@
             mAlpha = alpha;
             if (mCurrDrawable != null) {
                 if (mEnterAnimationEnd == 0) {
-                    mCurrDrawable.mutate().setAlpha(alpha);
+                    mCurrDrawable.setAlpha(alpha);
                 } else {
                     animate(false);
                 }
@@ -162,7 +162,7 @@
         if (mDrawableContainerState.mDither != dither) {
             mDrawableContainerState.mDither = dither;
             if (mCurrDrawable != null) {
-                mCurrDrawable.mutate().setDither(mDrawableContainerState.mDither);
+                mCurrDrawable.setDither(mDrawableContainerState.mDither);
             }
         }
     }
@@ -175,7 +175,7 @@
             mDrawableContainerState.mColorFilter = colorFilter;
 
             if (mCurrDrawable != null) {
-                mCurrDrawable.mutate().setColorFilter(colorFilter);
+                mCurrDrawable.setColorFilter(colorFilter);
             }
         }
     }
@@ -188,7 +188,7 @@
             mDrawableContainerState.mTintList = tint;
 
             if (mCurrDrawable != null) {
-                mCurrDrawable.mutate().setTintList(tint);
+                mCurrDrawable.setTintList(tint);
             }
         }
     }
@@ -201,7 +201,7 @@
             mDrawableContainerState.mTintMode = tintMode;
 
             if (mCurrDrawable != null) {
-                mCurrDrawable.mutate().setTintMode(tintMode);
+                mCurrDrawable.setTintMode(tintMode);
             }
         }
     }
@@ -244,7 +244,7 @@
         if (mDrawableContainerState.mAutoMirrored != mirrored) {
             mDrawableContainerState.mAutoMirrored = mirrored;
             if (mCurrDrawable != null) {
-                mCurrDrawable.mutate().setAutoMirrored(mDrawableContainerState.mAutoMirrored);
+                mCurrDrawable.setAutoMirrored(mDrawableContainerState.mAutoMirrored);
             }
         }
     }
@@ -266,7 +266,7 @@
         if (mCurrDrawable != null) {
             mCurrDrawable.jumpToCurrentState();
             if (mHasAlpha) {
-                mCurrDrawable.mutate().setAlpha(mAlpha);
+                mCurrDrawable.setAlpha(mAlpha);
             }
         }
         if (mExitAnimationEnd != 0) {
@@ -499,8 +499,6 @@
      * @param d The drawable to initialize.
      */
     private void initializeDrawableForDisplay(Drawable d) {
-        d.mutate();
-
         if (mDrawableContainerState.mEnterFadeDuration <= 0 && mHasAlpha) {
             d.setAlpha(mAlpha);
         }
@@ -540,13 +538,12 @@
         if (mCurrDrawable != null) {
             if (mEnterAnimationEnd != 0) {
                 if (mEnterAnimationEnd <= now) {
-                    mCurrDrawable.mutate().setAlpha(mAlpha);
+                    mCurrDrawable.setAlpha(mAlpha);
                     mEnterAnimationEnd = 0;
                 } else {
                     int animAlpha = (int)((mEnterAnimationEnd-now)*255)
                             / mDrawableContainerState.mEnterFadeDuration;
-                    if (DEBUG) android.util.Log.i(TAG, toString() + " cur alpha " + animAlpha);
-                    mCurrDrawable.mutate().setAlpha(((255-animAlpha)*mAlpha)/255);
+                    mCurrDrawable.setAlpha(((255-animAlpha)*mAlpha)/255);
                     animating = true;
                 }
             }
@@ -563,8 +560,7 @@
                 } else {
                     int animAlpha = (int)((mExitAnimationEnd-now)*255)
                             / mDrawableContainerState.mExitFadeDuration;
-                    if (DEBUG) android.util.Log.i(TAG, toString() + " last alpha " + animAlpha);
-                    mLastDrawable.mutate().setAlpha((animAlpha*mAlpha)/255);
+                    mLastDrawable.setAlpha((animAlpha*mAlpha)/255);
                     animating = true;
                 }
             }
@@ -832,9 +828,7 @@
         private Drawable prepareDrawable(Drawable child) {
             child.setLayoutDirection(mLayoutDirection);
             child.setCallback(mOwner);
-            if (mMutated) {
-                child = child.mutate();
-            }
+            child = child.mutate();
             return child;
         }
 
diff --git a/libs/androidfw/tests/AppAsLib_test.cpp b/libs/androidfw/tests/AppAsLib_test.cpp
index bdb0c3d..8489acf 100644
--- a/libs/androidfw/tests/AppAsLib_test.cpp
+++ b/libs/androidfw/tests/AppAsLib_test.cpp
@@ -16,7 +16,6 @@
 
 #include <androidfw/ResourceTypes.h>
 
-#include "data/basic/R.h"
 #include "data/appaslib/R.h"
 
 #include <gtest/gtest.h>
@@ -25,29 +24,45 @@
 
 namespace {
 
-#include "data/basic/basic_arsc.h"
+#include "data/appaslib/appaslib_arsc.h"
+#include "data/appaslib/appaslib_lib_arsc.h"
 
+// This tests the app resources loaded as app.
 TEST(AppAsLibTest, loadedAsApp) {
   ResTable table;
-  ASSERT_EQ(NO_ERROR, table.add(basic_arsc, basic_arsc_len));
+  ASSERT_EQ(NO_ERROR, table.add(appaslib_arsc, appaslib_arsc_len));
 
   Res_value val;
-  ssize_t block = table.getResource(base::R::integer::number2, &val);
+  ssize_t block = table.getResource(appaslib::R::app::integer::number1, &val);
   ASSERT_GE(block, 0);
   ASSERT_EQ(Res_value::TYPE_REFERENCE, val.dataType);
-  ASSERT_EQ(base::R::array::integerArray1, val.data);
+  ASSERT_EQ(appaslib::R::app::array::integerArray1, val.data);
 }
 
+// This tests the app resources loaded as shared-lib.
 TEST(AppAsLibTest, loadedAsSharedLib) {
   ResTable table;
   // Load as shared library.
-  ASSERT_EQ(NO_ERROR, table.add(basic_arsc, basic_arsc_len, NULL, 0, -1, false, true));
+  ASSERT_EQ(NO_ERROR, table.add(appaslib_arsc, appaslib_arsc_len, NULL, 0, -1, false, true));
 
   Res_value val;
-  ssize_t block = table.getResource(appaslib::R::integer::number2, &val);
+  ssize_t block = table.getResource(appaslib::R::lib::integer::number1, &val);
   ASSERT_GE(block, 0);
   ASSERT_EQ(Res_value::TYPE_REFERENCE, val.dataType);
-  ASSERT_EQ(appaslib::R::array::integerArray1, val.data);
+  ASSERT_EQ(appaslib::R::lib::array::integerArray1, val.data);
+}
+
+// This tests the shared-lib loaded with appAsLib as true.
+TEST(AppAsLibTest, loadedSharedLib) {
+  ResTable table;
+  // Load shared library with appAsLib as true.
+  ASSERT_EQ(NO_ERROR, table.add(appaslib_lib_arsc, appaslib_lib_arsc_len, NULL, 0, -1, false, true));
+
+  Res_value val;
+  ssize_t block = table.getResource(appaslib::R::lib::integer::number1, &val);
+  ASSERT_GE(block, 0);
+  ASSERT_EQ(Res_value::TYPE_REFERENCE, val.dataType);
+  ASSERT_EQ(appaslib::R::lib::array::integerArray1, val.data);
 }
 
 }
diff --git a/libs/androidfw/tests/data/appaslib/AndroidManifest.xml b/libs/androidfw/tests/data/appaslib/AndroidManifest.xml
new file mode 100644
index 0000000..e00045b
--- /dev/null
+++ b/libs/androidfw/tests/data/appaslib/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.test.basic">
+    <application>
+    </application>
+</manifest>
diff --git a/libs/androidfw/tests/data/appaslib/R.h b/libs/androidfw/tests/data/appaslib/R.h
index f89d4bf..3af921a 100644
--- a/libs/androidfw/tests/data/appaslib/R.h
+++ b/libs/androidfw/tests/data/appaslib/R.h
@@ -19,19 +19,33 @@
 
 namespace appaslib {
 namespace R {
-
+namespace lib {
 namespace integer {
     enum {
-        number2     = 0x02040001,   // default
+        number1     = 0x02020000,   // default
     };
 }
 
 namespace array {
     enum {
-        integerArray1 = 0x02060000,   // default
+        integerArray1 = 0x02030000,   // default
+    };
+}
+} // namespace lib
+
+namespace app {
+namespace integer {
+    enum {
+        number1     = 0x7f020000,     // default
     };
 }
 
+namespace array {
+    enum {
+        integerArray1 = 0x7f030000,   // default
+    };
+}
+} // namespace app
 } // namespace R
 } // namespace appaslib
 
diff --git a/libs/androidfw/tests/data/appaslib/appaslib_arsc.h b/libs/androidfw/tests/data/appaslib/appaslib_arsc.h
new file mode 100644
index 0000000..be176ab
--- /dev/null
+++ b/libs/androidfw/tests/data/appaslib/appaslib_arsc.h
@@ -0,0 +1,68 @@
+unsigned char appaslib_arsc[] = {
+  0x02, 0x00, 0x0c, 0x00, 0x04, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
+  0x01, 0x00, 0x1c, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x20, 0x01, 0xdc, 0x02, 0x00, 0x00,
+  0x7f, 0x00, 0x00, 0x00, 0x63, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x2e, 0x00,
+  0x61, 0x00, 0x6e, 0x00, 0x64, 0x00, 0x72, 0x00, 0x6f, 0x00, 0x69, 0x00,
+  0x64, 0x00, 0x2e, 0x00, 0x74, 0x00, 0x65, 0x00, 0x73, 0x00, 0x74, 0x00,
+  0x2e, 0x00, 0x62, 0x00, 0x61, 0x00, 0x73, 0x00, 0x69, 0x00, 0x63, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00,
+  0x03, 0x00, 0x00, 0x00, 0x74, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x1c, 0x00, 0x54, 0x00, 0x00, 0x00,
+  0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x0c, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x04, 0x00, 0x61, 0x00,
+  0x74, 0x00, 0x74, 0x00, 0x72, 0x00, 0x00, 0x00, 0x07, 0x00, 0x69, 0x00,
+  0x6e, 0x00, 0x74, 0x00, 0x65, 0x00, 0x67, 0x00, 0x65, 0x00, 0x72, 0x00,
+  0x00, 0x00, 0x05, 0x00, 0x61, 0x00, 0x72, 0x00, 0x72, 0x00, 0x61, 0x00,
+  0x79, 0x00, 0x00, 0x00, 0x01, 0x00, 0x1c, 0x00, 0x54, 0x00, 0x00, 0x00,
+  0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x12, 0x00, 0x00, 0x00, 0x07, 0x00, 0x6e, 0x00, 0x75, 0x00, 0x6d, 0x00,
+  0x62, 0x00, 0x65, 0x00, 0x72, 0x00, 0x31, 0x00, 0x00, 0x00, 0x0d, 0x00,
+  0x69, 0x00, 0x6e, 0x00, 0x74, 0x00, 0x65, 0x00, 0x67, 0x00, 0x65, 0x00,
+  0x72, 0x00, 0x41, 0x00, 0x72, 0x00, 0x72, 0x00, 0x61, 0x00, 0x79, 0x00,
+  0x31, 0x00, 0x00, 0x00, 0x02, 0x02, 0x10, 0x00, 0x10, 0x00, 0x00, 0x00,
+  0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x02, 0x10, 0x00,
+  0x14, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x48, 0x00, 0x5c, 0x00, 0x00, 0x00,
+  0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x4c, 0x00, 0x00, 0x00,
+  0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0x7f,
+  0x02, 0x02, 0x10, 0x00, 0x14, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00,
+  0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x48, 0x00,
+  0x80, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
+  0x4c, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x10, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x08, 0x00, 0x00, 0x10,
+  0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x08, 0x00, 0x00, 0x10,
+  0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x08, 0x00, 0x00, 0x10,
+  0x03, 0x00, 0x00, 0x00
+};
+unsigned int appaslib_arsc_len = 772;
diff --git a/libs/androidfw/tests/data/appaslib/appaslib_lib_arsc.h b/libs/androidfw/tests/data/appaslib/appaslib_lib_arsc.h
new file mode 100644
index 0000000..099285a
--- /dev/null
+++ b/libs/androidfw/tests/data/appaslib/appaslib_lib_arsc.h
@@ -0,0 +1,68 @@
+unsigned char appaslib_lib_arsc[] = {
+  0x02, 0x00, 0x0c, 0x00, 0x04, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
+  0x01, 0x00, 0x1c, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x20, 0x01, 0xdc, 0x02, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x63, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x2e, 0x00,
+  0x61, 0x00, 0x6e, 0x00, 0x64, 0x00, 0x72, 0x00, 0x6f, 0x00, 0x69, 0x00,
+  0x64, 0x00, 0x2e, 0x00, 0x74, 0x00, 0x65, 0x00, 0x73, 0x00, 0x74, 0x00,
+  0x2e, 0x00, 0x62, 0x00, 0x61, 0x00, 0x73, 0x00, 0x69, 0x00, 0x63, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00,
+  0x03, 0x00, 0x00, 0x00, 0x74, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x1c, 0x00, 0x54, 0x00, 0x00, 0x00,
+  0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x0c, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x04, 0x00, 0x61, 0x00,
+  0x74, 0x00, 0x74, 0x00, 0x72, 0x00, 0x00, 0x00, 0x07, 0x00, 0x69, 0x00,
+  0x6e, 0x00, 0x74, 0x00, 0x65, 0x00, 0x67, 0x00, 0x65, 0x00, 0x72, 0x00,
+  0x00, 0x00, 0x05, 0x00, 0x61, 0x00, 0x72, 0x00, 0x72, 0x00, 0x61, 0x00,
+  0x79, 0x00, 0x00, 0x00, 0x01, 0x00, 0x1c, 0x00, 0x54, 0x00, 0x00, 0x00,
+  0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x12, 0x00, 0x00, 0x00, 0x07, 0x00, 0x6e, 0x00, 0x75, 0x00, 0x6d, 0x00,
+  0x62, 0x00, 0x65, 0x00, 0x72, 0x00, 0x31, 0x00, 0x00, 0x00, 0x0d, 0x00,
+  0x69, 0x00, 0x6e, 0x00, 0x74, 0x00, 0x65, 0x00, 0x67, 0x00, 0x65, 0x00,
+  0x72, 0x00, 0x41, 0x00, 0x72, 0x00, 0x72, 0x00, 0x61, 0x00, 0x79, 0x00,
+  0x31, 0x00, 0x00, 0x00, 0x02, 0x02, 0x10, 0x00, 0x10, 0x00, 0x00, 0x00,
+  0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x02, 0x10, 0x00,
+  0x14, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x48, 0x00, 0x5c, 0x00, 0x00, 0x00,
+  0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x4c, 0x00, 0x00, 0x00,
+  0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x07, 0x00, 0x00, 0x03, 0x00,
+  0x02, 0x02, 0x10, 0x00, 0x14, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00,
+  0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x48, 0x00,
+  0x80, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
+  0x4c, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x10, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x08, 0x00, 0x00, 0x10,
+  0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x08, 0x00, 0x00, 0x10,
+  0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x08, 0x00, 0x00, 0x10,
+  0x03, 0x00, 0x00, 0x00
+};
+unsigned int appaslib_lib_arsc_len = 772;
diff --git a/libs/androidfw/tests/data/appaslib/build b/libs/androidfw/tests/data/appaslib/build
new file mode 100755
index 0000000..e4bd88b
--- /dev/null
+++ b/libs/androidfw/tests/data/appaslib/build
@@ -0,0 +1,28 @@
+#!/bin/bash
+#
+# Copyright (C) 2015 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.
+#
+
+PATH_TO_FRAMEWORK_RES=$(gettop)/prebuilts/sdk/current/android.jar
+
+aapt package -M AndroidManifest.xml -S res -I $PATH_TO_FRAMEWORK_RES -F bundle.apk -f && \
+unzip bundle.apk resources.arsc && \
+mv resources.arsc appaslib.arsc && \
+xxd -i appaslib.arsc > appaslib_arsc.h && \
+aapt package -M AndroidManifest.xml -S res -I $PATH_TO_FRAMEWORK_RES -F bundle.apk -f --shared-lib && \
+unzip bundle.apk resources.arsc && \
+mv resources.arsc appaslib_lib.arsc && \
+xxd -i appaslib_lib.arsc > appaslib_lib_arsc.h \
+
diff --git a/libs/androidfw/tests/data/appaslib/res/values/values.xml b/libs/androidfw/tests/data/appaslib/res/values/values.xml
new file mode 100644
index 0000000..39b99a6
--- /dev/null
+++ b/libs/androidfw/tests/data/appaslib/res/values/values.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+
+<resources>
+    <integer name="number1">@array/integerArray1</integer>
+    <integer-array name="integerArray1">
+        <item>1</item>
+        <item>2</item>
+        <item>3</item>
+    </integer-array>
+</resources>
diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/Keyguard/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 57ee319..3d78028 100644
--- a/packages/Keyguard/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/Keyguard/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -603,6 +603,7 @@
 
     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
 
+        @Override
         public void onReceive(Context context, Intent intent) {
             final String action = intent.getAction();
             if (DEBUG) Log.d(TAG, "received broadcast " + action);
@@ -656,6 +657,7 @@
 
     private final BroadcastReceiver mBroadcastAllReceiver = new BroadcastReceiver() {
 
+        @Override
         public void onReceive(Context context, Intent intent) {
             final String action = intent.getAction();
             if (AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED.equals(action)) {
@@ -788,6 +790,7 @@
             return new SimData(state, slotId, subId);
         }
 
+        @Override
         public String toString() {
             return "SimData{state=" + simState + ",slotId=" + slotId + ",subId=" + subId + "}";
         }
@@ -1693,5 +1696,19 @@
         for (int subId : mServiceStates.keySet()) {
             pw.println("    " + subId + "=" + mServiceStates.get(subId));
         }
+        if (mFpm != null && mFpm.isHardwareDetected()) {
+            final int userId = ActivityManager.getCurrentUser();
+            final int strongAuthFlags = mStrongAuthTracker.getStrongAuthForUser(userId);
+            pw.println("  Fingerprint state (user=" + userId + ")");
+            pw.println("    allowed=" + isUnlockingWithFingerprintAllowed());
+            pw.println("    auth'd=" + mUserFingerprintAuthenticated.get(userId));
+            pw.println("    authSinceBoot="
+                    + getStrongAuthTracker().hasUserAuthenticatedSinceBoot());
+            pw.println("    disabled(DPM)=" + isFingerprintDisabled(userId));
+            pw.println("    possible=" + isUnlockWithFingerprintPossible(userId));
+            pw.println("    strongAuthFlags=" + Integer.toHexString(strongAuthFlags));
+            pw.println("    timedout=" + hasFingerprintUnlockTimedOut(userId));
+            pw.println("    trustManaged=" + getUserTrustIsManaged(userId));
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/Constants.java b/packages/SystemUI/src/com/android/systemui/recents/Constants.java
index ebfacac..a429447 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/Constants.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/Constants.java
@@ -21,6 +21,7 @@
  */
 public class Constants {
 
+    // TODO: Move into RecentsMetrics
     public static class Metrics {
         // DO NOT MODIFY THE ORDER OF THESE METRICS
         public static final int DismissSourceKeyboard = 0;
@@ -28,6 +29,7 @@
         public static final int DismissSourceHeaderButton = 2;
     }
 
+    // TODO: Move into RecentsDebugFlags
     public static class DebugFlags {
 
         public static class App {
@@ -37,7 +39,7 @@
             public static final boolean EnableTaskFiltering = false;
             // Enables dismiss-all
             public static final boolean EnableDismissAll = false;
-            // Enables fast-toggling
+            // Enables fast-toggling by just tapping on the recents button
             public static final boolean EnableFastToggleRecents = false;
             // Enables the thumbnail alpha on the front-most task
             public static final boolean EnableThumbnailAlphaOnFrontmost = false;
@@ -57,14 +59,4 @@
             public static final int SystemServicesProxyMockTaskCount = 100;
         }
     }
-
-    public static class Values {
-        public static class App {
-            public static int AppWidgetHostId = 1024;
-        }
-
-        public static class TaskStackView {
-            public static final int FilterStartDelay = 25;
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
index f800785..6874247 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
@@ -32,19 +32,20 @@
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.util.Log;
 import android.view.KeyEvent;
 import android.view.View;
 import android.view.ViewStub;
 import android.view.ViewTreeObserver;
-
 import com.android.internal.logging.MetricsLogger;
 import com.android.systemui.R;
 import com.android.systemui.recents.events.EventBus;
 import com.android.systemui.recents.events.activity.AppWidgetProviderChangedEvent;
+import com.android.systemui.recents.events.activity.CancelEnterRecentsWindowAnimationEvent;
 import com.android.systemui.recents.events.activity.EnterRecentsWindowAnimationStartedEvent;
+import com.android.systemui.recents.events.activity.EnterRecentsWindowLastAnimationFrameEvent;
 import com.android.systemui.recents.events.activity.HideRecentsEvent;
 import com.android.systemui.recents.events.activity.IterateRecentsEvent;
-import com.android.systemui.recents.events.activity.EnterRecentsWindowLastAnimationFrameEvent;
 import com.android.systemui.recents.events.activity.ToggleRecentsEvent;
 import com.android.systemui.recents.events.component.RecentsVisibilityChangedEvent;
 import com.android.systemui.recents.events.component.ScreenPinningRequestEvent;
@@ -57,7 +58,6 @@
 import com.android.systemui.recents.events.ui.focus.DismissFocusedTaskViewEvent;
 import com.android.systemui.recents.events.ui.focus.FocusNextTaskViewEvent;
 import com.android.systemui.recents.events.ui.focus.FocusPreviousTaskViewEvent;
-import com.android.systemui.recents.misc.Console;
 import com.android.systemui.recents.misc.DozeTrigger;
 import com.android.systemui.recents.misc.ReferenceCountedTrigger;
 import com.android.systemui.recents.misc.SystemServicesProxy;
@@ -137,8 +137,7 @@
             try {
                 startActivityAsUser(mLaunchIntent, mLaunchOpts.toBundle(), UserHandle.CURRENT);
             } catch (Exception e) {
-                Console.logError(RecentsActivity.this,
-                        getString(R.string.recents_launch_error_message, "Home"));
+                Log.e(TAG, getString(R.string.recents_launch_error_message, "Home"), e);
             }
         }
     }
@@ -345,7 +344,7 @@
 
         // Initialize the widget host (the host id is static and does not change)
         if (!Constants.DebugFlags.App.DisableSearchBar) {
-            mAppWidgetHost = new RecentsAppWidgetHost(this, Constants.Values.App.AppWidgetHostId);
+            mAppWidgetHost = new RecentsAppWidgetHost(this, RecentsAppWidgetHost.HOST_ID);
         }
         mPackageMonitor = new RecentsPackageMonitor();
         mPackageMonitor.register(this);
@@ -511,12 +510,7 @@
                 if (event.getRepeatCount() <= 0 || hasRepKeyTimeElapsed) {
                     // As we iterate to the next/previous task, cancel any current/lagging window
                     // transition animations
-                    RecentsConfiguration config = Recents.getConfiguration();
-                    RecentsActivityLaunchState launchState = config.getLaunchState();
-                    if (launchState.launchedToTaskId != -1) {
-                        SystemServicesProxy ssp = Recents.getSystemServices();
-                        ssp.cancelWindowTransition(launchState.launchedToTaskId);
-                    }
+                    EventBus.getDefault().send(new CancelEnterRecentsWindowAnimationEvent(null));
 
                     // Focus the next task in the stack
                     final boolean backward = event.isShiftPressed();
@@ -575,10 +569,6 @@
     /**** RecentsView.RecentsViewCallbacks Implementation ****/
 
     @Override
-    public void onTaskViewClicked() {
-    }
-
-    @Override
     public void onTaskLaunchFailed() {
         // Return to Home
         dismissRecentsToHome(true);
@@ -657,6 +647,17 @@
         mRecentsView.getViewTreeObserver().addOnPreDrawListener(this);
     }
 
+    public final void onBusEvent(CancelEnterRecentsWindowAnimationEvent event) {
+        RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState();
+        int launchToTaskId = launchState.launchedToTaskId;
+        if (launchToTaskId != -1 &&
+                (event.launchTask == null || launchToTaskId != event.launchTask.key.id)) {
+            SystemServicesProxy ssp = Recents.getSystemServices();
+            ssp.cancelWindowTransition(launchState.launchedToTaskId);
+            ssp.cancelThumbnailTransition(getTaskId());
+        }
+    }
+
     public final void onBusEvent(AppWidgetProviderChangedEvent event) {
         refreshSearchWidgetView();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivityLaunchState.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivityLaunchState.java
index 9ee6f5b..01ffd2a 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivityLaunchState.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivityLaunchState.java
@@ -19,7 +19,6 @@
 /**
  * The launch state of the RecentsActivity.
  *
- * TODO: We will be refactoring this out RecentsConfiguration.
  * Current Constraints:
  *  - needed in onStart() before onNewIntent()
  *  - needs to be reset when Recents is hidden
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsAppWidgetHost.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsAppWidgetHost.java
index fc96c11..573db98 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsAppWidgetHost.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsAppWidgetHost.java
@@ -26,6 +26,8 @@
 /** Our special app widget host for the Search widget */
 public class RecentsAppWidgetHost extends AppWidgetHost {
 
+    public static final int HOST_ID = 1024;
+
     boolean mIsListening;
 
     public RecentsAppWidgetHost(Context context, int hostId) {
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
index 675ef2f..db65e00 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
@@ -16,8 +16,6 @@
 
 package com.android.systemui.recents;
 
-import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID;
-
 import android.app.ActivityManager;
 import android.app.ActivityOptions;
 import android.app.ITaskStackListener;
@@ -32,24 +30,24 @@
 import android.os.Handler;
 import android.os.SystemClock;
 import android.os.UserHandle;
+import android.util.Log;
 import android.util.MutableBoolean;
 import android.view.AppTransitionAnimationSpec;
 import android.view.LayoutInflater;
 import android.view.View;
-
 import com.android.internal.logging.MetricsLogger;
 import com.android.systemui.Prefs;
 import com.android.systemui.R;
 import com.android.systemui.SystemUIApplication;
 import com.android.systemui.recents.events.EventBus;
 import com.android.systemui.recents.events.activity.EnterRecentsWindowAnimationStartedEvent;
+import com.android.systemui.recents.events.activity.EnterRecentsWindowLastAnimationFrameEvent;
 import com.android.systemui.recents.events.activity.HideRecentsEvent;
 import com.android.systemui.recents.events.activity.IterateRecentsEvent;
-import com.android.systemui.recents.events.activity.EnterRecentsWindowLastAnimationFrameEvent;
 import com.android.systemui.recents.events.activity.ToggleRecentsEvent;
 import com.android.systemui.recents.events.component.RecentsVisibilityChangedEvent;
 import com.android.systemui.recents.events.component.ScreenPinningRequestEvent;
-import com.android.systemui.recents.misc.Console;
+import com.android.systemui.recents.misc.DozeTrigger;
 import com.android.systemui.recents.misc.ForegroundThread;
 import com.android.systemui.recents.misc.SystemServicesProxy;
 import com.android.systemui.recents.model.RecentsTaskLoadPlan;
@@ -57,14 +55,16 @@
 import com.android.systemui.recents.model.Task;
 import com.android.systemui.recents.model.TaskGrouping;
 import com.android.systemui.recents.model.TaskStack;
-import com.android.systemui.recents.views.TaskStackView;
 import com.android.systemui.recents.views.TaskStackLayoutAlgorithm;
+import com.android.systemui.recents.views.TaskStackView;
 import com.android.systemui.recents.views.TaskViewHeader;
 import com.android.systemui.recents.views.TaskViewTransform;
 import com.android.systemui.statusbar.phone.PhoneStatusBar;
 
 import java.util.ArrayList;
 
+import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID;
+
 /**
  * An implementation of the Recents component for the current user.  For secondary users, this can
  * be called remotely from the system user.
@@ -75,7 +75,12 @@
     private final static String TAG = "RecentsImpl";
     private final static boolean DEBUG = false;
 
-    private final static int sMinToggleDelay = 350;
+    // The minimum amount of time between each recents button press that we will handle
+    private final static int MIN_TOGGLE_DELAY_MS = 350;
+    // The duration within which the user releasing the alt tab (from when they pressed alt tab)
+    // that the fast alt-tab animation will run.  If the user's alt-tab takes longer than this
+    // duration, then we will toggle recents after this duration.
+    private final static int FAST_ALT_TAB_DELAY_MS = 225;
 
     public final static String RECENTS_PACKAGE = "com.android.systemui";
     public final static String RECENTS_ACTIVITY = "com.android.systemui.recents.RecentsActivity";
@@ -156,6 +161,14 @@
     // Variables to keep track of if we need to start recents after binding
     boolean mTriggeredFromAltTab;
     long mLastToggleTime;
+    DozeTrigger mFastAltTabTrigger = new DozeTrigger(FAST_ALT_TAB_DELAY_MS, new Runnable() {
+        @Override
+        public void run() {
+            // When this fires, then the user has not released alt-tab for at least
+            // FAST_ALT_TAB_DELAY_MS milliseconds
+            showRecents(mTriggeredFromAltTab);
+        }
+    });
 
     Bitmap mThumbnailTransitionBitmapCache;
     Task mThumbnailTransitionBitmapCacheKey;
@@ -164,7 +177,7 @@
     public RecentsImpl(Context context) {
         mContext = context;
         mHandler = new Handler();
-        mAppWidgetHost = new RecentsAppWidgetHost(mContext, Constants.Values.App.AppWidgetHostId);
+        mAppWidgetHost = new RecentsAppWidgetHost(mContext, RecentsAppWidgetHost.HOST_ID);
         Resources res = mContext.getResources();
         LayoutInflater inflater = LayoutInflater.from(mContext);
 
@@ -239,6 +252,29 @@
     @Override
     public void showRecents(boolean triggeredFromAltTab) {
         mTriggeredFromAltTab = triggeredFromAltTab;
+        if (mFastAltTabTrigger.hasTriggered()) {
+            // We are calling this from the doze trigger, so just fall through to show Recents
+            mFastAltTabTrigger.resetTrigger();
+        } else if (mFastAltTabTrigger.isDozing()) {
+            // We are dozing but haven't yet triggered, ignore this if this is not another alt-tab,
+            // otherwise, this is an additional tab (alt-tab*), which means that we should trigger
+            // immediately (fall through and disable the pending trigger)
+            // TODO: This is tricky, we need to handle the tab key, but Recents has not yet started
+            //       so we may actually additional signal to handle multiple quick tab cases.  The
+            //       severity of this is inversely proportional to the FAST_ALT_TAB_DELAY_MS
+            //       duration though
+            if (!triggeredFromAltTab) {
+                return;
+            }
+            mFastAltTabTrigger.stopDozing();
+        } else {
+            // Otherwise, the doze trigger is not running, and if this is an alt tab, we should
+            // start the trigger and then wait for the hide (or for it to elapse)
+            if (triggeredFromAltTab) {
+                mFastAltTabTrigger.startDozing();
+                return;
+            }
+        }
 
         try {
             // Check if the top task is in the home stack, and start the recents activity
@@ -249,13 +285,24 @@
                 startRecentsActivity(topTask, isTopTaskHome.value);
             }
         } catch (ActivityNotFoundException e) {
-            Console.logRawError("Failed to launch RecentAppsIntent", e);
+            Log.e(TAG, "Failed to launch RecentsActivity", e);
         }
     }
 
     @Override
     public void hideRecents(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) {
         if (mBootCompleted) {
+            if (triggeredFromAltTab && mFastAltTabTrigger.isDozing()) {
+                // The user has released alt-tab before the trigger has run, so just show the next
+                // task immediately
+                showNextTask();
+
+                // Cancel the fast alt-tab trigger
+                mFastAltTabTrigger.stopDozing();
+                mFastAltTabTrigger.resetTrigger();
+                return;
+            }
+
             // Defer to the activity to handle hiding recents, if it handles it, then it must still
             // be visible
             EventBus.getDefault().post(new HideRecentsEvent(triggeredFromAltTab,
@@ -265,6 +312,11 @@
 
     @Override
     public void toggleRecents() {
+        // Skip this toggle if we are already waiting to trigger recents via alt-tab
+        if (mFastAltTabTrigger.isDozing()) {
+            return;
+        }
+
         mTriggeredFromAltTab = false;
 
         try {
@@ -283,7 +335,7 @@
                     // better than showing a janky screenshot).
                     // NOTE: Ideally, the screenshot mechanism would take the window transform into
                     // account
-                    if ((SystemClock.elapsedRealtime() - mLastToggleTime) < sMinToggleDelay) {
+                    if ((SystemClock.elapsedRealtime() - mLastToggleTime) < MIN_TOGGLE_DELAY_MS) {
                         return;
                     }
 
@@ -296,7 +348,7 @@
                 // better than showing a janky screenshot).
                 // NOTE: Ideally, the screenshot mechanism would take the window transform into
                 // account
-                if ((SystemClock.elapsedRealtime() - mLastToggleTime) < sMinToggleDelay) {
+                if ((SystemClock.elapsedRealtime() - mLastToggleTime) < MIN_TOGGLE_DELAY_MS) {
                     return;
                 }
 
@@ -305,7 +357,7 @@
                 mLastToggleTime = SystemClock.elapsedRealtime();
             }
         } catch (ActivityNotFoundException e) {
-            Console.logRawError("Failed to launch RecentAppsIntent", e);
+            Log.e(TAG, "Failed to launch RecentsActivity", e);
         }
     }
 
@@ -335,10 +387,58 @@
         // Do nothing
     }
 
-    public void showRelativeAffiliatedTask(boolean showNextTask) {
-        // Return early if there is no focused stack
+    /**
+     * Transitions to the next recent task in the stack.
+     */
+    public void showNextTask() {
         SystemServicesProxy ssp = Recents.getSystemServices();
-        int focusedStackId = ssp.getFocusedStack();
+        RecentsTaskLoader loader = Recents.getTaskLoader();
+        RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext);
+        loader.preloadTasks(plan, true /* isTopTaskHome */);
+        TaskStack focusedStack = plan.getTaskStack();
+
+        // Return early if there are no tasks in the focused stack
+        if (focusedStack == null || focusedStack.getTaskCount() == 0) return;
+
+        ActivityManager.RunningTaskInfo runningTask = ssp.getTopMostTask();
+        // Return early if there is no running task
+        if (runningTask == null) return;
+        // Return early if the running task is in the home stack (optimization)
+        if (SystemServicesProxy.isHomeStack(runningTask.stackId)) return;
+
+        // Find the task in the recents list
+        ArrayList<Task> tasks = focusedStack.getTasks();
+        Task toTask = null;
+        ActivityOptions launchOpts = null;
+        int taskCount = tasks.size();
+        for (int i = taskCount - 1; i >= 1; i--) {
+            Task task = tasks.get(i);
+            if (task.key.id == runningTask.id) {
+                toTask = tasks.get(i - 1);
+                launchOpts = ActivityOptions.makeCustomAnimation(mContext,
+                        R.anim.recents_launch_prev_affiliated_task_target,
+                        R.anim.recents_launch_prev_affiliated_task_source);
+                break;
+            }
+        }
+
+        // Return early if there is no next task
+        if (toTask == null) {
+            ssp.startInPlaceAnimationOnFrontMostApplication(
+                    ActivityOptions.makeCustomInPlaceAnimation(mContext,
+                            R.anim.recents_launch_prev_affiliated_task_bounce));
+            return;
+        }
+
+        // Launch the task
+        ssp.startActivityFromRecents(mContext, toTask.key.id, toTask.activityLabel, launchOpts);
+    }
+
+    /**
+     * Transitions to the next affiliated task.
+     */
+    public void showRelativeAffiliatedTask(boolean showNextTask) {
+        SystemServicesProxy ssp = Recents.getSystemServices();
         RecentsTaskLoader loader = Recents.getTaskLoader();
         RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext);
         loader.preloadTasks(plan, true /* isTopTaskHome */);
diff --git a/packages/SystemUI/src/com/android/systemui/recents/events/activity/CancelEnterRecentsWindowAnimationEvent.java b/packages/SystemUI/src/com/android/systemui/recents/events/activity/CancelEnterRecentsWindowAnimationEvent.java
new file mode 100644
index 0000000..7604de1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/events/activity/CancelEnterRecentsWindowAnimationEvent.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2015 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.systemui.recents.events.activity;
+
+import com.android.systemui.recents.events.EventBus;
+import com.android.systemui.recents.model.Task;
+
+/**
+ * This is sent when we want to cancel the enter-recents window animation for the launch task.
+ */
+public class CancelEnterRecentsWindowAnimationEvent extends EventBus.Event {
+
+    // This is set for the task that is launching, which allows us to ensure that we are not
+    // cancelling the same task animation (it will just be overwritten instead)
+    public final Task launchTask;
+
+    public CancelEnterRecentsWindowAnimationEvent(Task launchTask) {
+        this.launchTask = launchTask;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/Console.java b/packages/SystemUI/src/com/android/systemui/recents/misc/Console.java
deleted file mode 100644
index 28ac9d3..0000000
--- a/packages/SystemUI/src/com/android/systemui/recents/misc/Console.java
+++ /dev/null
@@ -1,196 +0,0 @@
-/*
- * Copyright (C) 2014 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.systemui.recents.misc;
-
-
-import android.content.ComponentCallbacks2;
-import android.content.Context;
-import android.util.Log;
-import android.view.MotionEvent;
-import android.widget.Toast;
-
-import java.util.HashMap;
-import java.util.Map;
-
-
-public class Console {
-    // Timer
-    public static final Map<Object, Long> mTimeLogs = new HashMap<Object, Long>();
-
-    // Colors
-    public static final String AnsiReset = "\u001B[0m";
-    public static final String AnsiBlack = "\u001B[30m";
-    public static final String AnsiRed = "\u001B[31m";      // SystemUIHandshake
-    public static final String AnsiGreen = "\u001B[32m";    // MeasureAndLayout
-    public static final String AnsiYellow = "\u001B[33m";   // SynchronizeViewsWithModel
-    public static final String AnsiBlue = "\u001B[34m";     // TouchEvents, Search
-    public static final String AnsiPurple = "\u001B[35m";   // Draw
-    public static final String AnsiCyan = "\u001B[36m";     // ClickEvents
-    public static final String AnsiWhite = "\u001B[37m";
-
-    // Console enabled state
-    public static boolean Enabled = false;
-
-    /** Logs a key */
-    public static void log(String key) {
-        log(true, key, "", AnsiReset);
-    }
-
-    /** Logs a conditioned key */
-    public static void log(boolean condition, String key) {
-        if (condition) {
-            log(condition, key, "", AnsiReset);
-        }
-    }
-
-    /** Logs a key in a specific color */
-    public static void log(boolean condition, String key, Object data) {
-        if (condition) {
-            log(condition, key, data, AnsiReset);
-        }
-    }
-
-    /** Logs a key with data in a specific color */
-    public static void log(boolean condition, String key, Object data, String color) {
-        if (condition) {
-            System.out.println(color + key + AnsiReset + " " + data.toString());
-        }
-    }
-
-    /** Logs an error */
-    public static void logError(Context context, String msg) {
-        Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
-        Log.e("Recents", msg);
-    }
-
-    /** Logs a raw error */
-    public static void logRawError(String msg, Exception e) {
-        Log.e("Recents", msg, e);
-    }
-
-    /** Logs a divider bar */
-    public static void logDivider(boolean condition) {
-        if (condition) {
-            System.out.println("==== [" + System.currentTimeMillis() +
-                    "] ============================================================");
-        }
-    }
-
-    /** Starts a time trace */
-    public static void logStartTracingTime(boolean condition, String key) {
-        if (condition) {
-            long curTime = System.currentTimeMillis();
-            mTimeLogs.put(key, curTime);
-            Console.log(condition, "[Recents|" + key + "]",
-                    "started @ " + curTime);
-        }
-    }
-
-    /** Continues a time trace */
-    public static void logTraceTime(boolean condition, String key, String desc) {
-        if (condition) {
-            long timeDiff = System.currentTimeMillis() - mTimeLogs.get(key);
-            Console.log(condition, "[Recents|" + key + "|" + desc + "]",
-                    "+" + timeDiff + "ms");
-        }
-    }
-
-    /** Logs a stack trace */
-    public static void logStackTrace() {
-        logStackTrace("", 99);
-    }
-
-    /** Logs a stack trace to a certain depth */
-    public static void logStackTrace(int depth) {
-        logStackTrace("", depth);
-    }
-
-    /** Logs a stack trace to a certain depth with a key */
-    public static void logStackTrace(String key, int depth) {
-        int offset = 0;
-        StackTraceElement[] callStack = Thread.currentThread().getStackTrace();
-        String tinyStackTrace = "";
-        // Skip over the known stack trace classes
-        for (int i = 0; i < callStack.length; i++) {
-            StackTraceElement el = callStack[i];
-            String className = el.getClassName();
-            if (className.indexOf("dalvik.system.VMStack") == -1 &&
-                className.indexOf("java.lang.Thread") == -1 &&
-                className.indexOf("recents.Console") == -1) {
-                break;
-            } else {
-                offset++;
-            }
-        }
-        // Build the pretty stack trace
-        int start = Math.min(offset + depth, callStack.length);
-        int end = offset;
-        String indent = "";
-        for (int i = start - 1; i >= end; i--) {
-            StackTraceElement el = callStack[i];
-            tinyStackTrace += indent + " -> " + el.getClassName() +
-                    "[" + el.getLineNumber() + "]." + el.getMethodName();
-            if (i > end) {
-                tinyStackTrace += "\n";
-                indent += "  ";
-            }
-        }
-        log(true, key, tinyStackTrace, AnsiRed);
-    }
-
-
-    /** Returns the stringified MotionEvent action */
-    public static String motionEventActionToString(int action) {
-        switch (action) {
-            case MotionEvent.ACTION_DOWN:
-                return "Down";
-            case MotionEvent.ACTION_UP:
-                return "Up";
-            case MotionEvent.ACTION_MOVE:
-                return "Move";
-            case MotionEvent.ACTION_CANCEL:
-                return "Cancel";
-            case MotionEvent.ACTION_POINTER_DOWN:
-                return "Pointer Down";
-            case MotionEvent.ACTION_POINTER_UP:
-                return "Pointer Up";
-            default:
-                return "" + action;
-        }
-    }
-
-    public static String trimMemoryLevelToString(int level) {
-        switch (level) {
-            case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:
-                return "UI Hidden";
-            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
-                return "Running Moderate";
-            case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
-                return "Background";
-            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
-                return "Running Low";
-            case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
-                return "Moderate";
-            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:
-                return "Critical";
-            case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:
-                return "Complete";
-            default:
-                return "" + level;
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/DozeTrigger.java b/packages/SystemUI/src/com/android/systemui/recents/misc/DozeTrigger.java
index 336d2db..dc8f547 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/misc/DozeTrigger.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/misc/DozeTrigger.java
@@ -29,44 +29,53 @@
     boolean mIsDozing;
     boolean mHasTriggered;
     int mDozeDurationMilliseconds;
-    Runnable mSleepRunnable;
+    Runnable mOnSleepRunnable;
 
     // Sleep-runnable
     Runnable mDozeRunnable = new Runnable() {
         @Override
         public void run() {
-            mSleepRunnable.run();
             mIsDozing = false;
             mHasTriggered = true;
+            mOnSleepRunnable.run();
         }
     };
 
-    public DozeTrigger(int dozeDurationMilliseconds, Runnable sleepRunnable) {
+    public DozeTrigger(int dozeDurationMilliseconds, Runnable onSleepRunnable) {
         mHandler = new Handler();
         mDozeDurationMilliseconds = dozeDurationMilliseconds;
-        mSleepRunnable = sleepRunnable;
+        mOnSleepRunnable = onSleepRunnable;
     }
 
-    /** Starts dozing. This also resets the trigger flag. */
+    /**
+     * Starts dozing and queues the onSleepRunnable to be called. This also resets the trigger flag.
+     */
     public void startDozing() {
         forcePoke();
         mHasTriggered = false;
     }
 
-    /** Stops dozing. */
+    /**
+     * Stops dozing and prevents the onSleepRunnable from being called.
+     */
     public void stopDozing() {
         mHandler.removeCallbacks(mDozeRunnable);
         mIsDozing = false;
     }
 
-    /** Poke this dozer to wake it up for a little bit, if it is dozing. */
+    /**
+     * Poke this dozer to wake it up if it is dozing, delaying the onSleepRunnable from being
+     * called for a for the doze duration.
+     */
     public void poke() {
         if (mIsDozing) {
             forcePoke();
         }
     }
 
-    /** Poke this dozer to wake it up for a little bit. */
+    /**
+     * Poke this dozer to wake it up even if it is not currently dozing.
+     */
     void forcePoke() {
         mHandler.removeCallbacks(mDozeRunnable);
         mHandler.postDelayed(mDozeRunnable, mDozeDurationMilliseconds);
diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/ReferenceCountedTrigger.java b/packages/SystemUI/src/com/android/systemui/recents/misc/ReferenceCountedTrigger.java
index c87a901..401b448 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/misc/ReferenceCountedTrigger.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/misc/ReferenceCountedTrigger.java
@@ -19,6 +19,7 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.content.Context;
+import android.util.Log;
 
 import java.util.ArrayList;
 
@@ -28,6 +29,8 @@
  */
 public class ReferenceCountedTrigger {
 
+    private static final String TAG = "ReferenceCountedTrigger";
+
     Context mContext;
     int mCount;
     ArrayList<Runnable> mFirstIncRunnables = new ArrayList<Runnable>();
@@ -94,8 +97,7 @@
             if (mErrorRunnable != null) {
                 mErrorRunnable.run();
             } else {
-                new Throwable("Invalid ref count").printStackTrace();
-                Console.logError(mContext, "Invalid ref count");
+                Log.e(TAG, "Invalid ref count");
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java b/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java
index 4ee0357..0432ac9 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java
@@ -363,6 +363,19 @@
         }
     }
 
+    /**
+     * Cancels the current thumbnail transtion to/from Recents for the given task id.
+     */
+    public void cancelThumbnailTransition(int taskId) {
+        if (mWm == null) return;
+
+        try {
+            WindowManagerGlobal.getWindowManagerService().cancelTaskThumbnailTransition(taskId);
+        } catch (RemoteException e) {
+            e.printStackTrace();
+        }
+    }
+
     /** Returns the top task thumbnail for the given task id */
     public Bitmap getTaskThumbnail(int taskId) {
         if (mAm == null) return null;
@@ -775,8 +788,7 @@
                         taskId, INVALID_STACK_ID, options == null ? null : options.toBundle());
                 return true;
             } catch (Exception e) {
-                Console.logError(context,
-                        context.getString(R.string.recents_launch_error_message, taskName));
+                Log.e(TAG, context.getString(R.string.recents_launch_error_message, taskName), e);
             }
         }
         return false;
diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java
index 495c8fd..6734012 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java
@@ -17,14 +17,12 @@
 package com.android.systemui.recents.model;
 
 import android.animation.ObjectAnimator;
-import android.app.ActivityManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.graphics.Color;
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.drawable.ColorDrawable;
-import android.util.Log;
 import com.android.systemui.R;
 import com.android.systemui.recents.Constants;
 import com.android.systemui.recents.Recents;
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
index 9a5e141..5f94fa7 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
@@ -51,6 +51,7 @@
 import com.android.systemui.recents.RecentsAppWidgetHostView;
 import com.android.systemui.recents.RecentsConfiguration;
 import com.android.systemui.recents.events.EventBus;
+import com.android.systemui.recents.events.activity.CancelEnterRecentsWindowAnimationEvent;
 import com.android.systemui.recents.events.activity.DismissRecentsToHomeAnimationStarted;
 import com.android.systemui.recents.events.component.ScreenPinningRequestEvent;
 import com.android.systemui.recents.events.ui.DismissTaskViewEvent;
@@ -64,8 +65,8 @@
 import java.util.ArrayList;
 import java.util.List;
 
-import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID;
 import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID;
+import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID;
 import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
 
 /**
@@ -82,7 +83,6 @@
 
     /** The RecentsView callbacks */
     public interface RecentsViewCallbacks {
-        public void onTaskViewClicked();
         public void onTaskLaunchFailed();
         public void onAllTaskViewsDismissed();
     }
@@ -107,6 +107,7 @@
 
     Rect mSystemInsets = new Rect();
 
+
     @GuardedBy("this")
     List<AppTransitionAnimationSpec> mAppTransitionAnimationSpecs;
 
@@ -246,6 +247,9 @@
         }
         ctx.postAnimationTrigger.decrement();
 
+        // If we are going home, cancel the previous task's window transition
+        EventBus.getDefault().send(new CancelEnterRecentsWindowAnimationEvent(null));
+
         // Notify of the exit animation
         EventBus.getDefault().send(new DismissRecentsToHomeAnimationStarted());
     }
@@ -555,30 +559,12 @@
         return new AppTransitionAnimationSpec(taskId, b, rect);
     }
 
-    /**
-     * Cancels any running window transitions for the launched task (the task animating into
-     * Recents).
-     */
-    private void cancelLaunchedTaskWindowTransition(final Task task) {
-        SystemServicesProxy ssp = Recents.getSystemServices();
-        RecentsConfiguration config = Recents.getConfiguration();
-        RecentsActivityLaunchState launchState = config.getLaunchState();
-        if (launchState.launchedToTaskId != -1 &&
-                launchState.launchedToTaskId != task.key.id) {
-            ssp.cancelWindowTransition(launchState.launchedToTaskId);
-        }
-    }
-
     /**** TaskStackView.TaskStackCallbacks Implementation ****/
 
     @Override
     public void onTaskViewClicked(final TaskStackView stackView, final TaskView tv,
             final TaskStack stack, final Task task, final boolean lockToTask,
             final boolean boundsValid, final Rect bounds, int destinationStack) {
-        // Notify any callbacks of the launching of a new task
-        if (mCb != null) {
-            mCb.onTaskViewClicked();
-        }
 
         // Upfront the processing of the thumbnail
         TaskViewTransform transform = new TaskViewTransform();
@@ -610,7 +596,7 @@
                 public void onAnimationStarted() {
                     // If we are launching into another task, cancel the previous task's
                     // window transition
-                    cancelLaunchedTaskWindowTransition(task);
+                    EventBus.getDefault().send(new CancelEnterRecentsWindowAnimationEvent(task));
 
                     if (lockToTask) {
                         // Request screen pinning after the animation runs
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
index 7f5c768..51091c3 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
@@ -31,7 +31,6 @@
 
 import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.LinkedList;
 
 
 /**
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
index fc3a681..3ef597f7 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
@@ -34,7 +34,6 @@
 import android.view.animation.Interpolator;
 import android.widget.FrameLayout;
 import com.android.systemui.R;
-import com.android.systemui.recents.Constants;
 import com.android.systemui.recents.Recents;
 import com.android.systemui.recents.RecentsActivity;
 import com.android.systemui.recents.RecentsActivityLaunchState;
@@ -228,6 +227,42 @@
         return mImmutableTaskViews;
     }
 
+    /**
+     * Returns the front most task view.
+     *
+     * @param stackTasksOnly if set, will return the front most task view in the stack (by default
+     *                       the front most task view will be freeform since they are placed above
+     *                       stack tasks)
+     */
+    private TaskView getFrontMostTaskView(boolean stackTasksOnly) {
+        List<TaskView> taskViews = getTaskViews();
+        int taskViewCount = taskViews.size();
+        for (int i = taskViewCount - 1; i >= 0; i--) {
+            TaskView tv = taskViews.get(i);
+            Task task = tv.getTask();
+            if (stackTasksOnly && task.isFreeformTask()) {
+                continue;
+            }
+            return tv;
+        }
+        return null;
+    }
+
+    /**
+     * Finds the child view given a specific {@param task}.
+     */
+    public TaskView getChildViewForTask(Task t) {
+        List<TaskView> taskViews = getTaskViews();
+        int taskViewCount = taskViews.size();
+        for (int i = 0; i < taskViewCount; i++) {
+            TaskView tv = taskViews.get(i);
+            if (tv.getTask() == t) {
+                return tv;
+            }
+        }
+        return null;
+    }
+
     /** Resets this TaskStackView for reuse. */
     void reset() {
         // Reset the focused task
@@ -288,19 +323,6 @@
         }
     }
 
-    /** Finds the child view given a specific task. */
-    public TaskView getChildViewForTask(Task t) {
-        List<TaskView> taskViews = getTaskViews();
-        int taskViewCount = taskViews.size();
-        for (int i = 0; i < taskViewCount; i++) {
-            TaskView tv = taskViews.get(i);
-            if (tv.getTask() == t) {
-                return tv;
-            }
-        }
-        return null;
-    }
-
     /** Returns the stack algorithm for this task stack. */
     public TaskStackLayoutAlgorithm getStackAlgorithm() {
         return mLayoutAlgorithm;
@@ -635,13 +657,49 @@
     /**
      * Sets the focused task relative to the currently focused task.
      *
+     * @param stackTasksOnly if set, will ensure that the traversal only goes along stack tasks, and
+     *                       if the currently focused task is not a stack task, will set the focus
+     *                       to the first visible stack task
      * @param animated determines whether to actually draw the highlight along with the change in
      *                            focus.
      */
-    public void setRelativeFocusedTask(boolean forward, boolean animated) {
-        // Find the next index to focus
-        int newIndex = mFocusedTaskIndex + (forward ? -1 : 1);
-        setFocusedTask(newIndex, true, animated);
+    public void setRelativeFocusedTask(boolean forward, boolean stackTasksOnly, boolean animated) {
+        int newIndex = -1;
+        if (mFocusedTaskIndex != -1) {
+            if (stackTasksOnly) {
+                List<Task> tasks =  mStack.getTasks();
+                newIndex = mFocusedTaskIndex;
+                Task task = tasks.get(mFocusedTaskIndex);
+                if (task.isFreeformTask()) {
+                    // Try and focus the front most stack task
+                    TaskView tv = getFrontMostTaskView(stackTasksOnly);
+                    if (tv != null) {
+                        newIndex = mStack.indexOfTask(tv.getTask());
+                    }
+                } else {
+                    // Try the next task if it is a stack task
+                    int tmpNewIndex = mFocusedTaskIndex + (forward ? -1 : 1);
+                    if (0 <= tmpNewIndex && tmpNewIndex < tasks.size()) {
+                        Task t = tasks.get(tmpNewIndex);
+                        if (!t.isFreeformTask()) {
+                            newIndex = tmpNewIndex;
+                        }
+                    }
+                }
+            } else {
+                // No restrictions, lets just move to the new task
+                newIndex = mFocusedTaskIndex + (forward ? -1 : 1);
+            }
+        } else {
+            // We don't have a focused task, so focus the first visible task view
+            TaskView tv = getFrontMostTaskView(stackTasksOnly);
+            if (tv != null) {
+                newIndex = mStack.indexOfTask(tv.getTask());
+            }
+        }
+        if (newIndex != -1) {
+            setFocusedTask(newIndex, true, animated);
+        }
     }
 
     /**
@@ -703,11 +761,11 @@
         }
         switch (action) {
             case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
-                setRelativeFocusedTask(true, false /* animated */);
+                setRelativeFocusedTask(true, false /* stackTasksOnly */, false /* animated */);
                 return true;
             }
             case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
-                setRelativeFocusedTask(false, false /* animated */);
+                setRelativeFocusedTask(false, false /* stackTasksOnly */, false /* animated */);
                 return true;
             }
         }
@@ -1324,11 +1382,11 @@
     }
 
     public final void onBusEvent(FocusNextTaskViewEvent event) {
-        setRelativeFocusedTask(true, true);
+        setRelativeFocusedTask(true, false /* stackTasksOnly */, true /* animated */);
     }
 
     public final void onBusEvent(FocusPreviousTaskViewEvent event) {
-        setRelativeFocusedTask(false, true);
+        setRelativeFocusedTask(false, false /* stackTasksOnly */, true /* animated */);
     }
 
     public final void onBusEvent(DismissFocusedTaskViewEvent event) {
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewFilterAlgorithm.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewFilterAlgorithm.java
index a32b242..45f573d 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewFilterAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewFilterAlgorithm.java
@@ -17,7 +17,6 @@
 package com.android.systemui.recents.views;
 
 import com.android.systemui.R;
-import com.android.systemui.recents.Constants;
 import com.android.systemui.recents.model.Task;
 
 import java.util.ArrayList;
@@ -113,7 +112,7 @@
                     tv.prepareTaskTransformForFilterTaskHidden(fromTransform);
                     tv.updateViewPropertiesToTaskTransform(fromTransform, 0);
 
-                    toTransform.startDelay = offset * Constants.Values.TaskStackView.FilterStartDelay;
+                    toTransform.startDelay = offset * 25;
                     childViewTransformsOut.put(tv, toTransform);
 
                     // Use the movement of the new views to calculate the duration of the animation
@@ -166,7 +165,7 @@
                         (int) tv.getTranslationY()));
             }
 
-            toTransform.startDelay = offset * Constants.Values.TaskStackView.FilterStartDelay;
+            toTransform.startDelay = offset * 25;
             childViewTransformsOut.put(tv, toTransform);
             offset++;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
index 59c9708..81c89a1 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
@@ -31,6 +31,7 @@
 import com.android.systemui.R;
 import com.android.systemui.recents.Constants;
 import com.android.systemui.recents.Recents;
+import com.android.systemui.recents.RecentsActivityLaunchState;
 import com.android.systemui.recents.events.EventBus;
 import com.android.systemui.recents.events.activity.HideRecentsEvent;
 import com.android.systemui.recents.events.ui.DismissTaskViewEvent;
@@ -149,6 +150,11 @@
         if (mSv.getTaskViews().size() == 0) {
             return false;
         }
+        // Short circuit while we are alt-tabbing
+        RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState();
+        if (launchState.launchedWithAltTab) {
+            return false;
+        }
 
         final TaskStackLayoutAlgorithm layoutAlgorithm = mSv.mLayoutAlgorithm;
         int action = ev.getAction();
@@ -305,9 +311,11 @@
                     // Find the front most task and scroll the next task to the front
                     float vScroll = ev.getAxisValue(MotionEvent.AXIS_VSCROLL);
                     if (vScroll > 0) {
-                        mSv.setRelativeFocusedTask(true, false /* animated */);
+                        mSv.setRelativeFocusedTask(true, true /* stackTasksOnly */,
+                                false /* animated */);
                     } else {
-                        mSv.setRelativeFocusedTask(false, false /* animated */);
+                        mSv.setRelativeFocusedTask(false, true /* stackTasksOnly */,
+                                false /* animated */);
                     }
                     return true;
             }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
index dd1474d..4f4b91a 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
@@ -29,6 +29,7 @@
 import android.graphics.Point;
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffColorFilter;
+import android.graphics.Rect;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.MotionEvent;
@@ -80,7 +81,6 @@
     boolean mTaskDataLoaded;
     boolean mIsFocused;
     boolean mIsFocusAnimated;
-    boolean mFocusAnimationsEnabled;
     boolean mClipViewInStack;
     AnimateableViewBounds mViewBounds;
 
@@ -642,6 +642,21 @@
         setDim(getDimFromTaskProgress());
     }
 
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        super.dispatchDraw(canvas);
+        if (Constants.DebugFlags.App.EnableFastToggleRecents && mIsFocused) {
+            Paint tmpPaint = new Paint();
+            Rect tmpRect = new Rect();
+            tmpRect.set(0, 0, getWidth(), getHeight());
+            tmpPaint.setColor(0xFFFF0000);
+            tmpPaint.setStrokeWidth(35);
+            tmpPaint.setStyle(Paint.Style.STROKE);
+            canvas.drawRect(tmpRect, tmpPaint);
+        }
+    }
+
     /**** View focus state ****/
 
     /**
@@ -672,6 +687,9 @@
                 clearAccessibilityFocus();
             }
         }
+        if (Constants.DebugFlags.App.EnableFastToggleRecents) {
+            invalidate();
+        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java
index 3d6c600..59d4011 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java
@@ -303,6 +303,10 @@
         if (mTmpRect.equals(mLastResizeRect)) {
             return;
         }
+
+        // Make sure shadows are updated
+        mBackground.invalidate();
+
         mLastResizeRect.set(mTmpRect);
         mWindowManagerProxy.resizeDockedStack(mTmpRect);
     }
diff --git a/services/core/java/com/android/server/am/TaskRecord.java b/services/core/java/com/android/server/am/TaskRecord.java
index c35b91f..031f6d2 100644
--- a/services/core/java/com/android/server/am/TaskRecord.java
+++ b/services/core/java/com/android/server/am/TaskRecord.java
@@ -319,7 +319,7 @@
         mCallingPackage = callingPackage;
         mResizeable = resizeable || mService.mForceResizableActivites;
         mPrivileged = privileged;
-        ActivityInfo info = mActivities.get(0).info;
+        ActivityInfo info = (mActivities.size() > 0) ? mActivities.get(0).info : null;
         mMinimalSize = info != null && info.layout != null ? info.layout.minimalSize : -1;
     }
 
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index a6a34af..3eb2d11 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -635,7 +635,7 @@
             }
             if (info != null && (info.flags&UserInfo.FLAG_INITIALIZED) == 0) {
                 info.flags |= UserInfo.FLAG_INITIALIZED;
-                scheduleWriteUserLP(info);
+                scheduleWriteUser(info);
             }
         }
     }
@@ -775,6 +775,7 @@
             Preconditions.checkState(mCachedEffectiveUserRestrictions.get(userId)
                     != newRestrictions);
             mBaseUserRestrictions.put(userId, newRestrictions);
+            scheduleWriteUser(mUsers.get(userId));
         }
 
         final Bundle effective = computeEffectiveUserRestrictionsLR(userId);
@@ -1082,7 +1083,7 @@
             UserInfo user = getUserInfoNoChecks(UserHandle.USER_SYSTEM);
             if ("Primary".equals(user.name)) {
                 user.name = mContext.getResources().getString(com.android.internal.R.string.owner_name);
-                scheduleWriteUserLP(user);
+                scheduleWriteUser(user);
             }
             userVersion = 1;
         }
@@ -1092,7 +1093,7 @@
             UserInfo user = getUserInfoNoChecks(UserHandle.USER_SYSTEM);
             if ((user.flags & UserInfo.FLAG_INITIALIZED) == 0) {
                 user.flags |= UserInfo.FLAG_INITIALIZED;
-                scheduleWriteUserLP(user);
+                scheduleWriteUser(user);
             }
             userVersion = 2;
         }
@@ -1116,7 +1117,7 @@
                     if (!splitSystemUser && user.isRestricted()
                             && (user.restrictedProfileParentId == UserInfo.NO_PROFILE_GROUP_ID)) {
                         user.restrictedProfileParentId = UserHandle.USER_SYSTEM;
-                        scheduleWriteUserLP(user);
+                        scheduleWriteUser(user);
                     }
                 }
             }
@@ -1161,7 +1162,9 @@
         writeUserLP(system);
     }
 
-    private void scheduleWriteUserLP(UserInfo userInfo) {
+    private void scheduleWriteUser(UserInfo userInfo) {
+        // No need to wrap it within a lock -- worst case, we'll just post the same message
+        // twice.
         if (!mHandler.hasMessages(WRITE_USER_MSG, userInfo)) {
             Message msg = mHandler.obtainMessage(WRITE_USER_MSG, userInfo);
             mHandler.sendMessageDelayed(msg, WRITE_USER_DELAY);
@@ -1543,7 +1546,7 @@
                         if (isManagedProfile) {
                             if (parent.profileGroupId == UserInfo.NO_PROFILE_GROUP_ID) {
                                 parent.profileGroupId = parent.id;
-                                scheduleWriteUserLP(parent);
+                                scheduleWriteUser(parent);
                             }
                             userInfo.profileGroupId = parent.profileGroupId;
                         } else if (isRestricted) {
@@ -1552,7 +1555,7 @@
                             }
                             if (parent.restrictedProfileParentId == UserInfo.NO_PROFILE_GROUP_ID) {
                                 parent.restrictedProfileParentId = parent.id;
-                                scheduleWriteUserLP(parent);
+                                scheduleWriteUser(parent);
                             }
                             userInfo.restrictedProfileParentId = parent.restrictedProfileParentId;
                         }
@@ -1571,7 +1574,7 @@
                     }
                     mPm.createNewUserLILPw(userId);
                     userInfo.partial = false;
-                    scheduleWriteUserLP(userInfo);
+                    scheduleWriteUser(userInfo);
                     updateUserIds();
                     Bundle restrictions = new Bundle();
                     synchronized (mRestrictionsLock) {
@@ -2148,7 +2151,7 @@
             }
             if (now > EPOCH_PLUS_30_YEARS) {
                 user.lastLoggedInTime = now;
-                scheduleWriteUserLP(user);
+                scheduleWriteUser(user);
             }
         }
     }
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 5864b25..6aaf3c7 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -328,6 +328,15 @@
         }
     }
 
+    /**
+     * Cancels any running thumbnail transitions associated with the task.
+     */
+    void cancelTaskThumbnailTransition() {
+        for (int activityNdx = mAppTokens.size() - 1; activityNdx >= 0; --activityNdx) {
+            mAppTokens.get(activityNdx).mAppAnimator.clearThumbnail();
+        }
+    }
+
     boolean showForAllUsers() {
         final int tokensCount = mAppTokens.size();
         return (tokensCount != 0) && mAppTokens.get(tokensCount - 1).showForAllUsers;
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index bc34ba7..3205c6c 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -4690,6 +4690,16 @@
         }
     }
 
+    @Override
+    public void cancelTaskThumbnailTransition(int taskId) {
+        synchronized (mWindowMap) {
+            Task task = mTaskIdToTask.get(taskId);
+            if (task != null) {
+                task.cancelTaskThumbnailTransition();
+            }
+        }
+    }
+
     public void addTask(int taskId, int stackId, boolean toTop) {
         synchronized (mWindowMap) {
             if (DEBUG_STACK) Slog.i(TAG, "addTask: adding taskId=" + taskId
diff --git a/tests/NetworkSecurityConfigTest/AndroidManifest.xml b/tests/NetworkSecurityConfigTest/AndroidManifest.xml
index 811a3f4..4c1fbd3 100644
--- a/tests/NetworkSecurityConfigTest/AndroidManifest.xml
+++ b/tests/NetworkSecurityConfigTest/AndroidManifest.xml
@@ -15,7 +15,7 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.security.tests"
+          package="android.security.net.config"
           android:sharedUserId="android.uid.system">
 
     <application>
@@ -23,7 +23,7 @@
     </application>
 
     <instrumentation android:name="android.test.InstrumentationTestRunner"
-        android:targetPackage="android.security.tests"
+        android:targetPackage="android.security.net.config"
         android:label="ANSC Tests">
     </instrumentation>
 </manifest>
diff --git a/tests/NetworkSecurityConfigTest/res/raw/ca_certs_der.der b/tests/NetworkSecurityConfigTest/res/raw/ca_certs_der.der
new file mode 100644
index 0000000..235bd47
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/raw/ca_certs_der.der
Binary files differ
diff --git a/tests/NetworkSecurityConfigTest/res/raw/ca_certs_pem.pem b/tests/NetworkSecurityConfigTest/res/raw/ca_certs_pem.pem
new file mode 100644
index 0000000..413e3c0
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/raw/ca_certs_pem.pem
@@ -0,0 +1,35 @@
+-----BEGIN CERTIFICATE-----
+MIIDfTCCAuagAwIBAgIDErvmMA0GCSqGSIb3DQEBBQUAME4xCzAJBgNVBAYTAlVT
+MRAwDgYDVQQKEwdFcXVpZmF4MS0wKwYDVQQLEyRFcXVpZmF4IFNlY3VyZSBDZXJ0
+aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDIwNTIxMDQwMDAwWhcNMTgwODIxMDQwMDAw
+WjBCMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UE
+AxMSR2VvVHJ1c3QgR2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEA2swYYzD99BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9m
+OSm9BXiLnTjoBbdqfnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIu
+T8rxh0PBFpVXLVDviS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6c
+JmTM386DGXHKTubU1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmR
+Cw7+OC7RHQWa9k0+bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5asz
+PeE4uwc2hGKceeoWMPRfwCvocWvk+QIDAQABo4HwMIHtMB8GA1UdIwQYMBaAFEjm
+aPkr0rKV10fYIyAQTzOYkJ/UMB0GA1UdDgQWBBTAephojYn7qwVkDBF9qn1luMrM
+TjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjA6BgNVHR8EMzAxMC+g
+LaArhilodHRwOi8vY3JsLmdlb3RydXN0LmNvbS9jcmxzL3NlY3VyZWNhLmNybDBO
+BgNVHSAERzBFMEMGBFUdIAAwOzA5BggrBgEFBQcCARYtaHR0cHM6Ly93d3cuZ2Vv
+dHJ1c3QuY29tL3Jlc291cmNlcy9yZXBvc2l0b3J5MA0GCSqGSIb3DQEBBQUAA4GB
+AHbhEm5OSxYShjAGsoEIz/AIx8dxfmbuwu3UOx//8PDITtZDOLC5MH0Y0FWDomrL
+NhGc6Ehmo21/uBPUR/6LWlxz/K7ZGzIZOKuXNBSqltLroxwUCEm2u+WR74M26x1W
+b8ravHNjkOR/ez4iyz0H7V84dJzjA1BOoa+Y7mHyhD8S
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICPDCCAaUCEDyRMcsf9tAbDpq40ES/Er4wDQYJKoZIhvcNAQEFBQAwXzELMAkG
+A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz
+cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2
+MDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV
+BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt
+YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN
+ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE
+BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is
+I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G
+CSqGSIb3DQEBBQUAA4GBABByUqkFFBkyCEHwxWsKzH4PIRnN5GfcX6kb5sroc50i
+2JhucwNhkcV8sEVAbkSdjbCxlnRhLQ2pRdKkkirWmnWXbj9T/UWZYB2oK0z5XqcJ
+2HUw19JlYD1n1khVdWk/kfVIC0dpImmClr7JyDiGSnoscxlIaU5rfGW/D/xwzoiQ
+-----END CERTIFICATE-----
diff --git a/tests/NetworkSecurityConfigTest/res/xml/attributes.xml b/tests/NetworkSecurityConfigTest/res/xml/attributes.xml
new file mode 100644
index 0000000..eff13c8
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/attributes.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <base-config cleartextTrafficPermitted="false" hstsEnforced="true">
+  </base-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/bad_config0.xml b/tests/NetworkSecurityConfigTest/res/xml/bad_config0.xml
new file mode 100644
index 0000000..6af855d
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/bad_config0.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <domain-config>
+    <domain>android.com</domain>
+    <pin-set>
+      <!-- Bad pin digest -->
+      <pin digest="I am probably not an algorithm">1HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
+    </pin-set>
+  </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/bad_config1.xml b/tests/NetworkSecurityConfigTest/res/xml/bad_config1.xml
new file mode 100644
index 0000000..d683b74
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/bad_config1.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <domain-config>
+    <domain>android.com</domain>
+    <pin-set>
+    <!-- Unknown pin digest -->
+      <pin>1HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
+    </pin-set>
+  </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/bad_config2.xml b/tests/NetworkSecurityConfigTest/res/xml/bad_config2.xml
new file mode 100644
index 0000000..6f3f8b4
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/bad_config2.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <domain-config>
+    <domain>android.com</domain>
+    <pin-set>
+      <!-- empty digest -->
+      <pin digest="SHA-256"></pin>
+    </pin-set>
+  </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/bad_config3.xml b/tests/NetworkSecurityConfigTest/res/xml/bad_config3.xml
new file mode 100644
index 0000000..fb2126c
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/bad_config3.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <domain-config>
+    <domain>android.com</domain>
+  </domain-config>
+  <domain-config>
+    <!-- Same domain name used in two configs -->
+    <domain>android.com</domain>
+  </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/bad_config4.xml b/tests/NetworkSecurityConfigTest/res/xml/bad_config4.xml
new file mode 100644
index 0000000..95972ce
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/bad_config4.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <base-config>
+    <!-- domains are not allowed in base-config -->
+    <domain>android.com</domain>
+  </base-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/bad_config5.xml b/tests/NetworkSecurityConfigTest/res/xml/bad_config5.xml
new file mode 100644
index 0000000..8b6b721
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/bad_config5.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <base-config>
+    <!-- pins are not allowed in base-config -->
+    <pin-set>
+    </pin-set>
+  </base-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/bad_pin.xml b/tests/NetworkSecurityConfigTest/res/xml/bad_pin.xml
new file mode 100644
index 0000000..62a7b88
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/bad_pin.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <domain-config>
+    <domain>android.com</domain>
+    <pin-set>
+      <pin digest="SHA-256">1HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
+    </pin-set>
+  </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/domain1.xml b/tests/NetworkSecurityConfigTest/res/xml/domain1.xml
new file mode 100644
index 0000000..6d8565c
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/domain1.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <base-config>
+    <trust-anchors>
+    </trust-anchors>
+  </base-config>
+  <domain-config>
+    <domain>android.com</domain>
+    <trust-anchors>
+      <certificates src="system" />
+      <certificates src="user" />
+    </trust-anchors>
+  </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/empty_config.xml b/tests/NetworkSecurityConfigTest/res/xml/empty_config.xml
new file mode 100644
index 0000000..1bd94b6
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/empty_config.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/empty_trust.xml b/tests/NetworkSecurityConfigTest/res/xml/empty_trust.xml
new file mode 100644
index 0000000..8093b9d
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/empty_trust.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <base-config>
+    <trust-anchors>
+    </trust-anchors>
+  </base-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/expired_pin.xml b/tests/NetworkSecurityConfigTest/res/xml/expired_pin.xml
new file mode 100644
index 0000000..f9f8465
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/expired_pin.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <domain-config>
+    <domain>android.com</domain>
+    <!-- Invalid pin that has expired -->
+    <pin-set expiration="2015-01-01">
+      <pin digest="SHA-256">aaaaaaaaaaa2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
+    </pin-set>
+  </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/multiple_configs.xml b/tests/NetworkSecurityConfigTest/res/xml/multiple_configs.xml
new file mode 100644
index 0000000..df08467
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/multiple_configs.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <base-config>
+    <trust-anchors>
+    </trust-anchors>
+  </base-config>
+  <domain-config>
+    <domain>android.com</domain>
+    <trust-anchors>
+      <certificates src="system" />
+      <certificates src="user" />
+    </trust-anchors>
+  </domain-config>
+  <domain-config>
+    <domain>google.com</domain>
+    <trust-anchors>
+      <certificates src="system" />
+      <certificates src="user" />
+    </trust-anchors>
+  </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/multiple_domains.xml b/tests/NetworkSecurityConfigTest/res/xml/multiple_domains.xml
new file mode 100644
index 0000000..9743c5f
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/multiple_domains.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <base-config>
+    <trust-anchors>
+    </trust-anchors>
+  </base-config>
+  <domain-config>
+    <domain>android.com</domain>
+    <domain>google.com</domain>
+    <trust-anchors>
+      <certificates src="system" />
+      <certificates src="user" />
+    </trust-anchors>
+  </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/nested_domains.xml b/tests/NetworkSecurityConfigTest/res/xml/nested_domains.xml
new file mode 100644
index 0000000..d45fd77
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/nested_domains.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+    <domain-config>
+      <domain includeSubdomains="true">android.com</domain>
+      <trust-anchors>
+          <certificates src="system" />
+      </trust-anchors>
+      <!-- nested config that adds pins -->
+      <domain-config>
+          <domain>developer.android.com</domain>
+          <pin-set>
+              <pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
+          </pin-set>
+      </domain-config>
+    </domain-config>
+    <base-config cleartextTrafficPermitted="false">
+    </base-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/nested_domains_override.xml b/tests/NetworkSecurityConfigTest/res/xml/nested_domains_override.xml
new file mode 100644
index 0000000..84e06e3
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/nested_domains_override.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+    <base-config cleartextTrafficPermitted="false">
+    </base-config>
+    <!-- Nested config that overrides parent -->
+    <domain-config cleartextTrafficPermitted="true">
+        <domain includeSubdomains="true">android.com</domain>
+        <domain-config cleartextTrafficPermitted="false">
+            <domain>developer.android.com</domain>
+        </domain-config>
+    </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/override_pins.xml b/tests/NetworkSecurityConfigTest/res/xml/override_pins.xml
new file mode 100644
index 0000000..785714a
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/override_pins.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <domain-config>
+    <domain>android.com</domain>
+    <pin-set>
+      <pin digest="SHA-256">aaaaaaaaIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
+    </pin-set>
+    <trust-anchors>
+      <certificates src="system" overridePins="true" />
+    </trust-anchors>
+  </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/pins1.xml b/tests/NetworkSecurityConfigTest/res/xml/pins1.xml
new file mode 100644
index 0000000..1773d280
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/pins1.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <domain-config>
+    <domain>android.com</domain>
+    <pin-set>
+      <pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
+    </pin-set>
+  </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/resource_anchors_der.xml b/tests/NetworkSecurityConfigTest/res/xml/resource_anchors_der.xml
new file mode 100644
index 0000000..dfd6fd9
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/resource_anchors_der.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <base-config>
+    <trust-anchors>
+    </trust-anchors>
+  </base-config>
+  <domain-config>
+    <domain>android.com</domain>
+    <trust-anchors>
+      <certificates src="@raw/ca_certs_der" />
+    </trust-anchors>
+  </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/resource_anchors_pem.xml b/tests/NetworkSecurityConfigTest/res/xml/resource_anchors_pem.xml
new file mode 100644
index 0000000..894f29b
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/resource_anchors_pem.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <base-config>
+    <trust-anchors>
+    </trust-anchors>
+  </base-config>
+  <domain-config>
+    <domain>android.com</domain>
+    <trust-anchors>
+      <certificates src="@raw/ca_certs_pem" />
+    </trust-anchors>
+  </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/subdomains.xml b/tests/NetworkSecurityConfigTest/res/xml/subdomains.xml
new file mode 100644
index 0000000..482b26c
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/subdomains.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <base-config>
+    <trust-anchors>
+    </trust-anchors>
+  </base-config>
+  <domain-config>
+    <domain includeSubdomains="true">android.com</domain>
+    <trust-anchors>
+      <certificates src="system" />
+      <certificates src="user" />
+    </trust-anchors>
+  </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/src/android/security/net/config/NetworkSecurityConfigTests.java b/tests/NetworkSecurityConfigTest/src/android/security/net/config/NetworkSecurityConfigTests.java
index 9a1fe15..9f48d56 100644
--- a/tests/NetworkSecurityConfigTest/src/android/security/net/config/NetworkSecurityConfigTests.java
+++ b/tests/NetworkSecurityConfigTest/src/android/security/net/config/NetworkSecurityConfigTests.java
@@ -50,49 +50,6 @@
         return data;
     }
 
-    private void assertConnectionFails(SSLContext context, String host, int port)
-            throws Exception {
-        try {
-            Socket s = context.getSocketFactory().createSocket(host, port);
-            s.getInputStream();
-            fail("Expected connection to " + host + ":" + port + " to fail.");
-        } catch (SSLHandshakeException expected) {
-        }
-    }
-
-    private void assertConnectionSucceeds(SSLContext context, String host, int port)
-            throws Exception {
-        Socket s = context.getSocketFactory().createSocket(host, port);
-        s.getInputStream();
-    }
-
-    private void assertUrlConnectionFails(SSLContext context, String host, int port)
-            throws Exception {
-        URL url = new URL("https://" + host + ":" + port);
-        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
-        connection.setSSLSocketFactory(context.getSocketFactory());
-        try {
-            connection.getInputStream();
-            fail("Connection to " + host + ":" + port + " expected to fail");
-        } catch (SSLHandshakeException expected) {
-            // ignored.
-        }
-    }
-
-    private void assertUrlConnectionSucceeds(SSLContext context, String host, int port)
-            throws Exception {
-        URL url = new URL("https://" + host + ":" + port);
-        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
-        connection.setSSLSocketFactory(context.getSocketFactory());
-        connection.getInputStream();
-    }
-
-    private SSLContext getSSLContext(ConfigSource source) throws Exception {
-        ApplicationConfig config = new ApplicationConfig(source);
-        SSLContext context = SSLContext.getInstance("TLS");
-        context.init(null, new TrustManager[] {config.getTrustManager()}, null);
-        return context;
-    }
 
 
     /**
@@ -100,16 +57,14 @@
      * SSLHandshakeException when used for a connection.
      */
     private NetworkSecurityConfig getEmptyConfig() {
-        return new NetworkSecurityConfig(true, false,
-                new PinSet(new ArraySet<Pin>(), -1),
-                new ArrayList<CertificatesEntryRef>());
+        return new NetworkSecurityConfig.Builder().build();
     }
 
     private NetworkSecurityConfig getSystemStoreConfig() {
-        ArrayList<CertificatesEntryRef> defaultSource = new ArrayList<CertificatesEntryRef>();
-        defaultSource.add(new CertificatesEntryRef(new SystemCertificateSource(), false));
-        return new NetworkSecurityConfig(true, false, new PinSet(new ArraySet<Pin>(),
-                    -1), defaultSource);
+        return new NetworkSecurityConfig.Builder()
+                .addCertificatesEntryRef(
+                        new CertificatesEntryRef(SystemCertificateSource.getInstance(), false))
+                .build();
     }
 
     public void testEmptyConfig() throws Exception {
@@ -117,8 +72,8 @@
                 = new ArraySet<Pair<Domain, NetworkSecurityConfig>>();
         ConfigSource testSource =
                 new TestConfigSource(domainMap, getEmptyConfig());
-        SSLContext context = getSSLContext(testSource);
-        assertConnectionFails(context, "android.com", 443);
+        SSLContext context = TestUtils.getSSLContext(testSource);
+        TestUtils.assertConnectionFails(context, "android.com", 443);
     }
 
     public void testEmptyPerNetworkSecurityConfig() throws Exception {
@@ -126,68 +81,64 @@
                 = new ArraySet<Pair<Domain, NetworkSecurityConfig>>();
         domainMap.add(new Pair<Domain, NetworkSecurityConfig>(
                 new Domain("android.com", true), getEmptyConfig()));
-        ArrayList<CertificatesEntryRef> defaultSource = new ArrayList<CertificatesEntryRef>();
-        defaultSource.add(new CertificatesEntryRef(new SystemCertificateSource(), false));
-        NetworkSecurityConfig defaultConfig = new NetworkSecurityConfig(true, false,
-                new PinSet(new ArraySet<Pin>(), -1),
-                defaultSource);
-        SSLContext context = getSSLContext(new TestConfigSource(domainMap, defaultConfig));
-        assertConnectionFails(context, "android.com", 443);
-        assertConnectionSucceeds(context, "google.com", 443);
+        NetworkSecurityConfig defaultConfig = getSystemStoreConfig();
+        SSLContext context = TestUtils.getSSLContext(new TestConfigSource(domainMap, defaultConfig));
+        TestUtils.assertConnectionFails(context, "android.com", 443);
+        TestUtils.assertConnectionSucceeds(context, "google.com", 443);
     }
 
     public void testBadPin() throws Exception {
-        ArrayList<CertificatesEntryRef> systemSource = new ArrayList<CertificatesEntryRef>();
-        systemSource.add(new CertificatesEntryRef(new SystemCertificateSource(), false));
         ArraySet<Pin> pins = new ArraySet<Pin>();
         pins.add(new Pin("SHA-256", new byte[0]));
-        NetworkSecurityConfig domain = new NetworkSecurityConfig(true, false,
-                new PinSet(pins, Long.MAX_VALUE),
-                systemSource);
+        NetworkSecurityConfig domain = new NetworkSecurityConfig.Builder()
+                .setPinSet(new PinSet(pins, Long.MAX_VALUE))
+                .addCertificatesEntryRef(
+                        new CertificatesEntryRef(SystemCertificateSource.getInstance(), false))
+                .build();
         ArraySet<Pair<Domain, NetworkSecurityConfig>> domainMap
                 = new ArraySet<Pair<Domain, NetworkSecurityConfig>>();
         domainMap.add(new Pair<Domain, NetworkSecurityConfig>(
                 new Domain("android.com", true), domain));
         SSLContext context
-                = getSSLContext(new TestConfigSource(domainMap, getSystemStoreConfig()));
-        assertConnectionFails(context, "android.com", 443);
-        assertConnectionSucceeds(context, "google.com", 443);
+                = TestUtils.getSSLContext(new TestConfigSource(domainMap, getSystemStoreConfig()));
+        TestUtils.assertConnectionFails(context, "android.com", 443);
+        TestUtils.assertConnectionSucceeds(context, "google.com", 443);
     }
 
     public void testGoodPin() throws Exception {
-        ArrayList<CertificatesEntryRef> systemSource = new ArrayList<CertificatesEntryRef>();
-        systemSource.add(new CertificatesEntryRef(new SystemCertificateSource(), false));
         ArraySet<Pin> pins = new ArraySet<Pin>();
         pins.add(new Pin("SHA-256", G2_SPKI_SHA256));
-        NetworkSecurityConfig domain = new NetworkSecurityConfig(true, false,
-                new PinSet(pins, Long.MAX_VALUE),
-                systemSource);
+        NetworkSecurityConfig domain = new NetworkSecurityConfig.Builder()
+                .setPinSet(new PinSet(pins, Long.MAX_VALUE))
+                .addCertificatesEntryRef(
+                        new CertificatesEntryRef(SystemCertificateSource.getInstance(), false))
+                .build();
         ArraySet<Pair<Domain, NetworkSecurityConfig>> domainMap
                 = new ArraySet<Pair<Domain, NetworkSecurityConfig>>();
         domainMap.add(new Pair<Domain, NetworkSecurityConfig>(
                 new Domain("android.com", true), domain));
         SSLContext context
-                = getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
-        assertConnectionSucceeds(context, "android.com", 443);
-        assertConnectionSucceeds(context, "developer.android.com", 443);
+                = TestUtils.getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
+        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
+        TestUtils.assertConnectionSucceeds(context, "developer.android.com", 443);
     }
 
     public void testOverridePins() throws Exception {
         // Use a bad pin + granting the system CA store the ability to override pins.
-        ArrayList<CertificatesEntryRef> systemSource = new ArrayList<CertificatesEntryRef>();
-        systemSource.add(new CertificatesEntryRef(new SystemCertificateSource(), true));
         ArraySet<Pin> pins = new ArraySet<Pin>();
         pins.add(new Pin("SHA-256", new byte[0]));
-        NetworkSecurityConfig domain = new NetworkSecurityConfig(true, false,
-                new PinSet(pins, Long.MAX_VALUE),
-                systemSource);
+        NetworkSecurityConfig domain = new NetworkSecurityConfig.Builder()
+                .setPinSet(new PinSet(pins, Long.MAX_VALUE))
+                .addCertificatesEntryRef(
+                        new CertificatesEntryRef(SystemCertificateSource.getInstance(), true))
+                .build();
         ArraySet<Pair<Domain, NetworkSecurityConfig>> domainMap
                 = new ArraySet<Pair<Domain, NetworkSecurityConfig>>();
         domainMap.add(new Pair<Domain, NetworkSecurityConfig>(
                 new Domain("android.com", true), domain));
         SSLContext context
-                = getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
-        assertConnectionSucceeds(context, "android.com", 443);
+                = TestUtils.getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
+        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
     }
 
     public void testMostSpecificNetworkSecurityConfig() throws Exception {
@@ -198,9 +149,9 @@
         domainMap.add(new Pair<Domain, NetworkSecurityConfig>(
                 new Domain("developer.android.com", false), getSystemStoreConfig()));
         SSLContext context
-                = getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
-        assertConnectionFails(context, "android.com", 443);
-        assertConnectionSucceeds(context, "developer.android.com", 443);
+                = TestUtils.getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
+        TestUtils.assertConnectionFails(context, "android.com", 443);
+        TestUtils.assertConnectionSucceeds(context, "developer.android.com", 443);
     }
 
     public void testSubdomainIncluded() throws Exception {
@@ -210,32 +161,51 @@
         domainMap.add(new Pair<Domain, NetworkSecurityConfig>(
                 new Domain("android.com", true), getSystemStoreConfig()));
         SSLContext context
-                = getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
-        assertConnectionSucceeds(context, "developer.android.com", 443);
+                = TestUtils.getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
+        TestUtils.assertConnectionSucceeds(context, "developer.android.com", 443);
         // Now try without including subdomains.
         domainMap = new ArraySet<Pair<Domain, NetworkSecurityConfig>>();
         domainMap.add(new Pair<Domain, NetworkSecurityConfig>(
                 new Domain("android.com", false), getSystemStoreConfig()));
-        context = getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
-        assertConnectionFails(context, "developer.android.com", 443);
+        context = TestUtils.getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
+        TestUtils.assertConnectionFails(context, "developer.android.com", 443);
+    }
+
+    public void testConfigBuilderUsesParents() throws Exception {
+        // Check that a builder with a parent uses the parent's values when non is set.
+        NetworkSecurityConfig config = new NetworkSecurityConfig.Builder()
+                .setParent(NetworkSecurityConfig.getDefaultBuilder())
+                .build();
+        assert(!config.getTrustAnchors().isEmpty());
+    }
+
+    public void testConfigBuilderParentLoop() throws Exception {
+        NetworkSecurityConfig.Builder config1 = new NetworkSecurityConfig.Builder();
+        NetworkSecurityConfig.Builder config2 = new NetworkSecurityConfig.Builder();
+        config1.setParent(config2);
+        try {
+            config2.setParent(config1);
+            fail("Loop in NetworkSecurityConfig parents");
+        } catch (IllegalArgumentException expected) {
+        }
     }
 
     public void testWithUrlConnection() throws Exception {
-        ArrayList<CertificatesEntryRef> systemSource = new ArrayList<CertificatesEntryRef>();
-        systemSource.add(new CertificatesEntryRef(new SystemCertificateSource(), false));
         ArraySet<Pin> pins = new ArraySet<Pin>();
         pins.add(new Pin("SHA-256", G2_SPKI_SHA256));
-        NetworkSecurityConfig domain = new NetworkSecurityConfig(true, false,
-                new PinSet(pins, Long.MAX_VALUE),
-                systemSource);
+        NetworkSecurityConfig domain = new NetworkSecurityConfig.Builder()
+                .setPinSet(new PinSet(pins, Long.MAX_VALUE))
+                .addCertificatesEntryRef(
+                        new CertificatesEntryRef(SystemCertificateSource.getInstance(), false))
+                .build();
         ArraySet<Pair<Domain, NetworkSecurityConfig>> domainMap
                 = new ArraySet<Pair<Domain, NetworkSecurityConfig>>();
         domainMap.add(new Pair<Domain, NetworkSecurityConfig>(
                 new Domain("android.com", true), domain));
         SSLContext context
-                = getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
-        assertUrlConnectionSucceeds(context, "android.com", 443);
-        assertUrlConnectionSucceeds(context, "developer.android.com", 443);
-        assertUrlConnectionFails(context, "google.com", 443);
+                = TestUtils.getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
+        TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443);
+        TestUtils.assertUrlConnectionSucceeds(context, "developer.android.com", 443);
+        TestUtils.assertUrlConnectionFails(context, "google.com", 443);
     }
 }
diff --git a/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestUtils.java b/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestUtils.java
new file mode 100644
index 0000000..43c0e57
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestUtils.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2015 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 android.security.net.config;
+
+import java.net.Socket;
+import java.net.URL;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.TrustManager;
+
+import junit.framework.Assert;
+
+public final class TestUtils extends Assert {
+
+    private TestUtils() {
+    }
+
+    public static void assertConnectionFails(SSLContext context, String host, int port)
+            throws Exception {
+        try {
+            Socket s = context.getSocketFactory().createSocket(host, port);
+            s.getInputStream();
+            fail("Expected connection to " + host + ":" + port + " to fail.");
+        } catch (SSLHandshakeException expected) {
+        }
+    }
+
+    public static void assertConnectionSucceeds(SSLContext context, String host, int port)
+            throws Exception {
+        Socket s = context.getSocketFactory().createSocket(host, port);
+        s.getInputStream();
+    }
+
+    public static void assertUrlConnectionFails(SSLContext context, String host, int port)
+            throws Exception {
+        URL url = new URL("https://" + host + ":" + port);
+        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
+        connection.setSSLSocketFactory(context.getSocketFactory());
+        try {
+            connection.getInputStream();
+            fail("Connection to " + host + ":" + port + " expected to fail");
+        } catch (SSLHandshakeException expected) {
+            // ignored.
+        }
+    }
+
+    public static void assertUrlConnectionSucceeds(SSLContext context, String host, int port)
+            throws Exception {
+        URL url = new URL("https://" + host + ":" + port);
+        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
+        connection.setSSLSocketFactory(context.getSocketFactory());
+        connection.getInputStream();
+    }
+
+    public static SSLContext getSSLContext(ConfigSource source) throws Exception {
+        ApplicationConfig config = new ApplicationConfig(source);
+        SSLContext context = SSLContext.getInstance("TLS");
+        context.init(null, new TrustManager[] {config.getTrustManager()}, null);
+        return context;
+    }
+}
diff --git a/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java b/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java
new file mode 100644
index 0000000..f52a279
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java
@@ -0,0 +1,313 @@
+/*
+ * Copyright (C) 2015 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 android.security.net.config;
+
+import android.content.Context;
+import android.test.AndroidTestCase;
+import android.test.MoreAsserts;
+import android.util.ArraySet;
+import android.util.Pair;
+import java.io.IOException;
+import java.net.Socket;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.TrustManager;
+
+public class XmlConfigTests extends AndroidTestCase {
+
+    public void testEmptyConfigFile() throws Exception {
+        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.empty_config);
+        ApplicationConfig appConfig = new ApplicationConfig(source);
+        assertFalse(appConfig.hasPerDomainConfigs());
+        NetworkSecurityConfig config = appConfig.getConfigForHostname("");
+        assertNotNull(config);
+        // Check defaults.
+        assertTrue(config.isCleartextTrafficPermitted());
+        assertFalse(config.isHstsEnforced());
+        assertFalse(config.getTrustAnchors().isEmpty());
+        PinSet pinSet = config.getPins();
+        assertTrue(pinSet.pins.isEmpty());
+        // Try some connections.
+        SSLContext context = TestUtils.getSSLContext(source);
+        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
+        TestUtils.assertConnectionSucceeds(context, "developer.android.com", 443);
+        TestUtils.assertUrlConnectionSucceeds(context, "google.com", 443);
+    }
+
+    public void testEmptyAnchors() throws Exception {
+        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.empty_trust);
+        ApplicationConfig appConfig = new ApplicationConfig(source);
+        assertFalse(appConfig.hasPerDomainConfigs());
+        NetworkSecurityConfig config = appConfig.getConfigForHostname("");
+        assertNotNull(config);
+        // Check defaults.
+        assertTrue(config.isCleartextTrafficPermitted());
+        assertFalse(config.isHstsEnforced());
+        assertTrue(config.getTrustAnchors().isEmpty());
+        PinSet pinSet = config.getPins();
+        assertTrue(pinSet.pins.isEmpty());
+        SSLContext context = TestUtils.getSSLContext(source);
+        TestUtils.assertConnectionFails(context, "android.com", 443);
+        TestUtils.assertConnectionFails(context, "developer.android.com", 443);
+        TestUtils.assertUrlConnectionFails(context, "google.com", 443);
+    }
+
+    public void testBasicDomainConfig() throws Exception {
+        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.domain1);
+        ApplicationConfig appConfig = new ApplicationConfig(source);
+        assertTrue(appConfig.hasPerDomainConfigs());
+        NetworkSecurityConfig config = appConfig.getConfigForHostname("");
+        assertNotNull(config);
+        // Check defaults.
+        assertTrue(config.isCleartextTrafficPermitted());
+        assertFalse(config.isHstsEnforced());
+        assertTrue(config.getTrustAnchors().isEmpty());
+        PinSet pinSet = config.getPins();
+        assertTrue(pinSet.pins.isEmpty());
+        // Check android.com.
+        config = appConfig.getConfigForHostname("android.com");
+        assertTrue(config.isCleartextTrafficPermitted());
+        assertFalse(config.isHstsEnforced());
+        assertFalse(config.getTrustAnchors().isEmpty());
+        pinSet = config.getPins();
+        assertTrue(pinSet.pins.isEmpty());
+        // Try connections.
+        SSLContext context = TestUtils.getSSLContext(source);
+        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
+        TestUtils.assertConnectionFails(context, "developer.android.com", 443);
+        TestUtils.assertUrlConnectionFails(context, "google.com", 443);
+        TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443);
+    }
+
+    public void testBasicPinning() throws Exception {
+        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.pins1);
+        ApplicationConfig appConfig = new ApplicationConfig(source);
+        assertTrue(appConfig.hasPerDomainConfigs());
+        // Check android.com.
+        NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com");
+        PinSet pinSet = config.getPins();
+        assertFalse(pinSet.pins.isEmpty());
+        // Try connections.
+        SSLContext context = TestUtils.getSSLContext(source);
+        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
+        TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443);
+        TestUtils.assertConnectionSucceeds(context, "google.com", 443);
+    }
+
+    public void testExpiredPin() throws Exception {
+        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.expired_pin);
+        ApplicationConfig appConfig = new ApplicationConfig(source);
+        assertTrue(appConfig.hasPerDomainConfigs());
+        // Check android.com.
+        NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com");
+        PinSet pinSet = config.getPins();
+        assertFalse(pinSet.pins.isEmpty());
+        // Try connections.
+        SSLContext context = TestUtils.getSSLContext(source);
+        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
+        TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443);
+    }
+
+    public void testOverridesPins() throws Exception {
+        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.override_pins);
+        ApplicationConfig appConfig = new ApplicationConfig(source);
+        assertTrue(appConfig.hasPerDomainConfigs());
+        // Check android.com.
+        NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com");
+        PinSet pinSet = config.getPins();
+        assertFalse(pinSet.pins.isEmpty());
+        // Try connections.
+        SSLContext context = TestUtils.getSSLContext(source);
+        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
+        TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443);
+    }
+
+    public void testBadPin() throws Exception {
+        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.bad_pin);
+        ApplicationConfig appConfig = new ApplicationConfig(source);
+        assertTrue(appConfig.hasPerDomainConfigs());
+        // Check android.com.
+        NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com");
+        PinSet pinSet = config.getPins();
+        assertFalse(pinSet.pins.isEmpty());
+        // Try connections.
+        SSLContext context = TestUtils.getSSLContext(source);
+        TestUtils.assertConnectionFails(context, "android.com", 443);
+        TestUtils.assertUrlConnectionFails(context, "android.com", 443);
+        TestUtils.assertConnectionSucceeds(context, "google.com", 443);
+    }
+
+    public void testMultipleDomains() throws Exception {
+        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.multiple_domains);
+        ApplicationConfig appConfig = new ApplicationConfig(source);
+        assertTrue(appConfig.hasPerDomainConfigs());
+        NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com");
+        assertTrue(config.isCleartextTrafficPermitted());
+        assertFalse(config.isHstsEnforced());
+        assertFalse(config.getTrustAnchors().isEmpty());
+        PinSet pinSet = config.getPins();
+        assertTrue(pinSet.pins.isEmpty());
+        // Both android.com and google.com should use the same config
+        NetworkSecurityConfig other = appConfig.getConfigForHostname("google.com");
+        assertEquals(config, other);
+        // Try connections.
+        SSLContext context = TestUtils.getSSLContext(source);
+        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
+        TestUtils.assertConnectionSucceeds(context, "google.com", 443);
+        TestUtils.assertConnectionFails(context, "developer.android.com", 443);
+        TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443);
+    }
+
+    public void testMultipleDomainConfigs() throws Exception {
+        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.multiple_configs);
+        ApplicationConfig appConfig = new ApplicationConfig(source);
+        assertTrue(appConfig.hasPerDomainConfigs());
+        // Should be two different config objects
+        NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com");
+        NetworkSecurityConfig other = appConfig.getConfigForHostname("google.com");
+        MoreAsserts.assertNotEqual(config, other);
+        // Try connections.
+        SSLContext context = TestUtils.getSSLContext(source);
+        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
+        TestUtils.assertConnectionSucceeds(context, "google.com", 443);
+        TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443);
+    }
+
+    public void testIncludeSubdomains() throws Exception {
+        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.subdomains);
+        ApplicationConfig appConfig = new ApplicationConfig(source);
+        assertTrue(appConfig.hasPerDomainConfigs());
+        // Try connections.
+        SSLContext context = TestUtils.getSSLContext(source);
+        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
+        TestUtils.assertConnectionSucceeds(context, "developer.android.com", 443);
+        TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443);
+        TestUtils.assertUrlConnectionSucceeds(context, "developer.android.com", 443);
+        TestUtils.assertConnectionFails(context, "google.com", 443);
+    }
+
+    public void testAttributes() throws Exception {
+        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.attributes);
+        ApplicationConfig appConfig = new ApplicationConfig(source);
+        assertFalse(appConfig.hasPerDomainConfigs());
+        NetworkSecurityConfig config = appConfig.getConfigForHostname("");
+        assertTrue(config.isHstsEnforced());
+        assertFalse(config.isCleartextTrafficPermitted());
+    }
+
+    public void testResourcePemCertificateSource() throws Exception {
+        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.resource_anchors_pem);
+        ApplicationConfig appConfig = new ApplicationConfig(source);
+        // Check android.com.
+        NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com");
+        assertTrue(config.isCleartextTrafficPermitted());
+        assertFalse(config.isHstsEnforced());
+        assertEquals(2, config.getTrustAnchors().size());
+        // Try connections.
+        SSLContext context = TestUtils.getSSLContext(source);
+        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
+        TestUtils.assertConnectionFails(context, "developer.android.com", 443);
+        TestUtils.assertUrlConnectionFails(context, "google.com", 443);
+        TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443);
+    }
+
+    public void testResourceDerCertificateSource() throws Exception {
+        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.resource_anchors_der);
+        ApplicationConfig appConfig = new ApplicationConfig(source);
+        // Check android.com.
+        NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com");
+        assertTrue(config.isCleartextTrafficPermitted());
+        assertFalse(config.isHstsEnforced());
+        assertEquals(2, config.getTrustAnchors().size());
+        // Try connections.
+        SSLContext context = TestUtils.getSSLContext(source);
+        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
+        TestUtils.assertConnectionFails(context, "developer.android.com", 443);
+        TestUtils.assertUrlConnectionFails(context, "google.com", 443);
+        TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443);
+    }
+
+    public void testNestedDomainConfigs() throws Exception {
+        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.nested_domains);
+        ApplicationConfig appConfig = new ApplicationConfig(source);
+        assertTrue(appConfig.hasPerDomainConfigs());
+        NetworkSecurityConfig parent = appConfig.getConfigForHostname("android.com");
+        NetworkSecurityConfig child = appConfig.getConfigForHostname("developer.android.com");
+        MoreAsserts.assertNotEqual(parent, child);
+        MoreAsserts.assertEmpty(parent.getPins().pins);
+        MoreAsserts.assertNotEmpty(child.getPins().pins);
+        // Check that the child inherited the cleartext value and anchors.
+        assertFalse(child.isCleartextTrafficPermitted());
+        MoreAsserts.assertNotEmpty(child.getTrustAnchors());
+        // Test connections.
+        SSLContext context = TestUtils.getSSLContext(source);
+        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
+        TestUtils.assertConnectionSucceeds(context, "developer.android.com", 443);
+    }
+
+    public void testNestedDomainConfigsOverride() throws Exception {
+        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.nested_domains_override);
+        ApplicationConfig appConfig = new ApplicationConfig(source);
+        assertTrue(appConfig.hasPerDomainConfigs());
+        NetworkSecurityConfig parent = appConfig.getConfigForHostname("android.com");
+        NetworkSecurityConfig child = appConfig.getConfigForHostname("developer.android.com");
+        MoreAsserts.assertNotEqual(parent, child);
+        assertTrue(parent.isCleartextTrafficPermitted());
+        assertFalse(child.isCleartextTrafficPermitted());
+    }
+
+    private void testBadConfig(int configId) throws Exception {
+        try {
+            XmlConfigSource source = new XmlConfigSource(getContext(), configId);
+            ApplicationConfig appConfig = new ApplicationConfig(source);
+            appConfig.getConfigForHostname("android.com");
+            fail("Bad config " + getContext().getResources().getResourceName(configId)
+                    + " did not fail to parse");
+        } catch (RuntimeException e) {
+            MoreAsserts.assertAssignableFrom(XmlConfigSource.ParserException.class,
+                    e.getCause());
+        }
+    }
+
+    public void testBadConfig0() throws Exception {
+        testBadConfig(R.xml.bad_config0);
+    }
+
+    public void testBadConfig1() throws Exception {
+        testBadConfig(R.xml.bad_config1);
+    }
+
+    public void testBadConfig2() throws Exception {
+        testBadConfig(R.xml.bad_config2);
+    }
+
+    public void testBadConfig3() throws Exception {
+        testBadConfig(R.xml.bad_config3);
+    }
+
+    public void testBadConfig4() throws Exception {
+        testBadConfig(R.xml.bad_config4);
+    }
+
+    public void testBadConfig5() throws Exception {
+        testBadConfig(R.xml.bad_config4);
+    }
+}
diff --git a/tools/aapt2/Android.mk b/tools/aapt2/Android.mk
index ceed21e..ec29c38 100644
--- a/tools/aapt2/Android.mk
+++ b/tools/aapt2/Android.mk
@@ -80,6 +80,7 @@
 	util/StringPiece_test.cpp \
 	util/Util_test.cpp \
 	ConfigDescription_test.cpp \
+	java/AnnotationProcessor_test.cpp \
 	java/JavaClassGenerator_test.cpp \
 	java/ManifestClassGenerator_test.cpp \
 	Locale_test.cpp \
diff --git a/tools/aapt2/ResourceParser.cpp b/tools/aapt2/ResourceParser.cpp
index 39a4116..f2a1878 100644
--- a/tools/aapt2/ResourceParser.cpp
+++ b/tools/aapt2/ResourceParser.cpp
@@ -49,6 +49,13 @@
     return {};
 }
 
+/**
+ * Returns true if the element is <skip> or <eat-comment> and can be safely ignored.
+ */
+static bool shouldIgnoreElement(const StringPiece16& ns, const StringPiece16& name) {
+    return ns.empty() && (name == u"skip" || name == u"eat-comment");
+}
+
 ResourceParser::ResourceParser(IDiagnostics* diag, ResourceTable* table, const Source& source,
                                const ConfigDescription& config,
                                const ResourceParserOptions& options) :
@@ -205,16 +212,14 @@
         }
     }
 
-    if (!res->value) {
-        return true;
-    }
+    if (res->value) {
+        // Attach the comment, source and config to the value.
+        res->value->setComment(std::move(res->comment));
+        res->value->setSource(std::move(res->source));
 
-    // Attach the comment, source and config to the value.
-    res->value->setComment(std::move(res->comment));
-    res->value->setSource(std::move(res->source));
-
-    if (!table->addResource(res->name, res->id, config, std::move(res->value), diag)) {
-        return false;
+        if (!table->addResource(res->name, res->id, config, std::move(res->value), diag)) {
+            return false;
+        }
     }
 
     bool error = false;
@@ -259,17 +264,6 @@
             continue;
         }
 
-        Maybe<StringPiece16> maybeName = findNonEmptyAttribute(parser, u"name");
-        if (!maybeName) {
-            mDiag->error(DiagMessage(mSource.withLine(parser->getLineNumber()))
-                         << "<" << elementName << "> tag must have a 'name' attribute");
-            error = true;
-            continue;
-        }
-
-        // Check if we should skip this product.
-        const bool stripResource = shouldStripResource(parser, mOptions.product);
-
         if (elementName == u"item") {
             // Items simply have their type encoded in the type attribute.
             if (Maybe<StringPiece16> maybeType = findNonEmptyAttribute(parser, u"type")) {
@@ -283,10 +277,22 @@
         }
 
         ParsedResource parsedResource;
-        parsedResource.name.entry = maybeName.value().toString();
         parsedResource.source = mSource.withLine(parser->getLineNumber());
         parsedResource.comment = std::move(comment);
 
+        if (Maybe<StringPiece16> maybeName = findNonEmptyAttribute(parser, u"name")) {
+            parsedResource.name.entry = maybeName.value().toString();
+
+        } else if (elementName != u"public-group") {
+            mDiag->error(DiagMessage(mSource.withLine(parser->getLineNumber()))
+                         << "<" << elementName << "> tag must have a 'name' attribute");
+            error = true;
+            continue;
+        }
+
+        // Check if we should skip this product.
+        const bool stripResource = shouldStripResource(parser, mOptions.product);
+
         bool result = true;
         if (elementName == u"id") {
             parsedResource.name.type = ResourceType::kId;
@@ -337,9 +343,24 @@
             result = parsePublic(parser, &parsedResource);
         } else if (elementName == u"java-symbol" || elementName == u"symbol") {
             result = parseSymbol(parser, &parsedResource);
+        } else if (elementName == u"public-group") {
+            result = parsePublicGroup(parser, &parsedResource);
         } else {
-            mDiag->warn(DiagMessage(mSource.withLine(parser->getLineNumber()))
-                        << "unknown resource type '" << elementName << "'");
+            // Try parsing the elementName (or type) as a resource. These shall only be
+            // resources like 'layout' or 'xml' and they can only be references.
+            if (const ResourceType* type = parseResourceType(elementName)) {
+                parsedResource.name.type = *type;
+                parsedResource.value = parseXml(parser, android::ResTable_map::TYPE_REFERENCE,
+                                                false);
+                if (!parsedResource.value) {
+                    mDiag->error(DiagMessage(parsedResource.source) << "invalid value for type '"
+                                 << *type << "'. Expected a reference");
+                    result = false;
+                }
+            } else {
+                mDiag->warn(DiagMessage(mSource.withLine(parser->getLineNumber()))
+                            << "unknown resource type '" << elementName << "'");
+            }
         }
 
         if (result) {
@@ -559,6 +580,92 @@
     return true;
 }
 
+bool ResourceParser::parsePublicGroup(XmlPullParser* parser, ParsedResource* outResource) {
+    const Source source = mSource.withLine(parser->getLineNumber());
+
+    Maybe<StringPiece16> maybeType = findNonEmptyAttribute(parser, u"type");
+    if (!maybeType) {
+        mDiag->error(DiagMessage(source) << "<public-group> must have a 'type' attribute");
+        return false;
+    }
+
+    const ResourceType* parsedType = parseResourceType(maybeType.value());
+    if (!parsedType) {
+        mDiag->error(DiagMessage(source) << "invalid resource type '" << maybeType.value()
+                     << "' in <public-group>");
+        return false;
+    }
+
+    Maybe<StringPiece16> maybeId = findNonEmptyAttribute(parser, u"first-id");
+    if (!maybeId) {
+        mDiag->error(DiagMessage(source) << "<public-group> must have a 'first-id' attribute");
+        return false;
+    }
+
+    android::Res_value val;
+    bool result = android::ResTable::stringToInt(maybeId.value().data(),
+                                                 maybeId.value().size(), &val);
+    ResourceId nextId(val.data);
+    if (!result || !nextId.isValid()) {
+        mDiag->error(DiagMessage(source) << "invalid resource ID '" << maybeId.value()
+                     << "' in <public-group>");
+        return false;
+    }
+
+    std::u16string comment;
+    bool error = false;
+    const size_t depth = parser->getDepth();
+    while (XmlPullParser::nextChildNode(parser, depth)) {
+        if (parser->getEvent() == XmlPullParser::Event::kComment) {
+            comment = util::trimWhitespace(parser->getComment()).toString();
+            continue;
+        } else if (parser->getEvent() != XmlPullParser::Event::kStartElement) {
+            // Skip text.
+            continue;
+        }
+
+        const Source itemSource = mSource.withLine(parser->getLineNumber());
+        const std::u16string& elementNamespace = parser->getElementNamespace();
+        const std::u16string& elementName = parser->getElementName();
+        if (elementNamespace.empty() && elementName == u"public") {
+            Maybe<StringPiece16> maybeName = findNonEmptyAttribute(parser, u"name");
+            if (!maybeName) {
+                mDiag->error(DiagMessage(itemSource) << "<public> must have a 'name' attribute");
+                error = true;
+                continue;
+            }
+
+            if (findNonEmptyAttribute(parser, u"id")) {
+                mDiag->error(DiagMessage(itemSource) << "'id' is ignored within <public-group>");
+                error = true;
+                continue;
+            }
+
+            if (findNonEmptyAttribute(parser, u"type")) {
+                mDiag->error(DiagMessage(itemSource) << "'type' is ignored within <public-group>");
+                error = true;
+                continue;
+            }
+
+            ParsedResource childResource;
+            childResource.name.type = *parsedType;
+            childResource.name.entry = maybeName.value().toString();
+            childResource.id = nextId;
+            childResource.comment = std::move(comment);
+            childResource.source = itemSource;
+            childResource.symbolState = SymbolState::kPublic;
+            outResource->childResources.push_back(std::move(childResource));
+
+            nextId.id += 1;
+
+        } else if (!shouldIgnoreElement(elementNamespace, elementName)) {
+            mDiag->error(DiagMessage(itemSource) << ":" << elementName << ">");
+            error = true;
+        }
+    }
+    return !error;
+}
+
 bool ResourceParser::parseSymbol(XmlPullParser* parser, ParsedResource* outResource) {
     const Source source = mSource.withLine(parser->getLineNumber());
 
@@ -608,12 +715,7 @@
     return mask;
 }
 
-/**
- * Returns true if the element is <skip> or <eat-comment> and can be safely ignored.
- */
-static bool shouldIgnoreElement(const StringPiece16& ns, const StringPiece16& name) {
-    return ns.empty() && (name == u"skip" || name == u"eat-comment");
-}
+
 
 bool ResourceParser::parseAttr(XmlPullParser* parser, ParsedResource* outResource) {
     outResource->source = mSource.withLine(parser->getLineNumber());
@@ -973,7 +1075,7 @@
     const Source source = mSource.withLine(parser->getLineNumber());
     std::unique_ptr<Styleable> styleable = util::make_unique<Styleable>();
 
-    // Declare-styleable is always public, because it technically only exists in R.java.
+    // Declare-styleable is kPrivate by default, because it technically only exists in R.java.
     outResource->symbolState = SymbolState::kPublic;
 
     std::u16string comment;
diff --git a/tools/aapt2/ResourceParser.h b/tools/aapt2/ResourceParser.h
index 06b2581..18101ee 100644
--- a/tools/aapt2/ResourceParser.h
+++ b/tools/aapt2/ResourceParser.h
@@ -83,6 +83,7 @@
     bool parseColor(XmlPullParser* parser, ParsedResource* outResource);
     bool parsePrimitive(XmlPullParser* parser, ParsedResource* outResource);
     bool parsePublic(XmlPullParser* parser, ParsedResource* outResource);
+    bool parsePublicGroup(XmlPullParser* parser, ParsedResource* outResource);
     bool parseSymbol(XmlPullParser* parser, ParsedResource* outResource);
     bool parseAttr(XmlPullParser* parser, ParsedResource* outResource);
     bool parseAttrImpl(XmlPullParser* parser, ParsedResource* outResource, bool weak);
diff --git a/tools/aapt2/ResourceParser_test.cpp b/tools/aapt2/ResourceParser_test.cpp
index ab7b172..b59eb95 100644
--- a/tools/aapt2/ResourceParser_test.cpp
+++ b/tools/aapt2/ResourceParser_test.cpp
@@ -489,4 +489,44 @@
     ASSERT_FALSE(testParse(input, std::u16string(u"phone")));
 }
 
+TEST_F(ResourceParserTest, AutoIncrementIdsInPublicGroup) {
+    std::string input = R"EOF(
+    <public-group type="attr" first-id="0x01010040">
+      <public name="foo" />
+      <public name="bar" />
+    </public-group>)EOF";
+    ASSERT_TRUE(testParse(input));
+
+    Maybe<ResourceTable::SearchResult> result = mTable.findResource(
+            test::parseNameOrDie(u"@attr/foo"));
+    AAPT_ASSERT_TRUE(result);
+
+    AAPT_ASSERT_TRUE(result.value().package->id);
+    AAPT_ASSERT_TRUE(result.value().type->id);
+    AAPT_ASSERT_TRUE(result.value().entry->id);
+    ResourceId actualId(result.value().package->id.value(),
+                        result.value().type->id.value(),
+                        result.value().entry->id.value());
+    EXPECT_EQ(ResourceId(0x01010040), actualId);
+
+    result = mTable.findResource(test::parseNameOrDie(u"@attr/bar"));
+    AAPT_ASSERT_TRUE(result);
+
+    AAPT_ASSERT_TRUE(result.value().package->id);
+    AAPT_ASSERT_TRUE(result.value().type->id);
+    AAPT_ASSERT_TRUE(result.value().entry->id);
+    actualId = ResourceId(result.value().package->id.value(),
+                          result.value().type->id.value(),
+                          result.value().entry->id.value());
+    EXPECT_EQ(ResourceId(0x01010041), actualId);
+}
+
+TEST_F(ResourceParserTest, ExternalTypesShouldOnlyBeReferences) {
+    std::string input = R"EOF(<item type="layout" name="foo">@layout/bar</item>)EOF";
+    ASSERT_TRUE(testParse(input));
+
+    input = R"EOF(<item type="layout" name="bar">"this is a string"</item>)EOF";
+    ASSERT_FALSE(testParse(input));
+}
+
 } // namespace aapt
diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp
index fa4b109..deafe20 100644
--- a/tools/aapt2/ResourceTable.cpp
+++ b/tools/aapt2/ResourceTable.cpp
@@ -284,7 +284,7 @@
     }
 
     const auto endIter = entry->values.end();
-    auto iter = std::lower_bound(entry->values.begin(), endIter, config, cmp::lessThan);
+    auto iter = std::lower_bound(entry->values.begin(), endIter, config, cmp::lessThanConfig);
     if (iter == endIter || iter->config != config) {
         // This resource did not exist before, add it.
         entry->values.insert(iter, ResourceConfigValue{ config, std::move(value) });
diff --git a/tools/aapt2/ResourceValues.cpp b/tools/aapt2/ResourceValues.cpp
index f312d75..8acff0d 100644
--- a/tools/aapt2/ResourceValues.cpp
+++ b/tools/aapt2/ResourceValues.cpp
@@ -44,7 +44,10 @@
 }
 
 RawString* RawString::clone(StringPool* newPool) const {
-    return new RawString(newPool->makeRef(*value));
+    RawString* rs = new RawString(newPool->makeRef(*value));
+    rs->mComment = mComment;
+    rs->mSource = mSource;
+    return rs;
 }
 
 bool RawString::flatten(android::Res_value* outValue) const {
@@ -77,6 +80,8 @@
 
 Reference* Reference::clone(StringPool* /*newPool*/) const {
     Reference* ref = new Reference();
+    ref->mComment = mComment;
+    ref->mSource = mSource;
     ref->referenceType = referenceType;
     ref->name = name;
     ref->id = id;
@@ -111,7 +116,10 @@
 }
 
 Id* Id::clone(StringPool* /*newPool*/) const {
-    return new Id();
+    Id* id = new Id();
+    id->mComment = mComment;
+    id->mSource = mSource;
+    return id;
 }
 
 void Id::print(std::ostream* out) const {
@@ -133,7 +141,10 @@
 }
 
 String* String::clone(StringPool* newPool) const {
-    return new String(newPool->makeRef(*value));
+    String* str = new String(newPool->makeRef(*value));
+    str->mComment = mComment;
+    str->mSource = mSource;
+    return str;
 }
 
 void String::print(std::ostream* out) const {
@@ -154,7 +165,10 @@
 }
 
 StyledString* StyledString::clone(StringPool* newPool) const {
-    return new StyledString(newPool->makeRef(value));
+    StyledString* str = new StyledString(newPool->makeRef(value));
+    str->mComment = mComment;
+    str->mSource = mSource;
+    return str;
 }
 
 void StyledString::print(std::ostream* out) const {
@@ -175,7 +189,10 @@
 }
 
 FileReference* FileReference::clone(StringPool* newPool) const {
-    return new FileReference(newPool->makeRef(*path));
+    FileReference* fr = new FileReference(newPool->makeRef(*path));
+    fr->mComment = mComment;
+    fr->mSource = mSource;
+    return fr;
 }
 
 void FileReference::print(std::ostream* out) const {
@@ -197,7 +214,10 @@
 }
 
 BinaryPrimitive* BinaryPrimitive::clone(StringPool* /*newPool*/) const {
-    return new BinaryPrimitive(value);
+    BinaryPrimitive* bp = new BinaryPrimitive(value);
+    bp->mComment = mComment;
+    bp->mSource = mSource;
+    return bp;
 }
 
 void BinaryPrimitive::print(std::ostream* out) const {
@@ -236,6 +256,8 @@
 
 Attribute* Attribute::clone(StringPool* /*newPool*/) const {
     Attribute* attr = new Attribute(weak);
+    attr->mComment = mComment;
+    attr->mSource = mSource;
     attr->typeMask = typeMask;
     std::copy(symbols.begin(), symbols.end(), std::back_inserter(attr->symbols));
     return attr;
@@ -358,6 +380,8 @@
     Style* style = new Style();
     style->parent = parent;
     style->parentInferred = parentInferred;
+    style->mComment = mComment;
+    style->mSource = mSource;
     for (auto& entry : entries) {
         style->entries.push_back(Entry{
                 entry.key,
@@ -390,6 +414,8 @@
 
 Array* Array::clone(StringPool* newPool) const {
     Array* array = new Array();
+    array->mComment = mComment;
+    array->mSource = mSource;
     for (auto& item : items) {
         array->items.emplace_back(std::unique_ptr<Item>(item->clone(newPool)));
     }
@@ -404,6 +430,8 @@
 
 Plural* Plural::clone(StringPool* newPool) const {
     Plural* p = new Plural();
+    p->mComment = mComment;
+    p->mSource = mSource;
     const size_t count = values.size();
     for (size_t i = 0; i < count; i++) {
         if (values[i]) {
@@ -423,6 +451,8 @@
 
 Styleable* Styleable::clone(StringPool* /*newPool*/) const {
     Styleable* styleable = new Styleable();
+    styleable->mComment = mComment;
+    styleable->mSource = mSource;
     std::copy(entries.begin(), entries.end(), std::back_inserter(styleable->entries));
     return styleable;
 }
diff --git a/tools/aapt2/ResourceValues.h b/tools/aapt2/ResourceValues.h
index 2629153..7ae346a 100644
--- a/tools/aapt2/ResourceValues.h
+++ b/tools/aapt2/ResourceValues.h
@@ -91,7 +91,7 @@
      */
     virtual void print(std::ostream* out) const = 0;
 
-private:
+protected:
     Source mSource;
     std::u16string mComment;
 };
diff --git a/tools/aapt2/compile/Compile.cpp b/tools/aapt2/compile/Compile.cpp
index 5d90ab9..39088bc 100644
--- a/tools/aapt2/compile/Compile.cpp
+++ b/tools/aapt2/compile/Compile.cpp
@@ -148,10 +148,14 @@
         fin.close();
     }
 
-    ResourceTablePackage* pkg = table.createPackage(context->getCompilationPackage());
-    if (!pkg->id) {
-        // If no package ID was set while parsing (public identifiers), auto assign an ID.
-        pkg->id = context->getPackageId();
+    // Ensure we have the compilation package at least.
+    table.createPackage(context->getCompilationPackage());
+
+    for (auto& pkg : table.packages) {
+        if (!pkg->id) {
+            // If no package ID was set while parsing (public identifiers), auto assign an ID.
+            pkg->id = context->getPackageId();
+        }
     }
 
     // Assign IDs to prepare the table for flattening.
diff --git a/tools/aapt2/java/AnnotationProcessor.cpp b/tools/aapt2/java/AnnotationProcessor.cpp
index b36682d..9c25d4e 100644
--- a/tools/aapt2/java/AnnotationProcessor.cpp
+++ b/tools/aapt2/java/AnnotationProcessor.cpp
@@ -25,33 +25,20 @@
     static const std::string sDeprecated = "@deprecated";
     static const std::string sSystemApi = "@SystemApi";
 
-    if (comment.find(sDeprecated) != std::string::npos && !mDeprecated) {
-        mDeprecated = true;
-        if (!mAnnotations.empty()) {
-            mAnnotations += "\n";
-        }
-        mAnnotations += mPrefix;
-        mAnnotations += "@Deprecated";
+    if (comment.find(sDeprecated) != std::string::npos) {
+        mAnnotationBitMask |= kDeprecated;
     }
 
-    if (comment.find(sSystemApi) != std::string::npos && !mSystemApi) {
-        mSystemApi = true;
-        if (!mAnnotations.empty()) {
-            mAnnotations += "\n";
-        }
-        mAnnotations += mPrefix;
-        mAnnotations += "@android.annotations.SystemApi";
+    if (comment.find(sSystemApi) != std::string::npos) {
+        mAnnotationBitMask |= kSystemApi;
     }
 
-    if (mComment.empty()) {
-        mComment += mPrefix;
-        mComment += "/**";
+    if (!mHasComments) {
+        mHasComments = true;
+        mComment << "/**";
     }
 
-    mComment += "\n";
-    mComment += mPrefix;
-    mComment += " * ";
-    mComment += std::move(comment);
+    mComment << "\n" << " * " << std::move(comment);
 }
 
 void AnnotationProcessor::appendComment(const StringPiece16& comment) {
@@ -73,17 +60,22 @@
     }
 }
 
-std::string AnnotationProcessor::buildComment() {
-    if (!mComment.empty()) {
-        mComment += "\n";
-        mComment += mPrefix;
-        mComment += " */";
+void AnnotationProcessor::writeToStream(std::ostream* out, const StringPiece& prefix) {
+    if (mHasComments) {
+        std::string result = mComment.str();
+        for (StringPiece line : util::tokenize<char>(result, '\n')) {
+           *out << prefix << line << "\n";
+        }
+        *out << prefix << " */" << "\n";
     }
-    return std::move(mComment);
-}
 
-std::string AnnotationProcessor::buildAnnotations() {
-    return std::move(mAnnotations);
+    if (mAnnotationBitMask & kDeprecated) {
+        *out << prefix << "@Deprecated\n";
+    }
+
+    if (mAnnotationBitMask & kSystemApi) {
+        *out << prefix << "@android.annotation.SystemApi\n";
+    }
 }
 
 } // namespace aapt
diff --git a/tools/aapt2/java/AnnotationProcessor.h b/tools/aapt2/java/AnnotationProcessor.h
index 81a6f6e..e7f2be0 100644
--- a/tools/aapt2/java/AnnotationProcessor.h
+++ b/tools/aapt2/java/AnnotationProcessor.h
@@ -19,6 +19,7 @@
 
 #include "util/StringPiece.h"
 
+#include <sstream>
 #include <string>
 
 namespace aapt {
@@ -54,13 +55,6 @@
 class AnnotationProcessor {
 public:
     /**
-     * Creates an AnnotationProcessor with a given prefix for each line generated.
-     * This is usually a set of spaces for indentation.
-     */
-    AnnotationProcessor(const StringPiece& prefix) : mPrefix(prefix.toString()) {
-    }
-
-    /**
      * Adds more comments. Since resources can have various values with different configurations,
      * we need to collect all the comments.
      */
@@ -68,23 +62,20 @@
     void appendComment(const StringPiece& comment);
 
     /**
-     * Finishes the comment and moves it to the caller. Subsequent calls to buildComment() have
-     * undefined results.
+     * Writes the comments and annotations to the stream, with the given prefix before each line.
      */
-    std::string buildComment();
-
-    /**
-     * Finishes the annotation and moves it to the caller. Subsequent calls to buildAnnotations()
-     * have undefined results.
-     */
-    std::string buildAnnotations();
+    void writeToStream(std::ostream* out, const StringPiece& prefix);
 
 private:
-    std::string mPrefix;
-    std::string mComment;
-    std::string mAnnotations;
-    bool mDeprecated = false;
-    bool mSystemApi = false;
+    enum : uint32_t {
+        kDeprecated = 0x01,
+        kSystemApi = 0x02,
+    };
+
+    std::stringstream mComment;
+    std::stringstream mAnnotations;
+    bool mHasComments = false;
+    uint32_t mAnnotationBitMask = 0;
 
     void appendCommentLine(const std::string& line);
 };
diff --git a/tools/aapt2/java/AnnotationProcessor_test.cpp b/tools/aapt2/java/AnnotationProcessor_test.cpp
new file mode 100644
index 0000000..d5a2b38
--- /dev/null
+++ b/tools/aapt2/java/AnnotationProcessor_test.cpp
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+#include "ResourceParser.h"
+#include "ResourceTable.h"
+#include "ResourceValues.h"
+#include "XmlPullParser.h"
+
+#include "java/AnnotationProcessor.h"
+
+#include "test/Builders.h"
+#include "test/Context.h"
+
+#include <gtest/gtest.h>
+
+namespace aapt {
+
+struct AnnotationProcessorTest : public ::testing::Test {
+    std::unique_ptr<IAaptContext> mContext;
+    ResourceTable mTable;
+
+    void SetUp() override {
+        mContext = test::ContextBuilder().build();
+    }
+
+    ::testing::AssertionResult parse(const StringPiece& str) {
+        ResourceParserOptions options;
+        ResourceParser parser(mContext->getDiagnostics(), &mTable, Source{}, ConfigDescription{},
+                              options);
+        std::stringstream in;
+        in << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" << str;
+        XmlPullParser xmlParser(in);
+        if (parser.parse(&xmlParser)) {
+            return ::testing::AssertionSuccess();
+        }
+        return ::testing::AssertionFailure();
+    }
+};
+
+TEST_F(AnnotationProcessorTest, EmitsDeprecated) {
+    ASSERT_TRUE(parse(R"EOF(
+    <resources>
+      <declare-styleable name="foo">      
+        <!-- Some comment, and it should contain
+             a marker word, something that marks
+             this resource as nor needed.
+             {@deprecated That's the marker! } -->
+        <attr name="autoText" format="boolean" />
+      </declare-styleable>
+    </resources>)EOF"));
+
+    Attribute* attr = test::getValue<Attribute>(&mTable, u"@attr/autoText");
+    ASSERT_NE(nullptr, attr);
+
+    AnnotationProcessor processor;
+    processor.appendComment(attr->getComment());
+
+    std::stringstream result;
+    processor.writeToStream(&result, "");
+    std::string annotations = result.str();
+
+    EXPECT_NE(std::string::npos, annotations.find("@Deprecated"));
+}
+
+} // namespace aapt
+
+
diff --git a/tools/aapt2/java/ClassDefinitionWriter.h b/tools/aapt2/java/ClassDefinitionWriter.h
new file mode 100644
index 0000000..b8886f9
--- /dev/null
+++ b/tools/aapt2/java/ClassDefinitionWriter.h
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+#ifndef AAPT_JAVA_CLASSDEFINITION_H
+#define AAPT_JAVA_CLASSDEFINITION_H
+
+#include "java/AnnotationProcessor.h"
+#include "util/StringPiece.h"
+#include "util/Util.h"
+
+#include <sstream>
+#include <string>
+
+namespace aapt {
+
+struct ClassDefinitionWriterOptions {
+    bool useFinalQualifier = false;
+    bool forceCreationIfEmpty = false;
+};
+
+/**
+ * Writes a class for use in R.java or Manifest.java.
+ */
+class ClassDefinitionWriter {
+public:
+    ClassDefinitionWriter(const StringPiece& name, const ClassDefinitionWriterOptions& options) :
+            mName(name.toString()), mOptions(options), mStarted(false) {
+    }
+
+    ClassDefinitionWriter(const StringPiece16& name, const ClassDefinitionWriterOptions& options) :
+            mName(util::utf16ToUtf8(name)), mOptions(options), mStarted(false) {
+    }
+
+    void addIntMember(const StringPiece& name, AnnotationProcessor* processor,
+                      const uint32_t val) {
+        ensureClassDeclaration();
+        if (processor) {
+            processor->writeToStream(&mOut, kIndent);
+        }
+        mOut << kIndent << "public static " << (mOptions.useFinalQualifier ? "final " : "")
+             << "int " << name << "=" << val << ";\n";
+    }
+
+    void addStringMember(const StringPiece16& name, AnnotationProcessor* processor,
+                         const StringPiece16& val) {
+        ensureClassDeclaration();
+        if (processor) {
+            processor->writeToStream(&mOut, kIndent);
+        }
+        mOut << kIndent << "public static " << (mOptions.useFinalQualifier ? "final " : "")
+             << "String " << name << "=\"" << val << "\";\n";
+    }
+
+    void addResourceMember(const StringPiece16& name, AnnotationProcessor* processor,
+                           const ResourceId id) {
+        ensureClassDeclaration();
+        if (processor) {
+            processor->writeToStream(&mOut, kIndent);
+        }
+        mOut << kIndent << "public static " << (mOptions.useFinalQualifier ? "final " : "")
+             << "int " << name << "=" << id <<";\n";
+    }
+
+    template <typename Iterator, typename FieldAccessorFunc>
+    void addArrayMember(const StringPiece16& name, AnnotationProcessor* processor,
+                        const Iterator begin, const Iterator end, FieldAccessorFunc f) {
+        ensureClassDeclaration();
+        if (processor) {
+            processor->writeToStream(&mOut, kIndent);
+        }
+        mOut << kIndent << "public static final int[] " << name << "={";
+
+        for (Iterator current = begin; current != end; ++current) {
+            if (std::distance(begin, current) % kAttribsPerLine == 0) {
+                mOut << "\n" << kIndent << kIndent;
+            }
+
+            mOut << f(*current);
+            if (std::distance(current, end) > 1) {
+                mOut << ", ";
+            }
+        }
+        mOut << "\n" << kIndent <<"};\n";
+    }
+
+    void writeToStream(std::ostream* out, const StringPiece& prefix,
+                       AnnotationProcessor* processor=nullptr) {
+        if (mOptions.forceCreationIfEmpty) {
+            ensureClassDeclaration();
+        }
+
+        if (!mStarted) {
+            return;
+        }
+
+        if (processor) {
+            processor->writeToStream(out, prefix);
+        }
+
+        std::string result = mOut.str();
+        for (StringPiece line : util::tokenize<char>(result, '\n')) {
+            *out << prefix << line << "\n";
+        }
+        *out << prefix << "}\n";
+    }
+
+private:
+    constexpr static const char* kIndent = "  ";
+
+    // The number of attributes to emit per line in a Styleable array.
+    constexpr static size_t kAttribsPerLine = 4;
+
+    void ensureClassDeclaration() {
+        if (!mStarted) {
+            mStarted = true;
+            mOut << "public static final class " << mName << " {\n";
+        }
+    }
+
+    std::stringstream mOut;
+    std::string mName;
+    ClassDefinitionWriterOptions mOptions;
+    bool mStarted;
+};
+
+} // namespace aapt
+
+#endif /* AAPT_JAVA_CLASSDEFINITION_H */
diff --git a/tools/aapt2/java/JavaClassGenerator.cpp b/tools/aapt2/java/JavaClassGenerator.cpp
index dfd2ef6..7280f3a 100644
--- a/tools/aapt2/java/JavaClassGenerator.cpp
+++ b/tools/aapt2/java/JavaClassGenerator.cpp
@@ -21,7 +21,9 @@
 #include "ValueVisitor.h"
 
 #include "java/AnnotationProcessor.h"
+#include "java/ClassDefinitionWriter.h"
 #include "java/JavaClassGenerator.h"
+#include "util/Comparators.h"
 #include "util/StringPiece.h"
 
 #include <algorithm>
@@ -32,9 +34,6 @@
 
 namespace aapt {
 
-// The number of attributes to emit per line in a Styleable array.
-constexpr size_t kAttribsPerLine = 4;
-
 JavaClassGenerator::JavaClassGenerator(ResourceTable* table, JavaClassGeneratorOptions options) :
         mTable(table), mOptions(options) {
 }
@@ -92,12 +91,11 @@
     return true;
 }
 
-void JavaClassGenerator::generateStyleable(const StringPiece16& packageNameToGenerate,
-                                           const std::u16string& entryName,
-                                           const Styleable* styleable,
-                                           std::ostream* out) {
-    const StringPiece finalModifier = mOptions.useFinal ? " final" : "";
-
+void JavaClassGenerator::writeStyleableEntryForClass(ClassDefinitionWriter* outClassDef,
+                                                     AnnotationProcessor* processor,
+                                                     const StringPiece16& packageNameToGenerate,
+                                                     const std::u16string& entryName,
+                                                     const Styleable* styleable) {
     // This must be sorted by resource ID.
     std::vector<std::pair<ResourceId, ResourceNameRef>> sortedAttributes;
     sortedAttributes.reserve(styleable->entries.size());
@@ -110,36 +108,31 @@
     }
     std::sort(sortedAttributes.begin(), sortedAttributes.end());
 
+    auto accessorFunc = [](const std::pair<ResourceId, ResourceNameRef>& a) -> ResourceId {
+        return a.first;
+    };
+
     // First we emit the array containing the IDs of each attribute.
-    *out << "    "
-         << "public static final int[] " << transform(entryName) << " = {";
-
-    const size_t attrCount = sortedAttributes.size();
-    for (size_t i = 0; i < attrCount; i++) {
-        if (i % kAttribsPerLine == 0) {
-            *out << "\n      ";
-        }
-
-        *out << sortedAttributes[i].first;
-        if (i != attrCount - 1) {
-            *out << ", ";
-        }
-    }
-    *out << "\n    };\n";
+    outClassDef->addArrayMember(transform(entryName), processor,
+                                sortedAttributes.begin(),
+                                sortedAttributes.end(),
+                                accessorFunc);
 
     // Now we emit the indices into the array.
+    size_t attrCount = sortedAttributes.size();
     for (size_t i = 0; i < attrCount; i++) {
-        *out << "    "
-             << "public static" << finalModifier
-             << " int " << transform(entryName);
+        std::stringstream name;
+        name << transform(entryName);
 
         // We may reference IDs from other packages, so prefix the entry name with
         // the package.
         const ResourceNameRef& itemName = sortedAttributes[i].second;
         if (!itemName.package.empty() && packageNameToGenerate != itemName.package) {
-            *out << "_" << transform(itemName.package);
+            name << "_" << transform(itemName.package);
         }
-        *out << "_" << transform(itemName.entry) << " = " << i << ";\n";
+        name << "_" << transform(itemName.entry);
+
+        outClassDef->addIntMember(name.str(), nullptr, i);
     }
 }
 
@@ -155,8 +148,8 @@
 
     if (typeMask & android::ResTable_map::TYPE_STRING) {
         processor->appendComment(
-                "<p>May be a string value, using '\\;' to escape characters such as\n"
-                "'\\n' or '\\uxxxx' for a unicode character;");
+                "<p>May be a string value, using '\\\\;' to escape characters such as\n"
+                "'\\\\n' or '\\\\uxxxx' for a unicode character;");
     }
 
     if (typeMask & android::ResTable_map::TYPE_INTEGER) {
@@ -222,14 +215,10 @@
     }
 }
 
-bool JavaClassGenerator::generateType(const StringPiece16& packageNameToGenerate,
-                                      const ResourceTablePackage* package,
-                                      const ResourceTableType* type,
-                                      std::ostream* out) {
-    const StringPiece finalModifier = mOptions.useFinal ? " final" : "";
-
-    std::u16string unmangledPackage;
-    std::u16string unmangledName;
+bool JavaClassGenerator::writeEntriesForClass(ClassDefinitionWriter* outClassDef,
+                                              const StringPiece16& packageNameToGenerate,
+                                              const ResourceTablePackage* package,
+                                              const ResourceTableType* type) {
     for (const auto& entry : type->entries) {
         if (skipSymbol(entry->symbolStatus.state)) {
             continue;
@@ -238,7 +227,8 @@
         ResourceId id(package->id.value(), type->id.value(), entry->id.value());
         assert(id.isValid());
 
-        unmangledName = entry->name;
+        std::u16string unmangledPackage;
+        std::u16string unmangledName = entry->name;
         if (NameMangler::unmangle(&unmangledName, &unmangledPackage)) {
             // The entry name was mangled, and we successfully unmangled it.
             // Check that we want to emit this symbol.
@@ -246,12 +236,10 @@
                 // Skip the entry if it doesn't belong to the package we're writing.
                 continue;
             }
-        } else {
-            if (packageNameToGenerate != package->name) {
-                // We are processing a mangled package name,
-                // but this is a non-mangled resource.
-                continue;
-            }
+        } else if (packageNameToGenerate != package->name) {
+            // We are processing a mangled package name,
+            // but this is a non-mangled resource.
+            continue;
         }
 
         if (!isValidSymbol(unmangledName)) {
@@ -262,39 +250,33 @@
             return false;
         }
 
+        // Build the comments and annotations for this entry.
+
+        AnnotationProcessor processor;
+        if (entry->symbolStatus.state != SymbolState::kUndefined) {
+            processor.appendComment(entry->symbolStatus.comment);
+        }
+
+        for (const auto& configValue : entry->values) {
+            processor.appendComment(configValue.value->getComment());
+        }
+
+        // If this is an Attribute, append the format Javadoc.
+        if (!entry->values.empty()) {
+            if (Attribute* attr = valueCast<Attribute>(entry->values.front().value.get())) {
+                // We list out the available values for the given attribute.
+                addAttributeFormatDoc(&processor, attr);
+            }
+        }
+
         if (type->type == ResourceType::kStyleable) {
             assert(!entry->values.empty());
-            generateStyleable(packageNameToGenerate, unmangledName, static_cast<const Styleable*>(
-                    entry->values.front().value.get()), out);
+            const Styleable* styleable = static_cast<const Styleable*>(
+                    entry->values.front().value.get());
+            writeStyleableEntryForClass(outClassDef, &processor, packageNameToGenerate,
+                                        unmangledName, styleable);
         } else {
-            AnnotationProcessor processor("    ");
-            if (entry->symbolStatus.state != SymbolState::kUndefined) {
-                processor.appendComment(entry->symbolStatus.comment);
-            }
-
-            for (const auto& configValue : entry->values) {
-                processor.appendComment(configValue.value->getComment());
-            }
-
-            if (!entry->values.empty()) {
-                if (Attribute* attr = valueCast<Attribute>(entry->values.front().value.get())) {
-                    // We list out the available values for the given attribute.
-                    addAttributeFormatDoc(&processor, attr);
-                }
-            }
-
-            std::string comment = processor.buildComment();
-            if (!comment.empty()) {
-                *out << comment << "\n";
-            }
-
-            std::string annotations = processor.buildAnnotations();
-            if (!annotations.empty()) {
-                *out << annotations << "\n";
-            }
-
-            *out << "    " << "public static" << finalModifier
-                 << " int " << transform(unmangledName) << " = " << id << ";\n";
+            outClassDef->addResourceMember(transform(unmangledName), &processor, id);
         }
     }
     return true;
@@ -312,17 +294,42 @@
 
     for (const auto& package : mTable->packages) {
         for (const auto& type : package->types) {
-            StringPiece16 typeStr;
             if (type->type == ResourceType::kAttrPrivate) {
-                typeStr = toString(ResourceType::kAttr);
-            } else {
-                typeStr = toString(type->type);
+                continue;
             }
-            *out << "  public static final class " << typeStr << " {\n";
-            if (!generateType(packageNameToGenerate, package.get(), type.get(), out)) {
+
+            ClassDefinitionWriterOptions classOptions;
+            classOptions.useFinalQualifier = mOptions.useFinal;
+            classOptions.forceCreationIfEmpty =
+                    (mOptions.types == JavaClassGeneratorOptions::SymbolTypes::kPublic);
+            ClassDefinitionWriter classDef(toString(type->type), classOptions);
+            bool result = writeEntriesForClass(&classDef, packageNameToGenerate,
+                                               package.get(), type.get());
+            if (!result) {
                 return false;
             }
-            *out << "  }\n";
+
+            if (type->type == ResourceType::kAttr) {
+                // Also include private attributes in this same class.
+                auto iter = std::lower_bound(package->types.begin(), package->types.end(),
+                                             ResourceType::kAttrPrivate, cmp::lessThanType);
+                if (iter != package->types.end() && (*iter)->type == ResourceType::kAttrPrivate) {
+                    result = writeEntriesForClass(&classDef, packageNameToGenerate,
+                                                  package.get(), iter->get());
+                    if (!result) {
+                        return false;
+                    }
+                }
+            }
+
+            AnnotationProcessor processor;
+            if (type->type == ResourceType::kStyleable &&
+                    mOptions.types == JavaClassGeneratorOptions::SymbolTypes::kPublic) {
+                // When generating a public R class, we don't want Styleable to be part of the API.
+                // It is only emitted for documentation purposes.
+                processor.appendComment("@doconly");
+            }
+            classDef.writeToStream(out, "  ", &processor);
         }
     }
 
diff --git a/tools/aapt2/java/JavaClassGenerator.h b/tools/aapt2/java/JavaClassGenerator.h
index e53a765..023d6d6 100644
--- a/tools/aapt2/java/JavaClassGenerator.h
+++ b/tools/aapt2/java/JavaClassGenerator.h
@@ -27,6 +27,9 @@
 
 namespace aapt {
 
+class AnnotationProcessor;
+class ClassDefinitionWriter;
+
 struct JavaClassGeneratorOptions {
     /*
      * Specifies whether to use the 'final' modifier
@@ -40,9 +43,6 @@
         kPublic,
     };
 
-    /*
-     *
-     */
     SymbolTypes types = SymbolTypes::kAll;
 };
 
@@ -69,15 +69,16 @@
     const std::string& getError() const;
 
 private:
-    bool generateType(const StringPiece16& packageNameToGenerate,
-                      const ResourceTablePackage* package,
-                      const ResourceTableType* type,
-                      std::ostream* out);
+    bool writeEntriesForClass(ClassDefinitionWriter* outClassDef,
+                              const StringPiece16& packageNameToGenerate,
+                              const ResourceTablePackage* package,
+                              const ResourceTableType* type);
 
-    void generateStyleable(const StringPiece16& packageNameToGenerate,
-                           const std::u16string& entryName,
-                           const Styleable* styleable,
-                           std::ostream* out);
+    void writeStyleableEntryForClass(ClassDefinitionWriter* outClassDef,
+                                     AnnotationProcessor* processor,
+                                     const StringPiece16& packageNameToGenerate,
+                                     const std::u16string& entryName,
+                                     const Styleable* styleable);
 
     bool skipSymbol(SymbolState state);
 
diff --git a/tools/aapt2/java/JavaClassGenerator_test.cpp b/tools/aapt2/java/JavaClassGenerator_test.cpp
index 2dc387b..e9e7881 100644
--- a/tools/aapt2/java/JavaClassGenerator_test.cpp
+++ b/tools/aapt2/java/JavaClassGenerator_test.cpp
@@ -56,13 +56,13 @@
     std::string output = out.str();
 
     EXPECT_NE(std::string::npos,
-              output.find("public static final int hey_man = 0x01020000;"));
+              output.find("public static final int hey_man=0x01020000;"));
 
     EXPECT_NE(std::string::npos,
-              output.find("public static final int[] hey_dude = {"));
+              output.find("public static final int[] hey_dude={"));
 
     EXPECT_NE(std::string::npos,
-              output.find("public static final int hey_dude_cool_attr = 0;"));
+              output.find("public static final int hey_dude_cool_attr=0;"));
 }
 
 TEST(JavaClassGeneratorTest, CorrectPackageNameIsUsed) {
@@ -78,7 +78,7 @@
 
     std::string output = out.str();
     EXPECT_NE(std::string::npos, output.find("package com.android.internal;"));
-    EXPECT_NE(std::string::npos, output.find("public static final int one = 0x01020000;"));
+    EXPECT_NE(std::string::npos, output.find("public static final int one=0x01020000;"));
     EXPECT_EQ(std::string::npos, output.find("two"));
     EXPECT_EQ(std::string::npos, output.find("com_foo$two"));
 }
@@ -86,6 +86,7 @@
 TEST(JavaClassGeneratorTest, AttrPrivateIsWrittenAsAttr) {
     std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder()
             .setPackageId(u"android", 0x01)
+            .addSimple(u"@android:attr/two", ResourceId(0x01010001))
             .addSimple(u"@android:^attr-private/one", ResourceId(0x01010000))
             .build();
 
@@ -116,7 +117,7 @@
         std::stringstream out;
         ASSERT_TRUE(generator.generate(u"android", &out));
         std::string output = out.str();
-        EXPECT_NE(std::string::npos, output.find("public static final int one = 0x01020000;"));
+        EXPECT_NE(std::string::npos, output.find("public static final int one=0x01020000;"));
         EXPECT_EQ(std::string::npos, output.find("two"));
         EXPECT_EQ(std::string::npos, output.find("three"));
     }
@@ -127,8 +128,8 @@
         std::stringstream out;
         ASSERT_TRUE(generator.generate(u"android", &out));
         std::string output = out.str();
-        EXPECT_NE(std::string::npos, output.find("public static final int one = 0x01020000;"));
-        EXPECT_NE(std::string::npos, output.find("public static final int two = 0x01020001;"));
+        EXPECT_NE(std::string::npos, output.find("public static final int one=0x01020000;"));
+        EXPECT_NE(std::string::npos, output.find("public static final int two=0x01020001;"));
         EXPECT_EQ(std::string::npos, output.find("three"));
     }
 
@@ -138,9 +139,9 @@
         std::stringstream out;
         ASSERT_TRUE(generator.generate(u"android", &out));
         std::string output = out.str();
-        EXPECT_NE(std::string::npos, output.find("public static final int one = 0x01020000;"));
-        EXPECT_NE(std::string::npos, output.find("public static final int two = 0x01020001;"));
-        EXPECT_NE(std::string::npos, output.find("public static final int three = 0x01020002;"));
+        EXPECT_NE(std::string::npos, output.find("public static final int one=0x01020000;"));
+        EXPECT_NE(std::string::npos, output.find("public static final int two=0x01020001;"));
+        EXPECT_NE(std::string::npos, output.find("public static final int three=0x01020002;"));
     }
 }
 
@@ -194,8 +195,8 @@
     EXPECT_TRUE(generator.generate(u"android", &out));
 
     std::string output = out.str();
-    EXPECT_NE(std::string::npos, output.find("int foo_bar ="));
-    EXPECT_NE(std::string::npos, output.find("int foo_com_lib_bar ="));
+    EXPECT_NE(std::string::npos, output.find("int foo_bar="));
+    EXPECT_NE(std::string::npos, output.find("int foo_com_lib_bar="));
 }
 
 TEST(JavaClassGeneratorTest, CommentsForSimpleResourcesArePresent) {
@@ -218,7 +219,7 @@
      * @deprecated
      */
     @Deprecated
-    public static final int foo = 0x01010000;)EOF"));
+    public static final int foo=0x01010000;)EOF"));
 }
 
 TEST(JavaClassGeneratorTest, CommentsForEnumAndFlagAttributesArePresent) {
diff --git a/tools/aapt2/java/ManifestClassGenerator.cpp b/tools/aapt2/java/ManifestClassGenerator.cpp
index 901a344..d963d89 100644
--- a/tools/aapt2/java/ManifestClassGenerator.cpp
+++ b/tools/aapt2/java/ManifestClassGenerator.cpp
@@ -18,6 +18,7 @@
 #include "XmlDom.h"
 
 #include "java/AnnotationProcessor.h"
+#include "java/ClassDefinitionWriter.h"
 #include "java/ManifestClassGenerator.h"
 #include "util/Maybe.h"
 
@@ -58,8 +59,8 @@
     return result;
 }
 
-static bool writeSymbol(IDiagnostics* diag, const Source& source, xml::Element* el,
-                        std::ostream* out) {
+static bool writeSymbol(IDiagnostics* diag, ClassDefinitionWriter* outClassDef, const Source& source,
+                        xml::Element* el) {
     xml::Attribute* attr = el->findAttribute(xml::kSchemaAndroid, u"name");
     if (!attr) {
         diag->error(DiagMessage(source) << "<" << el->name << "> must define 'android:name'");
@@ -72,18 +73,9 @@
         return false;
     }
 
-    *out << "\n";
-
-    if (!util::trimWhitespace(el->comment).empty()) {
-        AnnotationProcessor processor("    ");
-        processor.appendComment(el->comment);
-        *out << processor.buildComment() << "\n";
-        std::string annotations = processor.buildAnnotations();
-        if (!annotations.empty()) {
-            *out << annotations << "\n";
-        }
-    }
-    *out << "    public static final String " << result.value() << "=\"" << attr->value << "\";\n";
+    AnnotationProcessor processor;
+    processor.appendComment(el->comment);
+    outClassDef->addStringMember(result.value(), &processor, attr->value);
     return true;
 }
 
@@ -100,29 +92,32 @@
     }
 
     *out << "package " << package << ";\n\n"
-         << "public class Manifest {\n";
+         << "public final class Manifest {\n";
 
     bool error = false;
     std::vector<xml::Element*> children = el->getChildElements();
 
+    ClassDefinitionWriterOptions classOptions;
+    classOptions.useFinalQualifier = true;
+    classOptions.forceCreationIfEmpty = false;
 
     // First write out permissions.
-    *out << "  public static class permission {\n";
+    ClassDefinitionWriter classDef("permission", classOptions);
     for (xml::Element* childEl : children) {
         if (childEl->namespaceUri.empty() && childEl->name == u"permission") {
-            error |= !writeSymbol(diag, res->file.source, childEl, out);
+            error |= !writeSymbol(diag, &classDef, res->file.source, childEl);
         }
     }
-    *out << "  }\n";
+    classDef.writeToStream(out, "  ");
 
     // Next write out permission groups.
-    *out << "  public static class permission_group {\n";
+    classDef = ClassDefinitionWriter("permission_group", classOptions);
     for (xml::Element* childEl : children) {
         if (childEl->namespaceUri.empty() && childEl->name == u"permission-group") {
-            error |= !writeSymbol(diag, res->file.source, childEl, out);
+            error |= !writeSymbol(diag, &classDef, res->file.source, childEl);
         }
     }
-    *out << "  }\n";
+    classDef.writeToStream(out, "  ");
 
     *out << "}\n";
     return !error;
diff --git a/tools/aapt2/java/ManifestClassGenerator_test.cpp b/tools/aapt2/java/ManifestClassGenerator_test.cpp
index 1b5bc05..4081287 100644
--- a/tools/aapt2/java/ManifestClassGenerator_test.cpp
+++ b/tools/aapt2/java/ManifestClassGenerator_test.cpp
@@ -39,8 +39,9 @@
 
     std::string actual = out.str();
 
-    const size_t permissionClassPos = actual.find("public static class permission {");
-    const size_t permissionGroupClassPos = actual.find("public static class permission_group {");
+    const size_t permissionClassPos = actual.find("public static final class permission {");
+    const size_t permissionGroupClassPos =
+            actual.find("public static final class permission_group {");
     ASSERT_NE(std::string::npos, permissionClassPos);
     ASSERT_NE(std::string::npos, permissionGroupClassPos);
 
@@ -113,7 +114,7 @@
      * @hide
      * @SystemApi
      */
-    @android.annotations.SystemApi
+    @android.annotation.SystemApi
     public static final String SECRET="android.permission.SECRET";)EOF"));
 }
 
diff --git a/tools/aapt2/link/AutoVersioner.cpp b/tools/aapt2/link/AutoVersioner.cpp
index 11fcc5d..c7e603e 100644
--- a/tools/aapt2/link/AutoVersioner.cpp
+++ b/tools/aapt2/link/AutoVersioner.cpp
@@ -31,7 +31,7 @@
                                      const int sdkVersionToGenerate) {
     assert(sdkVersionToGenerate > config.sdkVersion);
     const auto endIter = entry->values.end();
-    auto iter = std::lower_bound(entry->values.begin(), endIter, config, cmp::lessThan);
+    auto iter = std::lower_bound(entry->values.begin(), endIter, config, cmp::lessThanConfig);
 
     // The source config came from this list, so it should be here.
     assert(iter != entry->values.end());
@@ -124,7 +124,7 @@
                                 auto iter = std::lower_bound(entry->values.begin(),
                                                              entry->values.end(),
                                                              newConfig,
-                                                             cmp::lessThan);
+                                                             cmp::lessThanConfig);
 
                                 entry->values.insert(
                                         iter,
diff --git a/tools/aapt2/link/Link.cpp b/tools/aapt2/link/Link.cpp
index 0236e98..9ce3734 100644
--- a/tools/aapt2/link/Link.cpp
+++ b/tools/aapt2/link/Link.cpp
@@ -49,6 +49,7 @@
     std::string manifestPath;
     std::vector<std::string> includePaths;
     Maybe<std::string> generateJavaClassPath;
+    std::vector<std::string> extraJavaPackages;
     Maybe<std::string> generateProguardRulesPath;
     bool noAutoVersion = false;
     bool staticLib = false;
@@ -695,6 +696,9 @@
                 options.useFinal = false;
             }
 
+            StringPiece16 actualPackage = mContext.getCompilationPackage();
+            StringPiece16 outputPackage = mContext.getCompilationPackage();
+
             if (mOptions.privateSymbols) {
                 // If we defined a private symbols package, we only emit Public symbols
                 // to the original package, and private and public symbols to the private package.
@@ -706,16 +710,16 @@
                 }
 
                 options.types = JavaClassGeneratorOptions::SymbolTypes::kPublicPrivate;
-                if (!writeJavaFile(&mergedTable, mContext.getCompilationPackage(),
-                                   mOptions.privateSymbols.value(), options)) {
-                    return 1;
-                }
+                outputPackage = mOptions.privateSymbols.value();
+            }
 
-            } else {
-                // Emit Everything.
+            if (!writeJavaFile(&mergedTable, actualPackage, outputPackage, options)) {
+                return 1;
+            }
 
-                if (!writeJavaFile(&mergedTable, mContext.getCompilationPackage(),
-                                   mContext.getCompilationPackage(), options)) {
+            for (std::string& extraPackage : mOptions.extraJavaPackages) {
+                if (!writeJavaFile(&mergedTable, actualPackage, util::utf8ToUtf16(extraPackage),
+                                   options)) {
                     return 1;
                 }
             }
@@ -770,6 +774,8 @@
                           "private symbols.\n"
                           "If not specified, public and private symbols will use the application's "
                           "package name", &privateSymbolsPackage)
+            .optionalFlagList("--extra-packages", "Generate the same R.java but with different "
+                              "package names", &options.extraJavaPackages)
             .optionalSwitch("-v", "Enables verbose logging", &options.verbose);
 
     if (!flags.parse("aapt2 link", args, &std::cerr)) {
diff --git a/tools/aapt2/link/TableMerger.cpp b/tools/aapt2/link/TableMerger.cpp
index 636c2ba..1eea410 100644
--- a/tools/aapt2/link/TableMerger.cpp
+++ b/tools/aapt2/link/TableMerger.cpp
@@ -34,6 +34,9 @@
     assert(mMasterPackage && "package name or ID already taken");
 }
 
+/**
+ * This will merge packages with the same package name (or no package name).
+ */
 bool TableMerger::merge(const Source& src, ResourceTable* table) {
     const uint8_t desiredPackageId = mContext->getPackageId();
 
@@ -46,18 +49,37 @@
             continue;
         }
 
-        bool manglePackage = false;
-        if (!package->name.empty() && mContext->getCompilationPackage() != package->name) {
-            manglePackage = true;
-            mMergedPackages.insert(package->name);
+        if (package->name.empty() || mContext->getCompilationPackage() == package->name) {
+            // Merge here. Once the entries are merged and mangled, any references to
+            // them are still valid. This is because un-mangled references are
+            // mangled, then looked up at resolution time.
+            // Also, when linking, we convert references with no package name to use
+            // the compilation package name.
+            if (!doMerge(src, table, package.get(), false)) {
+                error = true;
+            }
+        }
+    }
+    return !error;
+}
+
+/**
+ * This will merge and mangle resources from a static library.
+ */
+bool TableMerger::mergeAndMangle(const Source& src, const StringPiece16& packageName,
+                                 ResourceTable* table) {
+    bool error = false;
+    for (auto& package : table->packages) {
+        // Warn of packages with an unrelated ID.
+        if (packageName != package->name) {
+            mContext->getDiagnostics()->warn(DiagMessage(src)
+                                             << "ignoring package " << package->name);
+            continue;
         }
 
-        // Merge here. Once the entries are merged and mangled, any references to
-        // them are still valid. This is because un-mangled references are
-        // mangled, then looked up at resolution time.
-        // Also, when linking, we convert references with no package name to use
-        // the compilation package name.
-        if (!doMerge(src, table, package.get(), manglePackage)) {
+        bool mangle = packageName != mContext->getCompilationPackage();
+        mMergedPackages.insert(package->name);
+        if (!doMerge(src, table, package.get(), mangle)) {
             error = true;
         }
     }
@@ -122,7 +144,7 @@
 
             for (ResourceConfigValue& srcValue : srcEntry->values) {
                 auto iter = std::lower_bound(dstEntry->values.begin(), dstEntry->values.end(),
-                                             srcValue.config, cmp::lessThan);
+                                             srcValue.config, cmp::lessThanConfig);
 
                 if (iter != dstEntry->values.end() && iter->config == srcValue.config) {
                     const int collisionResult = ResourceTable::resolveValueCollision(
diff --git a/tools/aapt2/link/TableMerger.h b/tools/aapt2/link/TableMerger.h
index 157c16e..c903f1b 100644
--- a/tools/aapt2/link/TableMerger.h
+++ b/tools/aapt2/link/TableMerger.h
@@ -60,8 +60,16 @@
         return mMergedPackages;
     }
 
+    /**
+     * Merges resources from the same or empty package. This is for local sources.
+     */
     bool merge(const Source& src, ResourceTable* table);
 
+    /**
+     * Merges resources from the given package, mangling the name. This is for static libraries.
+     */
+    bool mergeAndMangle(const Source& src, const StringPiece16& package, ResourceTable* table);
+
 private:
     IAaptContext* mContext;
     ResourceTable* mMasterTable;
diff --git a/tools/aapt2/link/TableMerger_test.cpp b/tools/aapt2/link/TableMerger_test.cpp
index fa7ce86..0af4314 100644
--- a/tools/aapt2/link/TableMerger_test.cpp
+++ b/tools/aapt2/link/TableMerger_test.cpp
@@ -60,7 +60,7 @@
     TableMerger merger(mContext.get(), &finalTable);
 
     ASSERT_TRUE(merger.merge({}, tableA.get()));
-    ASSERT_TRUE(merger.merge({}, tableB.get()));
+    ASSERT_TRUE(merger.mergeAndMangle({}, u"com.app.b", tableB.get()));
 
     EXPECT_TRUE(merger.getMergedPackages().count(u"com.app.b") != 0);
 
@@ -90,7 +90,7 @@
     TableMerger merger(mContext.get(), &finalTable);
 
     ASSERT_TRUE(merger.merge({}, tableA.get()));
-    ASSERT_TRUE(merger.merge({}, tableB.get()));
+    ASSERT_TRUE(merger.mergeAndMangle({}, u"com.app.b", tableB.get()));
 
     FileReference* f = test::getValue<FileReference>(&finalTable, u"@com.app.a:xml/file");
     ASSERT_NE(f, nullptr);
diff --git a/tools/aapt2/process/SymbolTable.cpp b/tools/aapt2/process/SymbolTable.cpp
index 7309396..9a8b263 100644
--- a/tools/aapt2/process/SymbolTable.cpp
+++ b/tools/aapt2/process/SymbolTable.cpp
@@ -55,7 +55,7 @@
     if (name.type == ResourceType::kAttr || name.type == ResourceType::kAttrPrivate) {
         const ConfigDescription kDefaultConfig;
         auto iter = std::lower_bound(sr.entry->values.begin(), sr.entry->values.end(),
-                                     kDefaultConfig, cmp::lessThan);
+                                     kDefaultConfig, cmp::lessThanConfig);
 
         if (iter != sr.entry->values.end() && iter->config == kDefaultConfig) {
             // This resource has an Attribute.
diff --git a/tools/aapt2/util/Comparators.h b/tools/aapt2/util/Comparators.h
index 652018e..0ee0bf3 100644
--- a/tools/aapt2/util/Comparators.h
+++ b/tools/aapt2/util/Comparators.h
@@ -17,13 +17,20 @@
 #ifndef AAPT_UTIL_COMPARATORS_H
 #define AAPT_UTIL_COMPARATORS_H
 
+#include "ConfigDescription.h"
+#include "ResourceTable.h"
+
 namespace aapt {
 namespace cmp {
 
-inline bool lessThan(const ResourceConfigValue& a, const ConfigDescription& b) {
+inline bool lessThanConfig(const ResourceConfigValue& a, const ConfigDescription& b) {
     return a.config < b;
 }
 
+inline bool lessThanType(const std::unique_ptr<ResourceTableType>& a, ResourceType b) {
+    return a->type < b;
+}
+
 } // namespace cmp
 } // namespace aapt
 
diff --git a/tools/layoutlib/bridge/src/android/view/IWindowManagerImpl.java b/tools/layoutlib/bridge/src/android/view/IWindowManagerImpl.java
index 7ee06f3..3c260a8 100644
--- a/tools/layoutlib/bridge/src/android/view/IWindowManagerImpl.java
+++ b/tools/layoutlib/bridge/src/android/view/IWindowManagerImpl.java
@@ -241,6 +241,13 @@
     }
 
     @Override
+    public void overridePendingAppTransitionMultiThumbFuture(
+            IAppTransitionAnimationSpecsFuture specsFuture, IRemoteCallback startedCallback,
+            boolean scaleUp) throws RemoteException {
+
+    }
+
+    @Override
     public void overridePendingAppTransitionMultiThumb(AppTransitionAnimationSpec[] specs,
             IRemoteCallback callback0, IRemoteCallback callback1, boolean scaleUp) {
         // TODO Auto-generated method stub
@@ -515,10 +522,23 @@
     }
 
     @Override
+    public int getDockedStackSide() throws RemoteException {
+        return 0;
+    }
+
+    @Override
+    public void setDockedStackResizing(boolean resizing) throws RemoteException {
+    }
+
+    @Override
     public void cancelTaskWindowTransition(int taskId) {
     }
 
     @Override
+    public void cancelTaskThumbnailTransition(int taskId) {
+    }
+
+    @Override
     public void endProlongedAnimations() {
     }
 }
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindow.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindow.java
index c2d8d0c..3662573 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindow.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindow.java
@@ -48,7 +48,7 @@
 
     @Override
     public void resized(Rect rect, Rect rect2, Rect rect3, Rect rect4, Rect rect5, Rect rect6,
-            boolean b, Configuration configuration) throws RemoteException {
+            boolean b, Configuration configuration, Rect rect7) throws RemoteException {
         // pass for now.
     }