Merge "Change copy/move destination to Downloads when home directory is hidden." into nyc-dev
diff --git a/core/java/android/app/DownloadManager.java b/core/java/android/app/DownloadManager.java
index ed4bb28..536c4a8 100644
--- a/core/java/android/app/DownloadManager.java
+++ b/core/java/android/app/DownloadManager.java
@@ -1319,7 +1319,7 @@
                     return getLocalUri();
                 case COLUMN_LOCAL_FILENAME:
                     if (!mAccessFilename) {
-                        throw new IllegalArgumentException(
+                        throw new SecurityException(
                                 "COLUMN_LOCAL_FILENAME is deprecated;"
                                         + " use ContentResolver.openFileDescriptor() instead");
                     }
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 9d7f724..2987fbc 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -69,6 +69,7 @@
 import android.media.tv.ITvInputManager;
 import android.media.tv.TvInputManager;
 import android.net.ConnectivityManager;
+import android.net.ConnectivityThread;
 import android.net.EthernetManager;
 import android.net.IConnectivityManager;
 import android.net.IEthernetManager;
@@ -500,7 +501,8 @@
             public WifiManager createService(ContextImpl ctx) {
                 IBinder b = ServiceManager.getService(Context.WIFI_SERVICE);
                 IWifiManager service = IWifiManager.Stub.asInterface(b);
-                return new WifiManager(ctx.getOuterContext(), service);
+                return new WifiManager(ctx.getOuterContext(), service,
+                        ConnectivityThread.getInstanceLooper());
             }});
 
         registerService(Context.WIFI_P2P_SERVICE, WifiP2pManager.class,
diff --git a/core/java/android/net/ConnectivityThread.java b/core/java/android/net/ConnectivityThread.java
new file mode 100644
index 0000000..55c3402
--- /dev/null
+++ b/core/java/android/net/ConnectivityThread.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2016 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.net;
+
+import android.os.HandlerThread;
+import android.os.Looper;
+
+/**
+ * Shared singleton connectivity thread for the system.  This is a thread for
+ * connectivity operations such as AsyncChannel connections to system services.
+ * Various connectivity manager objects can use this singleton as a common
+ * resource for their handlers instead of creating separate threads of their own.
+ * @hide
+ */
+public final class ConnectivityThread extends HandlerThread {
+    private static ConnectivityThread sInstance;
+
+    private ConnectivityThread() {
+        super("ConnectivityThread");
+    }
+
+    private static synchronized ConnectivityThread getInstance() {
+        if (sInstance == null) {
+            sInstance = new ConnectivityThread();
+            sInstance.start();
+        }
+        return sInstance;
+    }
+
+    public static ConnectivityThread get() {
+        return getInstance();
+    }
+
+    public static Looper getInstanceLooper() {
+        return getInstance().getLooper();
+    }
+}
diff --git a/core/java/android/net/TrafficStats.java b/core/java/android/net/TrafficStats.java
index 95ffb44..cfd0468 100644
--- a/core/java/android/net/TrafficStats.java
+++ b/core/java/android/net/TrafficStats.java
@@ -147,8 +147,10 @@
     }
 
     /**
-     * System API for backup-related support components to tag network traffic
-     * appropriately.
+     * Set active tag to use when accounting {@link Socket} traffic originating
+     * from the current thread. The tag used internally is well-defined to
+     * distinguish all backup-related traffic.
+     *
      * @hide
      */
     @SystemApi
@@ -157,8 +159,10 @@
     }
 
     /**
-     * System API for restore-related support components to tag network traffic
-     * appropriately.
+     * Set active tag to use when accounting {@link Socket} traffic originating
+     * from the current thread. The tag used internally is well-defined to
+     * distinguish all restore-related traffic.
+     *
      * @hide
      */
     @SystemApi
@@ -205,7 +209,13 @@
         NetworkManagementSocketTagger.setThreadSocketStatsUid(uid);
     }
 
-    /** {@hide} */
+    /**
+     * Clear any active UID set to account {@link Socket} traffic originating
+     * from the current thread.
+     *
+     * @see #setThreadStatsUid(int)
+     * @hide
+     */
     @SystemApi
     public static void clearThreadStatsUid() {
         NetworkManagementSocketTagger.setThreadSocketStatsUid(-1);
diff --git a/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java b/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java
index dcf987b..d9227ce 100644
--- a/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java
+++ b/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java
@@ -16,17 +16,20 @@
 
 package android.util.apk;
 
+import android.system.ErrnoException;
+import android.system.OsConstants;
+import android.util.ArrayMap;
 import android.util.Pair;
 
 import java.io.ByteArrayInputStream;
+import java.io.FileDescriptor;
 import java.io.IOException;
 import java.io.RandomAccessFile;
 import java.math.BigInteger;
 import java.nio.BufferUnderflowException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
-import java.nio.MappedByteBuffer;
-import java.nio.channels.FileChannel;
+import java.nio.DirectByteBuffer;
 import java.security.DigestException;
 import java.security.InvalidAlgorithmParameterException;
 import java.security.InvalidKeyException;
@@ -52,11 +55,13 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Date;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import libcore.io.Libcore;
+import libcore.io.Os;
+
 /**
  * APK Signature Scheme v2 verifier.
  *
@@ -75,44 +80,17 @@
     public static final int SF_ATTRIBUTE_ANDROID_APK_SIGNED_ID = 2;
 
     /**
-     * Returns {@code true} if the provided APK contains an APK Signature Scheme V2
-     * signature. The signature will not be verified.
+     * Returns {@code true} if the provided APK contains an APK Signature Scheme V2 signature.
+     *
+     * <p><b>NOTE: This method does not verify the signature.</b>
      */
     public static boolean hasSignature(String apkFile) throws IOException {
         try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) {
-            long fileSize = apk.length();
-            if (fileSize > Integer.MAX_VALUE) {
-                return false;
-            }
-            MappedByteBuffer apkContents;
-            try {
-                apkContents = apk.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, fileSize);
-            } catch (IOException e) {
-                if (e.getCause() instanceof OutOfMemoryError) {
-                    // TODO: Remove this temporary workaround once verifying large APKs is
-                    // supported. Very large APKs cannot be memory-mapped. This verification code
-                    // needs to change to use a different approach for verifying such APKs.
-                    return false; // Pretend that this APK does not have a v2 signature.
-                } else {
-                    throw new IOException("Failed to memory-map APK", e);
-                }
-            }
-            // ZipUtils and APK Signature Scheme v2 verifier expect little-endian byte order.
-            apkContents.order(ByteOrder.LITTLE_ENDIAN);
-
-            final int centralDirOffset =
-                    (int) getCentralDirOffset(apkContents, getEocdOffset(apkContents));
-            // Find the APK Signing Block.
-            int apkSigningBlockOffset = findApkSigningBlock(apkContents, centralDirOffset);
-            ByteBuffer apkSigningBlock =
-                    sliceFromTo(apkContents, apkSigningBlockOffset, centralDirOffset);
-
-            // Find the APK Signature Scheme v2 Block inside the APK Signing Block.
-            findApkSignatureSchemeV2Block(apkSigningBlock);
+            findSignature(apk);
             return true;
         } catch (SignatureNotFoundException e) {
+            return false;
         }
-        return false;
     }
 
     /**
@@ -135,90 +113,97 @@
      * associated with each signer.
      *
      * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2.
-     * @throws SecurityException if a APK Signature Scheme v2 signature of this APK does not verify.
+     * @throws SecurityException if an APK Signature Scheme v2 signature of this APK does not
+     *         verify.
      * @throws IOException if an I/O error occurs while reading the APK file.
      */
-    public static X509Certificate[][] verify(RandomAccessFile apk)
+    private static X509Certificate[][] verify(RandomAccessFile apk)
             throws SignatureNotFoundException, SecurityException, IOException {
-
-        long fileSize = apk.length();
-        if (fileSize > Integer.MAX_VALUE) {
-            throw new IOException("File too large: " + apk.length() + " bytes");
-        }
-        MappedByteBuffer apkContents;
-        try {
-            apkContents = apk.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, fileSize);
-            // Attempt to preload the contents into memory for faster overall verification (v2 and
-            // older) at the expense of somewhat increased latency for rejecting malformed APKs.
-            apkContents.load();
-        } catch (IOException e) {
-            if (e.getCause() instanceof OutOfMemoryError) {
-                // TODO: Remove this temporary workaround once verifying large APKs is supported.
-                // Very large APKs cannot be memory-mapped. This verification code needs to change
-                // to use a different approach for verifying such APKs.
-                // This workaround pretends that this APK does not have a v2 signature. This works
-                // fine provided the APK is not actually v2-signed. If the APK is v2 signed, v2
-                // signature stripping protection inside v1 signature verification code will reject
-                // this APK.
-                throw new SignatureNotFoundException("Failed to memory-map APK", e);
-            } else {
-                throw new IOException("Failed to memory-map APK", e);
-            }
-        }
-        return verify(apkContents);
+        SignatureInfo signatureInfo = findSignature(apk);
+        return verify(apk.getFD(), signatureInfo);
     }
 
     /**
-     * Verifies APK Signature Scheme v2 signatures of the provided APK and returns the certificates
-     * associated with each signer.
-     *
-     * @param apkContents contents of the APK. The contents start at the current position and end
-     *        at the limit of the buffer.
+     * APK Signature Scheme v2 block and additional information relevant to verifying the signatures
+     * contained in the block against the file.
+     */
+    private static class SignatureInfo {
+        /** Contents of APK Signature Scheme v2 block. */
+        private final ByteBuffer signatureBlock;
+
+        /** Position of the APK Signing Block in the file. */
+        private final long apkSigningBlockOffset;
+
+        /** Position of the ZIP Central Directory in the file. */
+        private final long centralDirOffset;
+
+        /** Position of the ZIP End of Central Directory (EoCD) in the file. */
+        private final long eocdOffset;
+
+        /** Contents of ZIP End of Central Directory (EoCD) of the file. */
+        private final ByteBuffer eocd;
+
+        private SignatureInfo(
+                ByteBuffer signatureBlock,
+                long apkSigningBlockOffset,
+                long centralDirOffset,
+                long eocdOffset,
+                ByteBuffer eocd) {
+            this.signatureBlock = signatureBlock;
+            this.apkSigningBlockOffset = apkSigningBlockOffset;
+            this.centralDirOffset = centralDirOffset;
+            this.eocdOffset = eocdOffset;
+            this.eocd = eocd;
+        }
+    }
+
+    /**
+     * Returns the APK Signature Scheme v2 block contained in the provided APK file and the
+     * additional information relevant for verifying the block against the file.
      *
      * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2.
-     * @throws SecurityException if a APK Signature Scheme v2 signature of this APK does not verify.
+     * @throws IOException if an I/O error occurs while reading the APK file.
      */
-    public static X509Certificate[][] verify(ByteBuffer apkContents)
-            throws SignatureNotFoundException, SecurityException {
-        // Avoid modifying byte order, position, limit, and mark of the original apkContents.
-        apkContents = apkContents.slice();
+    private static SignatureInfo findSignature(RandomAccessFile apk)
+            throws IOException, SignatureNotFoundException {
+        // Find the ZIP End of Central Directory (EoCD) record.
+        Pair<ByteBuffer, Long> eocdAndOffsetInFile = getEocd(apk);
+        ByteBuffer eocd = eocdAndOffsetInFile.first;
+        long eocdOffset = eocdAndOffsetInFile.second;
+        if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(apk, eocdOffset)) {
+            throw new SignatureNotFoundException("ZIP64 APK not supported");
+        }
 
-        // ZipUtils and APK Signature Scheme v2 verifier expect little-endian byte order.
-        apkContents.order(ByteOrder.LITTLE_ENDIAN);
-
-        final int eocdOffset = getEocdOffset(apkContents);
-        final int centralDirOffset = (int) getCentralDirOffset(apkContents, eocdOffset);
-
-        // Find the APK Signing Block.
-        int apkSigningBlockOffset = findApkSigningBlock(apkContents, centralDirOffset);
-        ByteBuffer apkSigningBlock =
-                sliceFromTo(apkContents, apkSigningBlockOffset, centralDirOffset);
+        // Find the APK Signing Block. The block immediately precedes the Central Directory.
+        long centralDirOffset = getCentralDirOffset(eocd, eocdOffset);
+        Pair<ByteBuffer, Long> apkSigningBlockAndOffsetInFile =
+                findApkSigningBlock(apk, centralDirOffset);
+        ByteBuffer apkSigningBlock = apkSigningBlockAndOffsetInFile.first;
+        long apkSigningBlockOffset = apkSigningBlockAndOffsetInFile.second;
 
         // Find the APK Signature Scheme v2 Block inside the APK Signing Block.
         ByteBuffer apkSignatureSchemeV2Block = findApkSignatureSchemeV2Block(apkSigningBlock);
 
-        // Verify the contents of the APK outside of the APK Signing Block using the APK Signature
-        // Scheme v2 Block.
-        return verify(
-                apkContents,
+        return new SignatureInfo(
                 apkSignatureSchemeV2Block,
                 apkSigningBlockOffset,
                 centralDirOffset,
-                eocdOffset);
+                eocdOffset,
+                eocd);
     }
 
     /**
-     * Verifies the contents outside of the APK Signing Block using the provided APK Signature
-     * Scheme v2 Block.
+     * Verifies the contents of the provided APK file against the provided APK Signature Scheme v2
+     * Block.
+     *
+     * @param signatureInfo APK Signature Scheme v2 Block and information relevant for verifying it
+     *        against the APK file.
      */
     private static X509Certificate[][] verify(
-            ByteBuffer apkContents,
-            ByteBuffer v2Block,
-            int apkSigningBlockOffset,
-            int centralDirOffset,
-            int eocdOffset) throws SecurityException {
+            FileDescriptor apkFileDescriptor,
+            SignatureInfo signatureInfo) throws SecurityException {
         int signerCount = 0;
-        Map<Integer, byte[]> contentDigests = new HashMap<>();
+        Map<Integer, byte[]> contentDigests = new ArrayMap<>();
         List<X509Certificate[]> signerCerts = new ArrayList<>();
         CertificateFactory certFactory;
         try {
@@ -228,7 +213,7 @@
         }
         ByteBuffer signers;
         try {
-            signers = getLengthPrefixedSlice(v2Block);
+            signers = getLengthPrefixedSlice(signatureInfo.signatureBlock);
         } catch (IOException e) {
             throw new SecurityException("Failed to read list of signers", e);
         }
@@ -255,10 +240,11 @@
 
         verifyIntegrity(
                 contentDigests,
-                apkContents,
-                apkSigningBlockOffset,
-                centralDirOffset,
-                eocdOffset);
+                apkFileDescriptor,
+                signatureInfo.apkSigningBlockOffset,
+                signatureInfo.centralDirOffset,
+                signatureInfo.eocdOffset,
+                signatureInfo.eocd);
 
         return signerCerts.toArray(new X509Certificate[signerCerts.size()][]);
     }
@@ -401,25 +387,38 @@
 
     private static void verifyIntegrity(
             Map<Integer, byte[]> expectedDigests,
-            ByteBuffer apkContents,
-            int apkSigningBlockOffset,
-            int centralDirOffset,
-            int eocdOffset) throws SecurityException {
+            FileDescriptor apkFileDescriptor,
+            long apkSigningBlockOffset,
+            long centralDirOffset,
+            long eocdOffset,
+            ByteBuffer eocdBuf) throws SecurityException {
 
         if (expectedDigests.isEmpty()) {
             throw new SecurityException("No digests provided");
         }
 
-        ByteBuffer beforeApkSigningBlock = sliceFromTo(apkContents, 0, apkSigningBlockOffset);
-        ByteBuffer centralDir = sliceFromTo(apkContents, centralDirOffset, eocdOffset);
+        // We need to verify the integrity of the following three sections of the file:
+        // 1. Everything up to the start of the APK Signing Block.
+        // 2. ZIP Central Directory.
+        // 3. ZIP End of Central Directory (EoCD).
+        // Each of these sections is represented as a separate DataSource instance below.
+
+        // To handle large APKs, these sections are read in 1 MB chunks using memory-mapped I/O to
+        // avoid wasting physical memory. In most APK verification scenarios, the contents of the
+        // APK are already there in the OS's page cache and thus mmap does not use additional
+        // physical memory.
+        DataSource beforeApkSigningBlock =
+                new MemoryMappedFileDataSource(apkFileDescriptor, 0, apkSigningBlockOffset);
+        DataSource centralDir =
+                new MemoryMappedFileDataSource(
+                        apkFileDescriptor, centralDirOffset, eocdOffset - centralDirOffset);
+
         // For the purposes of integrity verification, ZIP End of Central Directory's field Start of
         // Central Directory must be considered to point to the offset of the APK Signing Block.
-        byte[] eocdBytes = new byte[apkContents.capacity() - eocdOffset];
-        apkContents.position(eocdOffset);
-        apkContents.get(eocdBytes);
-        ByteBuffer eocd = ByteBuffer.wrap(eocdBytes);
-        eocd.order(apkContents.order());
-        ZipUtils.setZipEocdCentralDirectoryOffset(eocd, apkSigningBlockOffset);
+        eocdBuf = eocdBuf.duplicate();
+        eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
+        ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, apkSigningBlockOffset);
+        DataSource eocd = new ByteBufferDataSource(eocdBuf);
 
         int[] digestAlgorithms = new int[expectedDigests.size()];
         int digestAlgorithmCount = 0;
@@ -427,30 +426,30 @@
             digestAlgorithms[digestAlgorithmCount] = digestAlgorithm;
             digestAlgorithmCount++;
         }
-        Map<Integer, byte[]> actualDigests;
+        byte[][] actualDigests;
         try {
             actualDigests =
                     computeContentDigests(
                             digestAlgorithms,
-                            new ByteBuffer[] {beforeApkSigningBlock, centralDir, eocd});
+                            new DataSource[] {beforeApkSigningBlock, centralDir, eocd});
         } catch (DigestException e) {
             throw new SecurityException("Failed to compute digest(s) of contents", e);
         }
-        for (Map.Entry<Integer, byte[]> entry : expectedDigests.entrySet()) {
-            int digestAlgorithm = entry.getKey();
-            byte[] expectedDigest = entry.getValue();
-            byte[] actualDigest = actualDigests.get(digestAlgorithm);
+        for (int i = 0; i < digestAlgorithms.length; i++) {
+            int digestAlgorithm = digestAlgorithms[i];
+            byte[] expectedDigest = expectedDigests.get(digestAlgorithm);
+            byte[] actualDigest = actualDigests[i];
             if (!MessageDigest.isEqual(expectedDigest, actualDigest)) {
                 throw new SecurityException(
                         getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm)
-                        + " digest of contents did not verify");
+                                + " digest of contents did not verify");
             }
         }
     }
 
-    private static Map<Integer, byte[]> computeContentDigests(
+    private static byte[][] computeContentDigests(
             int[] digestAlgorithms,
-            ByteBuffer[] contents) throws DigestException {
+            DataSource[] contents) throws DigestException {
         // For each digest algorithm the result is computed as follows:
         // 1. Each segment of contents is split into consecutive chunks of 1 MB in size.
         //    The final chunk will be shorter iff the length of segment is not a multiple of 1 MB.
@@ -461,13 +460,18 @@
         //    chunks (uint32 little-endian) and the concatenation of digests of chunks of all
         //    segments in-order.
 
-        int totalChunkCount = 0;
-        for (ByteBuffer input : contents) {
-            totalChunkCount += getChunkCount(input.remaining());
+        long totalChunkCountLong = 0;
+        for (DataSource input : contents) {
+            totalChunkCountLong += getChunkCount(input.size());
         }
+        if (totalChunkCountLong >= Integer.MAX_VALUE / 1024) {
+            throw new DigestException("Too many chunks: " + totalChunkCountLong);
+        }
+        int totalChunkCount = (int) totalChunkCountLong;
 
-        Map<Integer, byte[]> digestsOfChunks = new HashMap<>(totalChunkCount);
-        for (int digestAlgorithm : digestAlgorithms) {
+        byte[][] digestsOfChunks = new byte[digestAlgorithms.length][];
+        for (int i = 0; i < digestAlgorithms.length; i++) {
+            int digestAlgorithm = digestAlgorithms[i];
             int digestOutputSizeBytes = getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
             byte[] concatenationOfChunkCountAndChunkDigests =
                     new byte[5 + totalChunkCount * digestOutputSizeBytes];
@@ -476,49 +480,71 @@
                     totalChunkCount,
                     concatenationOfChunkCountAndChunkDigests,
                     1);
-            digestsOfChunks.put(digestAlgorithm, concatenationOfChunkCountAndChunkDigests);
+            digestsOfChunks[i] = concatenationOfChunkCountAndChunkDigests;
         }
 
         byte[] chunkContentPrefix = new byte[5];
         chunkContentPrefix[0] = (byte) 0xa5;
         int chunkIndex = 0;
-        for (ByteBuffer input : contents) {
-            while (input.hasRemaining()) {
-                int chunkSize = Math.min(input.remaining(), CHUNK_SIZE_BYTES);
-                ByteBuffer chunk = getByteBuffer(input, chunkSize);
-                for (int digestAlgorithm : digestAlgorithms) {
-                    String jcaAlgorithmName =
-                            getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm);
-                    MessageDigest md;
-                    try {
-                        md = MessageDigest.getInstance(jcaAlgorithmName);
-                    } catch (NoSuchAlgorithmException e) {
-                        throw new RuntimeException(jcaAlgorithmName + " digest not supported", e);
-                    }
-                    chunk.clear();
-                    setUnsignedInt32LittleEndian(chunk.remaining(), chunkContentPrefix, 1);
-                    md.update(chunkContentPrefix);
-                    md.update(chunk);
-                    byte[] concatenationOfChunkCountAndChunkDigests =
-                            digestsOfChunks.get(digestAlgorithm);
+        MessageDigest[] mds = new MessageDigest[digestAlgorithms.length];
+        for (int i = 0; i < digestAlgorithms.length; i++) {
+            String jcaAlgorithmName =
+                    getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithms[i]);
+            try {
+                mds[i] = MessageDigest.getInstance(jcaAlgorithmName);
+            } catch (NoSuchAlgorithmException e) {
+                throw new RuntimeException(jcaAlgorithmName + " digest not supported", e);
+            }
+        }
+        // TODO: Compute digests of chunks in parallel when beneficial. This requires some research
+        // into how to parallelize (if at all) based on the capabilities of the hardware on which
+        // this code is running and based on the size of input.
+        int dataSourceIndex = 0;
+        for (DataSource input : contents) {
+            long inputOffset = 0;
+            long inputRemaining = input.size();
+            while (inputRemaining > 0) {
+                int chunkSize = (int) Math.min(inputRemaining, CHUNK_SIZE_BYTES);
+                setUnsignedInt32LittleEndian(chunkSize, chunkContentPrefix, 1);
+                for (int i = 0; i < mds.length; i++) {
+                    mds[i].update(chunkContentPrefix);
+                }
+                try {
+                    input.feedIntoMessageDigests(mds, inputOffset, chunkSize);
+                } catch (IOException e) {
+                    throw new DigestException(
+                            "Failed to digest chunk #" + chunkIndex + " of section #"
+                                    + dataSourceIndex,
+                            e);
+                }
+                for (int i = 0; i < digestAlgorithms.length; i++) {
+                    int digestAlgorithm = digestAlgorithms[i];
+                    byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i];
                     int expectedDigestSizeBytes =
                             getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
-                    int actualDigestSizeBytes = md.digest(concatenationOfChunkCountAndChunkDigests,
-                            5 + chunkIndex * expectedDigestSizeBytes, expectedDigestSizeBytes);
+                    MessageDigest md = mds[i];
+                    int actualDigestSizeBytes =
+                            md.digest(
+                                    concatenationOfChunkCountAndChunkDigests,
+                                    5 + chunkIndex * expectedDigestSizeBytes,
+                                    expectedDigestSizeBytes);
                     if (actualDigestSizeBytes != expectedDigestSizeBytes) {
                         throw new RuntimeException(
                                 "Unexpected output size of " + md.getAlgorithm() + " digest: "
                                         + actualDigestSizeBytes);
                     }
                 }
+                inputOffset += chunkSize;
+                inputRemaining -= chunkSize;
                 chunkIndex++;
             }
+            dataSourceIndex++;
         }
 
-        Map<Integer, byte[]> result = new HashMap<>(digestAlgorithms.length);
-        for (Map.Entry<Integer, byte[]> entry : digestsOfChunks.entrySet()) {
-            int digestAlgorithm = entry.getKey();
-            byte[] input = entry.getValue();
+        byte[][] result = new byte[digestAlgorithms.length][];
+        for (int i = 0; i < digestAlgorithms.length; i++) {
+            int digestAlgorithm = digestAlgorithms[i];
+            byte[] input = digestsOfChunks[i];
             String jcaAlgorithmName = getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm);
             MessageDigest md;
             try {
@@ -527,49 +553,47 @@
                 throw new RuntimeException(jcaAlgorithmName + " digest not supported", e);
             }
             byte[] output = md.digest(input);
-            result.put(digestAlgorithm, output);
+            result[i] = output;
         }
         return result;
     }
 
     /**
-     * Finds the offset of ZIP End of Central Directory (EoCD).
+     * Returns the ZIP End of Central Directory (EoCD) and its offset in the file.
      *
-     * @throws SignatureNotFoundException If the EoCD could not be found
+     * @throws IOException if an I/O error occurs while reading the file.
+     * @throws SignatureNotFoundException if the EoCD could not be found.
      */
-    private static int getEocdOffset(ByteBuffer apkContents) throws SignatureNotFoundException {
-        int eocdOffset = ZipUtils.findZipEndOfCentralDirectoryRecord(apkContents);
-        if (eocdOffset == -1) {
+    private static Pair<ByteBuffer, Long> getEocd(RandomAccessFile apk)
+            throws IOException, SignatureNotFoundException {
+        Pair<ByteBuffer, Long> eocdAndOffsetInFile =
+                ZipUtils.findZipEndOfCentralDirectoryRecord(apk);
+        if (eocdAndOffsetInFile == null) {
             throw new SignatureNotFoundException(
                     "Not an APK file: ZIP End of Central Directory record not found");
         }
-        return eocdOffset;
+        return eocdAndOffsetInFile;
     }
 
-    private static long getCentralDirOffset(ByteBuffer apkContents, int eocdOffset)
+    private static long getCentralDirOffset(ByteBuffer eocd, long eocdOffset)
             throws SignatureNotFoundException {
-        if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(apkContents, eocdOffset)) {
-            throw new SignatureNotFoundException("ZIP64 APK not supported");
-        }
-        ByteBuffer eocd = sliceFromTo(apkContents, eocdOffset, apkContents.capacity());
-
         // Look up the offset of ZIP Central Directory.
-        long centralDirOffsetLong = ZipUtils.getZipEocdCentralDirectoryOffset(eocd);
-        if (centralDirOffsetLong >= eocdOffset) {
+        long centralDirOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocd);
+        if (centralDirOffset >= eocdOffset) {
             throw new SignatureNotFoundException(
-                    "ZIP Central Directory offset out of range: " + centralDirOffsetLong
+                    "ZIP Central Directory offset out of range: " + centralDirOffset
                     + ". ZIP End of Central Directory offset: " + eocdOffset);
         }
-        long centralDirSizeLong = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocd);
-        if (centralDirOffsetLong + centralDirSizeLong != eocdOffset) {
+        long centralDirSize = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocd);
+        if (centralDirOffset + centralDirSize != eocdOffset) {
             throw new SignatureNotFoundException(
                     "ZIP Central Directory is not immediately followed by End of Central"
                     + " Directory");
         }
-        return centralDirOffsetLong;
+        return centralDirOffset;
     }
 
-    private static final int getChunkCount(int inputSizeBytes) {
+    private static final long getChunkCount(long inputSizeBytes) {
         return (inputSizeBytes + CHUNK_SIZE_BYTES - 1) / CHUNK_SIZE_BYTES;
     }
 
@@ -837,10 +861,9 @@
 
     private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
 
-    private static int findApkSigningBlock(ByteBuffer apkContents, int centralDirOffset)
-            throws SignatureNotFoundException {
-        checkByteOrderLittleEndian(apkContents);
-
+    private static Pair<ByteBuffer, Long> findApkSigningBlock(
+            RandomAccessFile apk, long centralDirOffset)
+                    throws IOException, SignatureNotFoundException {
         // FORMAT:
         // OFFSET       DATA TYPE  DESCRIPTION
         // * @+0  bytes uint64:    size in bytes (excluding this field)
@@ -853,32 +876,42 @@
                     "APK too small for APK Signing Block. ZIP Central Directory offset: "
                             + centralDirOffset);
         }
-        // Check magic field present
-        if ((apkContents.getLong(centralDirOffset - 16) != APK_SIG_BLOCK_MAGIC_LO)
-                || (apkContents.getLong(centralDirOffset - 8) != APK_SIG_BLOCK_MAGIC_HI)) {
+        // Read the magic and offset in file from the footer section of the block:
+        // * uint64:   size of block
+        // * 16 bytes: magic
+        ByteBuffer footer = ByteBuffer.allocate(24);
+        footer.order(ByteOrder.LITTLE_ENDIAN);
+        apk.seek(centralDirOffset - footer.capacity());
+        apk.readFully(footer.array(), footer.arrayOffset(), footer.capacity());
+        if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO)
+                || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) {
             throw new SignatureNotFoundException(
                     "No APK Signing Block before ZIP Central Directory");
         }
         // Read and compare size fields
-        long apkSigBlockSizeLong = apkContents.getLong(centralDirOffset - 24);
-        if ((apkSigBlockSizeLong < 24) || (apkSigBlockSizeLong > Integer.MAX_VALUE - 8)) {
+        long apkSigBlockSizeInFooter = footer.getLong(0);
+        if ((apkSigBlockSizeInFooter < footer.capacity())
+                || (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) {
             throw new SignatureNotFoundException(
-                    "APK Signing Block size out of range: " + apkSigBlockSizeLong);
+                    "APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
         }
-        int apkSigBlockSizeFromFooter = (int) apkSigBlockSizeLong;
-        int totalSize = apkSigBlockSizeFromFooter + 8;
-        int apkSigBlockOffset = centralDirOffset - totalSize;
+        int totalSize = (int) (apkSigBlockSizeInFooter + 8);
+        long apkSigBlockOffset = centralDirOffset - totalSize;
         if (apkSigBlockOffset < 0) {
             throw new SignatureNotFoundException(
                     "APK Signing Block offset out of range: " + apkSigBlockOffset);
         }
-        long apkSigBlockSizeFromHeader = apkContents.getLong(apkSigBlockOffset);
-        if (apkSigBlockSizeFromHeader != apkSigBlockSizeFromFooter) {
+        ByteBuffer apkSigBlock = ByteBuffer.allocate(totalSize);
+        apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
+        apk.seek(apkSigBlockOffset);
+        apk.readFully(apkSigBlock.array(), apkSigBlock.arrayOffset(), apkSigBlock.capacity());
+        long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
+        if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) {
             throw new SignatureNotFoundException(
                     "APK Signing Block sizes in header and footer do not match: "
-                            + apkSigBlockSizeFromHeader + " vs " + apkSigBlockSizeFromFooter);
+                            + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter);
         }
-        return apkSigBlockOffset;
+        return Pair.create(apkSigBlock, apkSigBlockOffset);
     }
 
     private static ByteBuffer findApkSignatureSchemeV2Block(ByteBuffer apkSigningBlock)
@@ -930,6 +963,8 @@
     }
 
     public static class SignatureNotFoundException extends Exception {
+        private static final long serialVersionUID = 1L;
+
         public SignatureNotFoundException(String message) {
             super(message);
         }
@@ -940,6 +975,159 @@
     }
 
     /**
+     * Source of data to be digested.
+     */
+    private static interface DataSource {
+
+        /**
+         * Returns the size (in bytes) of the data offered by this source.
+         */
+        long size();
+
+        /**
+         * Feeds the specified region of this source's data into the provided digests. Each digest
+         * instance gets the same data.
+         *
+         * @param offset offset of the region inside this data source.
+         * @param size size (in bytes) of the region.
+         */
+        void feedIntoMessageDigests(MessageDigest[] mds, long offset, int size) throws IOException;
+    }
+
+    /**
+     * {@link DataSource} which provides data from a file descriptor by memory-mapping the sections
+     * of the file requested by
+     * {@link DataSource#feedIntoMessageDigests(MessageDigest[], long, int) feedIntoMessageDigests}.
+     */
+    private static final class MemoryMappedFileDataSource implements DataSource {
+        private static final Os OS = Libcore.os;
+        private static final long MEMORY_PAGE_SIZE_BYTES = OS.sysconf(OsConstants._SC_PAGESIZE);
+
+        private final FileDescriptor mFd;
+        private final long mFilePosition;
+        private final long mSize;
+
+        /**
+         * Constructs a new {@code MemoryMappedFileDataSource} for the specified region of the file.
+         *
+         * @param position start position of the region in the file.
+         * @param size size (in bytes) of the region.
+         */
+        public MemoryMappedFileDataSource(FileDescriptor fd, long position, long size) {
+            mFd = fd;
+            mFilePosition = position;
+            mSize = size;
+        }
+
+        @Override
+        public long size() {
+            return mSize;
+        }
+
+        @Override
+        public void feedIntoMessageDigests(
+                MessageDigest[] mds, long offset, int size) throws IOException {
+            // IMPLEMENTATION NOTE: After a lot of experimentation, the implementation of this
+            // method was settled on a straightforward mmap with prefaulting.
+            //
+            // This method is not using FileChannel.map API because that API does not offset a way
+            // to "prefault" the resulting memory pages. Without prefaulting, performance is about
+            // 10% slower on small to medium APKs, but is significantly worse for APKs in 500+ MB
+            // range. FileChannel.load (which currently uses madvise) doesn't help. Finally,
+            // invoking madvise (MADV_SEQUENTIAL) after mmap with prefaulting wastes quite a bit of
+            // time, which is not compensated for by faster reads.
+
+            // We mmap the smallest region of the file containing the requested data. mmap requires
+            // that the start offset in the file must be a multiple of memory page size. We thus may
+            // need to mmap from an offset less than the requested offset.
+            long filePosition = mFilePosition + offset;
+            long mmapFilePosition =
+                    (filePosition / MEMORY_PAGE_SIZE_BYTES) * MEMORY_PAGE_SIZE_BYTES;
+            int dataStartOffsetInMmapRegion = (int) (filePosition - mmapFilePosition);
+            long mmapRegionSize = size + dataStartOffsetInMmapRegion;
+            long mmapPtr = 0;
+            try {
+                mmapPtr = OS.mmap(
+                        0, // let the OS choose the start address of the region in memory
+                        mmapRegionSize,
+                        OsConstants.PROT_READ,
+                        OsConstants.MAP_SHARED | OsConstants.MAP_POPULATE, // "prefault" all pages
+                        mFd,
+                        mmapFilePosition);
+                // Feeding a memory region into MessageDigest requires the region to be represented
+                // as a direct ByteBuffer.
+                ByteBuffer buf = new DirectByteBuffer(
+                        size,
+                        mmapPtr + dataStartOffsetInMmapRegion,
+                        mFd,  // not really needed, but just in case
+                        null, // no need to clean up -- it's taken care of by the finally block
+                        true  // read only buffer
+                        );
+                for (MessageDigest md : mds) {
+                    buf.position(0);
+                    md.update(buf);
+                }
+            } catch (ErrnoException e) {
+                throw new IOException("Failed to mmap " + mmapRegionSize + " bytes", e);
+            } finally {
+                if (mmapPtr != 0) {
+                    try {
+                        OS.munmap(mmapPtr, mmapRegionSize);
+                    } catch (ErrnoException ignored) {}
+                }
+            }
+        }
+    }
+
+    /**
+     * {@link DataSource} which provides data from a {@link ByteBuffer}.
+     */
+    private static final class ByteBufferDataSource implements DataSource {
+        /**
+         * Underlying buffer. The data is stored between position 0 and the buffer's capacity.
+         * The buffer's position is 0 and limit is equal to capacity.
+         */
+        private final ByteBuffer mBuf;
+
+        public ByteBufferDataSource(ByteBuffer buf) {
+            // Defensive copy, to avoid changes to mBuf being visible in buf.
+            mBuf = buf.slice();
+        }
+
+        @Override
+        public long size() {
+            return mBuf.capacity();
+        }
+
+        @Override
+        public void feedIntoMessageDigests(
+                MessageDigest[] mds, long offset, int size) throws IOException {
+            // There's no way to tell MessageDigest to read data from ByteBuffer from a position
+            // other than the buffer's current position. We thus need to change the buffer's
+            // position to match the requested offset.
+            //
+            // In the future, it may be necessary to compute digests of multiple regions in
+            // parallel. Given that digest computation is a slow operation, we enable multiple
+            // such requests to be fulfilled by this instance. This is achieved by serially
+            // creating a new ByteBuffer corresponding to the requested data range and then,
+            // potentially concurrently, feeding these buffers into MessageDigest instances.
+            ByteBuffer region;
+            synchronized (mBuf) {
+                mBuf.position((int) offset);
+                mBuf.limit((int) offset + size);
+                region = mBuf.slice();
+            }
+
+            for (MessageDigest md : mds) {
+                // Need to reset position to 0 at the start of each iteration because
+                // MessageDigest.update below sets it to the buffer's limit.
+                region.position(0);
+                md.update(region);
+            }
+        }
+    }
+
+    /**
      * For legacy reasons we need to return exactly the original encoded certificate bytes, instead
      * of letting the underlying implementation have a shot at re-encoding the data.
      */
diff --git a/core/java/android/util/apk/ZipUtils.java b/core/java/android/util/apk/ZipUtils.java
index a383d5c..cdbac18 100644
--- a/core/java/android/util/apk/ZipUtils.java
+++ b/core/java/android/util/apk/ZipUtils.java
@@ -16,13 +16,17 @@
 
 package android.util.apk;
 
+import android.util.Pair;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 
 /**
  * Assorted ZIP format helpers.
  *
- * <p>NOTE: Most helper methods operating on {@code ByteBuffer} instances except that the byte
+ * <p>NOTE: Most helper methods operating on {@code ByteBuffer} instances expect that the byte
  * order of these buffers is little-endian.
  */
 abstract class ZipUtils {
@@ -35,9 +39,101 @@
     private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20;
 
     private static final int ZIP64_EOCD_LOCATOR_SIZE = 20;
-    private static final int ZIP64_EOCD_LOCATOR_SIG = 0x07064b50;
+    private static final int ZIP64_EOCD_LOCATOR_SIG_REVERSE_BYTE_ORDER = 0x504b0607;
 
-    private static final int UINT32_MAX_VALUE = 0xffff;
+    private static final int UINT16_MAX_VALUE = 0xffff;
+
+    /**
+     * Returns the ZIP End of Central Directory record of the provided ZIP file.
+     *
+     * @return contents of the ZIP End of Central Directory record and the record's offset in the
+     *         file or {@code null} if the file does not contain the record.
+     *
+     * @throws IOException if an I/O error occurs while reading the file.
+     */
+    static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(RandomAccessFile zip)
+            throws IOException {
+        // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
+        // The record can be identified by its 4-byte signature/magic which is located at the very
+        // beginning of the record. A complication is that the record is variable-length because of
+        // the comment field.
+        // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
+        // end of the buffer for the EOCD record signature. Whenever we find a signature, we check
+        // the candidate record's comment length is such that the remainder of the record takes up
+        // exactly the remaining bytes in the buffer. The search is bounded because the maximum
+        // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
+
+        long fileSize = zip.length();
+        if (fileSize < ZIP_EOCD_REC_MIN_SIZE) {
+            return null;
+        }
+
+        // Optimization: 99.99% of APKs have a zero-length comment field in the EoCD record and thus
+        // the EoCD record offset is known in advance. Try that offset first to avoid unnecessarily
+        // reading more data.
+        Pair<ByteBuffer, Long> result = findZipEndOfCentralDirectoryRecord(zip, 0);
+        if (result != null) {
+            return result;
+        }
+
+        // EoCD does not start where we expected it to. Perhaps it contains a non-empty comment
+        // field. Expand the search. The maximum size of the comment field in EoCD is 65535 because
+        // the comment length field is an unsigned 16-bit number.
+        return findZipEndOfCentralDirectoryRecord(zip, UINT16_MAX_VALUE);
+    }
+
+    /**
+     * Returns the ZIP End of Central Directory record of the provided ZIP file.
+     *
+     * @param maxCommentSize maximum accepted size (in bytes) of EoCD comment field. The permitted
+     *        value is from 0 to 65535 inclusive. The smaller the value, the faster this method
+     *        locates the record, provided its comment field is no longer than this value.
+     *
+     * @return contents of the ZIP End of Central Directory record and the record's offset in the
+     *         file or {@code null} if the file does not contain the record.
+     *
+     * @throws IOException if an I/O error occurs while reading the file.
+     */
+    private static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(
+            RandomAccessFile zip, int maxCommentSize) throws IOException {
+        // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
+        // The record can be identified by its 4-byte signature/magic which is located at the very
+        // beginning of the record. A complication is that the record is variable-length because of
+        // the comment field.
+        // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
+        // end of the buffer for the EOCD record signature. Whenever we find a signature, we check
+        // the candidate record's comment length is such that the remainder of the record takes up
+        // exactly the remaining bytes in the buffer. The search is bounded because the maximum
+        // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
+
+        if ((maxCommentSize < 0) || (maxCommentSize > UINT16_MAX_VALUE)) {
+            throw new IllegalArgumentException("maxCommentSize: " + maxCommentSize);
+        }
+
+        long fileSize = zip.length();
+        if (fileSize < ZIP_EOCD_REC_MIN_SIZE) {
+            // No space for EoCD record in the file.
+            return null;
+        }
+        // Lower maxCommentSize if the file is too small.
+        maxCommentSize = (int) Math.min(maxCommentSize, fileSize - ZIP_EOCD_REC_MIN_SIZE);
+
+        ByteBuffer buf = ByteBuffer.allocate(ZIP_EOCD_REC_MIN_SIZE + maxCommentSize);
+        buf.order(ByteOrder.LITTLE_ENDIAN);
+        long bufOffsetInFile = fileSize - buf.capacity();
+        zip.seek(bufOffsetInFile);
+        zip.readFully(buf.array(), buf.arrayOffset(), buf.capacity());
+        int eocdOffsetInBuf = findZipEndOfCentralDirectoryRecord(buf);
+        if (eocdOffsetInBuf == -1) {
+            // No EoCD record found in the buffer
+            return null;
+        }
+        // EoCD found
+        buf.position(eocdOffsetInBuf);
+        ByteBuffer eocd = buf.slice();
+        eocd.order(ByteOrder.LITTLE_ENDIAN);
+        return Pair.create(eocd, bufOffsetInFile + eocdOffsetInBuf);
+    }
 
     /**
      * Returns the position at which ZIP End of Central Directory record starts in the provided
@@ -45,7 +141,7 @@
      *
      * <p>NOTE: Byte order of {@code zipContents} must be little-endian.
      */
-    public static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) {
+    private static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) {
         assertByteOrderLittleEndian(zipContents);
 
         // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
@@ -56,14 +152,13 @@
         // end of the buffer for the EOCD record signature. Whenever we find a signature, we check
         // the candidate record's comment length is such that the remainder of the record takes up
         // exactly the remaining bytes in the buffer. The search is bounded because the maximum
-        // size of the comment field is 65535 bytes because the field is an unsigned 32-bit number.
+        // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
 
         int archiveSize = zipContents.capacity();
         if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) {
-            System.out.println("File size smaller than EOCD min size");
             return -1;
         }
-        int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT32_MAX_VALUE);
+        int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE);
         int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE;
         for (int expectedCommentLength = 0; expectedCommentLength < maxCommentLength;
                 expectedCommentLength++) {
@@ -82,24 +177,28 @@
     }
 
     /**
-     * Returns {@code true} if the provided buffer contains a ZIP64 End of Central Directory
+     * Returns {@code true} if the provided file contains a ZIP64 End of Central Directory
      * Locator.
      *
-     * <p>NOTE: Byte order of {@code zipContents} must be little-endian.
+     * @param zipEndOfCentralDirectoryPosition offset of the ZIP End of Central Directory record
+     *        in the file.
+     *
+     * @throws IOException if an I/O error occurs while reading the file.
      */
     public static final boolean isZip64EndOfCentralDirectoryLocatorPresent(
-            ByteBuffer zipContents, int zipEndOfCentralDirectoryPosition) {
-        assertByteOrderLittleEndian(zipContents);
+            RandomAccessFile zip, long zipEndOfCentralDirectoryPosition) throws IOException {
 
         // ZIP64 End of Central Directory Locator immediately precedes the ZIP End of Central
         // Directory Record.
-
-        int locatorPosition = zipEndOfCentralDirectoryPosition - ZIP64_EOCD_LOCATOR_SIZE;
+        long locatorPosition = zipEndOfCentralDirectoryPosition - ZIP64_EOCD_LOCATOR_SIZE;
         if (locatorPosition < 0) {
             return false;
         }
 
-        return zipContents.getInt(locatorPosition) == ZIP64_EOCD_LOCATOR_SIG;
+        zip.seek(locatorPosition);
+        // RandomAccessFile.readInt assumes big-endian byte order, but ZIP format uses
+        // little-endian.
+        return zip.readInt() == ZIP64_EOCD_LOCATOR_SIG_REVERSE_BYTE_ORDER;
     }
 
     /**
diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl
index 6a2cc80..a1e2e94 100644
--- a/core/java/android/view/IWindowSession.aidl
+++ b/core/java/android/view/IWindowSession.aidl
@@ -112,15 +112,18 @@
      *
      *  @param window The window being modified. Must be attached to a parent window
      *  or this call will fail.
-     *  @param x The new x position
-     *  @param y The new y position
-     *  @param width The new width
-     *  @param height The new height
+     *  @param left The new left position
+     *  @param top The new top position
+     *  @param right The new right position
+     *  @param bottom The new bottom position
+     *  @param requestedWidth The new requested width
+     *  @param requestedHeight The new requested height
      *  @param deferTransactionUntilFrame Frame number from our parent (attached) to
      *  defer this action until.
      *  @param outFrame Rect in which is placed the new position/size on screen.
      */
     void repositionChild(IWindow childWindow, int left, int top, int right, int bottom,
+            int requestedWidth, int requestedHeight,
             long deferTransactionUntilFrame, out Rect outFrame);
 
     /*
diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java
index 2c9d691..477ffd9 100644
--- a/core/java/android/view/SurfaceView.java
+++ b/core/java/android/view/SurfaceView.java
@@ -665,7 +665,9 @@
                             "postion = [%d, %d, %d, %d]", mWindowSpaceLeft, mWindowSpaceTop,
                             mLocation[0], mLocation[1]));
                     mSession.repositionChild(mWindow, mWindowSpaceLeft, mWindowSpaceTop,
-                            mLocation[0], mLocation[1], -1, mWinFrame);
+                            mLocation[0], mLocation[1],
+                            mWindowSpaceWidth, mWindowSpaceHeight,
+                            -1, mWinFrame);
                 } catch (RemoteException ex) {
                     Log.e(TAG, "Exception from relayout", ex);
                 }
@@ -700,7 +702,9 @@
                         right, bottom));
             }
             // Just using mRTLastReportedPosition as a dummy rect here
-            session.repositionChild(window, left, top, right, bottom, frameNumber,
+            session.repositionChild(window, left, top, right, bottom,
+                    mWindowSpaceWidth, mWindowSpaceHeight,
+                    frameNumber,
                     mRTLastReportedPosition);
             // Now overwrite mRTLastReportedPosition with our values
             mRTLastReportedPosition.set(left, top, right, bottom);
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 3586484..6d35a58 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -18002,7 +18002,13 @@
          * to clear the previous drawable. setVisible first while we still have the callback set.
          */
         if (mBackground != null) {
-            if (isAttachedToWindow()) {
+            // It's possible for this method to be invoked from the View constructor before
+            // subclass constructors have run. Drawables can and should trigger invalidations
+            // and other activity with their callback on visibility changes, which shouldn't
+            // happen before subclass constructors finish. However, we won't have set the
+            // drawable as visible until the view becomes attached. This guard below keeps
+            // multiple calls to this method from constructors from causing issues.
+            if (mBackground.isVisible()) {
                 mBackground.setVisible(false, false);
             }
             mBackground.setCallback(null);
@@ -18237,7 +18243,13 @@
         }
 
         if (mForegroundInfo.mDrawable != null) {
-            if (isAttachedToWindow()) {
+            // It's possible for this method to be invoked from the View constructor before
+            // subclass constructors have run. Drawables can and should trigger invalidations
+            // and other activity with their callback on visibility changes, which shouldn't
+            // happen before subclass constructors finish. However, we won't have set the
+            // drawable as visible until the view becomes attached. This guard below keeps
+            // multiple calls to this method from constructors from causing issues.
+            if (mForegroundInfo.mDrawable.isVisible()) {
                 mForegroundInfo.mDrawable.setVisible(false, false);
             }
             mForegroundInfo.mDrawable.setCallback(null);
diff --git a/core/java/android/widget/ImageView.java b/core/java/android/widget/ImageView.java
index 222a040..0206577 100644
--- a/core/java/android/widget/ImageView.java
+++ b/core/java/android/widget/ImageView.java
@@ -911,11 +911,17 @@
         }
 
         if (mDrawable != null) {
-            mDrawable.setCallback(null);
-            unscheduleDrawable(mDrawable);
-            if (isAttachedToWindow()) {
+            // It's possible for this method to be invoked from the constructor before
+            // subclass constructors have run. Drawables can and should trigger invalidations
+            // and other activity with their callback on visibility changes, which shouldn't
+            // happen before subclass constructors finish. However, we won't have set the
+            // drawable as visible until the view becomes attached. This guard below keeps
+            // multiple calls to this method from constructors from causing issues.
+            if (mDrawable.isVisible()) {
                 mDrawable.setVisible(false, false);
             }
+            mDrawable.setCallback(null);
+            unscheduleDrawable(mDrawable);
         }
 
         mDrawable = d;
diff --git a/core/java/android/widget/PopupWindow.java b/core/java/android/widget/PopupWindow.java
index a1417f0..d1b5fc8 100644
--- a/core/java/android/widget/PopupWindow.java
+++ b/core/java/android/widget/PopupWindow.java
@@ -2027,21 +2027,24 @@
             mAnchorYoff = yoff;
         }
 
+        final WindowManager.LayoutParams p =
+                (WindowManager.LayoutParams) mDecorView.getLayoutParams();
+
         if (updateDimension) {
             if (width == -1) {
                 width = mPopupWidth;
             } else {
                 mPopupWidth = width;
+                p.width = width;
             }
             if (height == -1) {
                 height = mPopupHeight;
             } else {
                 mPopupHeight = height;
+                p.height  = height;
             }
         }
 
-        final WindowManager.LayoutParams p =
-                (WindowManager.LayoutParams) mDecorView.getLayoutParams();
         final int x = p.x;
         final int y = p.y;
         if (updateLocation) {
diff --git a/core/tests/coretests/src/android/widget/TextViewActivityMouseTest.java b/core/tests/coretests/src/android/widget/TextViewActivityMouseTest.java
index 923b829..3fbc16a 100644
--- a/core/tests/coretests/src/android/widget/TextViewActivityMouseTest.java
+++ b/core/tests/coretests/src/android/widget/TextViewActivityMouseTest.java
@@ -44,6 +44,7 @@
 import android.support.test.espresso.Espresso;
 import android.test.ActivityInstrumentationTestCase2;
 import android.test.suitebuilder.annotation.SmallTest;
+import android.test.suitebuilder.annotation.Suppress;
 import android.view.MotionEvent;
 import android.widget.espresso.ContextMenuUtils;
 
@@ -97,6 +98,7 @@
     }
 
     @SmallTest
+    @Suppress
     public void testContextMenu() throws Exception {
         final String text = "abc def ghi.";
         onView(withId(R.id.textview)).perform(click());
diff --git a/docs/html/guide/topics/data/data-storage.jd b/docs/html/guide/topics/data/data-storage.jd
index 0d3bb4d..46db371 100644
--- a/docs/html/guide/topics/data/data-storage.jd
+++ b/docs/html/guide/topics/data/data-storage.jd
@@ -178,19 +178,6 @@
 android.content.Context#MODE_WORLD_READABLE}, and {@link
 android.content.Context#MODE_WORLD_WRITEABLE}.</p>
 
-<p class="note"><strong>Note:</strong> The constants {@link
-android.content.Context#MODE_WORLD_READABLE} and {@link
-android.content.Context#MODE_WORLD_WRITEABLE} have been deprecated since API level 17. Since
-API level 24 their use will result in a {@link java.lang.SecurityException} to be thrown.
-This means that apps targeting API level 24 and higher
-cannot share private files by name, and attempts to share a "file://" URI will result in a
-{@link android.os.FileUriExposedException} to be thrown. If your app needs to share private
-files with other apps, it may use a {@link android.support.v4.content.FileProvider} with
-the {@link android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION}.
-See also <a
-href="{@docRoot}training/secure-file-sharing/index.html">Sharing Files</a>.
-</p>
-
 <p>To read a file from internal storage:</p>
 
 <ol>
diff --git a/docs/html/training/basics/data-storage/files.jd b/docs/html/training/basics/data-storage/files.jd
index 983d59a..49a9169 100644
--- a/docs/html/training/basics/data-storage/files.jd
+++ b/docs/html/training/basics/data-storage/files.jd
@@ -59,7 +59,7 @@
 <p><b>Internal storage:</b></p>
 <ul>
 <li>It's always available.</li>
-<li>Files saved here are accessible by only your app.</li>
+<li>Files saved here are accessible by only your app by default.</li>
 <li>When the user uninstalls your app, the system removes all your app's files from
 internal storage.</li>
 </ul>
@@ -83,12 +83,6 @@
 with other apps or allow the user to access with a computer.</p>
 </div>
 
-<p class="note">
-<strong>Note:</strong> Before API level 24, internal files could be made accessible to other
-apps by means of relaxing file system permissions. This is no longer the case. If you wish
-to make the content of a private file accessible to other apps, your app may use the
-{@link android.support.v4.content.FileProvider}. See <a
-href="{@docRoot}training/secure-file-sharing/index.html">Sharing Files</a>.</p>
 
 <p class="note" style="clear:both">
 <strong>Tip:</strong> Although apps are installed onto the internal storage by
diff --git a/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java b/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java
index ae9ebc7..46a0f43 100644
--- a/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java
+++ b/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java
@@ -804,7 +804,7 @@
     }
 
     private interface VectorDrawableAnimator {
-        void init(AnimatorSet set);
+        void init(@NonNull AnimatorSet set);
         void start();
         void end();
         void reset();
@@ -818,21 +818,44 @@
     }
 
     private static class VectorDrawableAnimatorUI implements VectorDrawableAnimator {
-        private AnimatorSet mSet = new AnimatorSet();
+        // mSet is only initialized in init(). So we need to check whether it is null before any
+        // operation.
+        private AnimatorSet mSet = null;
         private final Drawable mDrawable;
+        // Caching the listener in the case when listener operation is called before the mSet is
+        // setup by init().
+        private ArrayList<AnimatorListener> mListenerArray = null;
 
-        VectorDrawableAnimatorUI(AnimatedVectorDrawable drawable) {
+        VectorDrawableAnimatorUI(@NonNull AnimatedVectorDrawable drawable) {
             mDrawable = drawable;
         }
 
         @Override
-        public void init(AnimatorSet set) {
-            mSet = set;
+        public void init(@NonNull AnimatorSet set) {
+            if (mSet != null) {
+                // Already initialized
+                throw new UnsupportedOperationException("VectorDrawableAnimator cannot be " +
+                        "re-initialized");
+            }
+            // Keep a deep copy of the set, such that set can be still be constantly representing
+            // the static content from XML file.
+            mSet = set.clone();
+
+            // If there are listeners added before calling init(), now they should be setup.
+            if (mListenerArray != null && !mListenerArray.isEmpty()) {
+                for (int i = 0; i < mListenerArray.size(); i++) {
+                    mSet.addListener(mListenerArray.get(i));
+                }
+                mListenerArray.clear();
+                mListenerArray = null;
+            }
         }
 
+        // Although start(), reset() and reverse() should call init() already, it is better to
+        // protect these functions from NPE in any situation.
         @Override
         public void start() {
-            if (mSet.isStarted()) {
+            if (mSet == null || mSet.isStarted()) {
                 return;
             }
             mSet.start();
@@ -841,51 +864,74 @@
 
         @Override
         public void end() {
+            if (mSet == null) {
+                return;
+            }
             mSet.end();
         }
 
         @Override
         public void reset() {
+            if (mSet == null) {
+                return;
+            }
             start();
             mSet.cancel();
         }
 
         @Override
         public void reverse() {
+            if (mSet == null) {
+                return;
+            }
             mSet.reverse();
             invalidateOwningView();
         }
 
         @Override
         public boolean canReverse() {
-            return mSet.canReverse();
+            return mSet != null && mSet.canReverse();
         }
 
         @Override
         public void setListener(AnimatorListener listener) {
-            mSet.addListener(listener);
+            if (mSet == null) {
+                if (mListenerArray == null) {
+                    mListenerArray = new ArrayList<AnimatorListener>();
+                }
+                mListenerArray.add(listener);
+            } else {
+                mSet.addListener(listener);
+            }
         }
 
         @Override
         public void removeListener(AnimatorListener listener) {
-            mSet.removeListener(listener);
+            if (mSet == null) {
+                if (mListenerArray == null) {
+                    return;
+                }
+                mListenerArray.remove(listener);
+            } else {
+                mSet.removeListener(listener);
+            }
         }
 
         @Override
         public void onDraw(Canvas canvas) {
-            if (mSet.isStarted()) {
+            if (mSet != null && mSet.isStarted()) {
                 invalidateOwningView();
             }
         }
 
         @Override
         public boolean isStarted() {
-            return mSet.isStarted();
+            return mSet != null && mSet.isStarted();
         }
 
         @Override
         public boolean isRunning() {
-            return mSet.isRunning();
+            return mSet != null && mSet.isRunning();
         }
 
         private void invalidateOwningView() {
@@ -928,7 +974,7 @@
         }
 
         @Override
-        public void init(AnimatorSet set) {
+        public void init(@NonNull AnimatorSet set) {
             if (mInitialized) {
                 // Already initialized
                 throw new UnsupportedOperationException("VectorDrawableAnimator cannot be " +
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
index a94b40c..8a96b97 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
@@ -188,7 +188,7 @@
 
     @Override
     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
-        Log.d(TAG, "onActivityResult() code=" + resultCode);
+        if (DEBUG) Log.d(TAG, "onActivityResult() code=" + resultCode);
 
         // Only relay back results when not canceled; otherwise stick around to
         // let the user pick another app/backend.
@@ -412,7 +412,7 @@
 
     @Override
     void onTaskFinished(Uri... uris) {
-        Log.d(TAG, "onFinished() " + Arrays.toString(uris));
+        if (DEBUG) Log.d(TAG, "onFinished() " + Arrays.toString(uris));
 
         final Intent intent = new Intent();
         if (uris.length == 1) {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java b/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
index d3888e8..2af6c46 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
@@ -412,7 +412,7 @@
 
     @Override
     void onTaskFinished(Uri... uris) {
-        Log.d(TAG, "onFinished() " + Arrays.toString(uris));
+        if (DEBUG) Log.d(TAG, "onFinished() " + Arrays.toString(uris));
 
         final Intent intent = new Intent();
         if (uris.length == 1) {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java b/packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java
index 2b6f396..ab45af1 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java
@@ -282,7 +282,7 @@
             logInvalidScopedAccessRequest(context, SCOPED_DIRECTORY_ACCESS_ERROR);
             return null;
         }
-        Log.d(TAG, "doc id for " + file + ": " + docId);
+        if (DEBUG) Log.d(TAG, "doc id for " + file + ": " + docId);
 
         final Uri uri = DocumentsContract.buildTreeDocumentUri(EXTERNAL_STORAGE_AUTH, docId);
         if (uri == null) {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RecentsProvider.java b/packages/DocumentsUI/src/com/android/documentsui/RecentsProvider.java
index e1b1c09..6ef9154 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RecentsProvider.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RecentsProvider.java
@@ -16,6 +16,7 @@
 
 package com.android.documentsui;
 
+import static com.android.documentsui.Shared.DEBUG;
 import static com.android.documentsui.model.DocumentInfo.getCursorString;
 
 import android.content.ContentProvider;
@@ -338,7 +339,7 @@
                 if (predicate.apply(authority)) {
                     db.delete(TABLE_STATE, StateColumns.AUTHORITY + "=?", new String[] {
                             authority });
-                    Log.d(TAG, "Purged state for " + authority);
+                    if (DEBUG) Log.d(TAG, "Purged state for " + authority);
                 }
             }
         } finally {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/SearchViewManager.java b/packages/DocumentsUI/src/com/android/documentsui/SearchViewManager.java
index 63dc2ee..4d0ba4b 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/SearchViewManager.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/SearchViewManager.java
@@ -16,6 +16,8 @@
 
 package com.android.documentsui;
 
+import static com.android.documentsui.Shared.DEBUG;
+
 import android.annotation.Nullable;
 import android.os.Bundle;
 import android.provider.DocumentsContract.Root;
@@ -80,7 +82,7 @@
      */
     void update(RootInfo root) {
         if (mMenu == null) {
-            Log.d(TAG, "update called before Search MenuItem installed.");
+            if (DEBUG) Log.d(TAG, "update called before Search MenuItem installed.");
             return;
         }
 
@@ -108,7 +110,7 @@
 
     void showMenu(boolean visible) {
         if (mMenu == null) {
-            Log.d(TAG, "showMenu called before Search MenuItem installed.");
+            if (DEBUG) Log.d(TAG, "showMenu called before Search MenuItem installed.");
             return;
         }
 
diff --git a/packages/DocumentsUI/src/com/android/documentsui/Shared.java b/packages/DocumentsUI/src/com/android/documentsui/Shared.java
index e0d36d6..d21afee 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/Shared.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/Shared.java
@@ -33,7 +33,7 @@
 
     public static final String TAG = "Documents";
 
-    public static final boolean DEBUG = true;
+    public static final boolean DEBUG = false;
 
     /** Intent action name to pick a copy destination. */
     public static final String ACTION_PICK_COPY_DESTINATION =
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 073cf14..589eac6 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -156,6 +156,9 @@
     <!-- TV picture-in-picture -->
     <uses-permission android:name="android.permission.RECEIVE_MEDIA_RESOURCE_USAGE" />
 
+    <!-- DND access -->
+    <uses-permission android:name="android.permission.MANAGE_NOTIFICATIONS" />
+
     <application
         android:name=".SystemUIApplication"
         android:persistent="true"
diff --git a/packages/SystemUI/res/drawable-hdpi/ic_cancel_white_24dp.png b/packages/SystemUI/res/drawable-hdpi/ic_cancel_white_24dp.png
new file mode 100644
index 0000000..73f5116
--- /dev/null
+++ b/packages/SystemUI/res/drawable-hdpi/ic_cancel_white_24dp.png
Binary files differ
diff --git a/packages/SystemUI/res/drawable-mdpi/ic_cancel_white_24dp.png b/packages/SystemUI/res/drawable-mdpi/ic_cancel_white_24dp.png
new file mode 100644
index 0000000..787e259
--- /dev/null
+++ b/packages/SystemUI/res/drawable-mdpi/ic_cancel_white_24dp.png
Binary files differ
diff --git a/packages/SystemUI/res/drawable-xhdpi/ic_cancel_white_24dp.png b/packages/SystemUI/res/drawable-xhdpi/ic_cancel_white_24dp.png
new file mode 100644
index 0000000..6ebbc83
--- /dev/null
+++ b/packages/SystemUI/res/drawable-xhdpi/ic_cancel_white_24dp.png
Binary files differ
diff --git a/packages/SystemUI/res/layout/hybrid_notification.xml b/packages/SystemUI/res/layout/hybrid_notification.xml
index f667859..476f52b 100644
--- a/packages/SystemUI/res/layout/hybrid_notification.xml
+++ b/packages/SystemUI/res/layout/hybrid_notification.xml
@@ -21,7 +21,7 @@
     android:layout_height="wrap_content"
     android:paddingStart="@*android:dimen/notification_content_margin_start"
     android:paddingEnd="12dp"
-    android:gravity="bottom">
+    android:gravity="bottom|start">
     <TextView
         android:id="@+id/notification_title"
         android:layout_width="wrap_content"
diff --git a/packages/SystemUI/res/layout/hybrid_overflow_number.xml b/packages/SystemUI/res/layout/hybrid_overflow_number.xml
new file mode 100644
index 0000000..f3dde8d
--- /dev/null
+++ b/packages/SystemUI/res/layout/hybrid_overflow_number.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2016 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
+  -->
+<TextView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/notification_text"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:textAppearance="@*android:style/TextAppearance.Material.Notification"
+    android:paddingEnd="@*android:dimen/notification_content_margin_end"
+    android:gravity="end"
+    android:singleLine="true"
+    />
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/notification_guts.xml b/packages/SystemUI/res/layout/notification_guts.xml
index 1ab6bf9..062ae35 100644
--- a/packages/SystemUI/res/layout/notification_guts.xml
+++ b/packages/SystemUI/res/layout/notification_guts.xml
@@ -31,12 +31,12 @@
     <!-- header -->
     <LinearLayout
             android:layout_width="match_parent"
-            android:layout_height="30dp"
-            android:paddingTop="9dp"
+            android:layout_height="wrap_content"
+            android:paddingTop="14dp"
             android:paddingEnd="8dp"
             android:id="@+id/notification_guts_header"
             android:orientation="horizontal"
-            android:layout_gravity="center_vertical|start">
+            android:layout_gravity="start">
 
         <ImageView
                 android:id="@+id/app_icon"
@@ -64,7 +64,6 @@
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:paddingTop="4dp"
-            android:paddingBottom="16dip"
             android:paddingEnd="8dp" >
         <RadioButton
                 android:id="@+id/silent_importance"
@@ -99,7 +98,6 @@
             android:orientation="vertical"
             android:clickable="false"
             android:focusable="false"
-            android:paddingBottom="8dip"
             android:paddingEnd="8dp"
             android:visibility="gone">
         <TextView
@@ -166,13 +164,14 @@
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:gravity="end"
+            android:paddingTop="16dp"
             android:paddingBottom="8dp" >
 
         <TextView
             android:id="@+id/more_settings"
             android:text="@string/notification_more_settings"
             android:layout_width="wrap_content"
-            android:layout_height="48dp"
+            android:layout_height="36dp"
             style="@style/TextAppearance.NotificationGuts.Button"
             android:background="@drawable/btn_borderless_rect"
             android:gravity="center"
@@ -184,7 +183,7 @@
             android:id="@+id/done"
             android:text="@string/notification_done"
             android:layout_width="wrap_content"
-            android:layout_height="48dp"
+            android:layout_height="36dp"
             style="@style/TextAppearance.NotificationGuts.Button"
             android:background="@drawable/btn_borderless_rect"
             android:gravity="center"
diff --git a/packages/SystemUI/res/layout/recents_on_tv.xml b/packages/SystemUI/res/layout/recents_on_tv.xml
index 9764074..f0bfebe 100644
--- a/packages/SystemUI/res/layout/recents_on_tv.xml
+++ b/packages/SystemUI/res/layout/recents_on_tv.xml
@@ -28,12 +28,8 @@
         android:clipChildren="false"
         android:clipToPadding="false"
         android:descendantFocusability="beforeDescendants"
-        android:layout_gravity="center"
-        android:gravity="center"
-        android:paddingStart="@dimen/recents_tv_grid_row_padding"
-        android:paddingEnd="@dimen/recents_tv_grid_row_padding"
+        android:layout_marginTop="@dimen/recents_tv_gird_row_top_margin"
         android:focusable="true" />
-
     <View
         android:id="@+id/pip_shade"
         android:layout_width="match_parent"
diff --git a/packages/SystemUI/res/layout/recents_tv_task_card_view.xml b/packages/SystemUI/res/layout/recents_tv_task_card_view.xml
index 54e97da..766ef60 100644
--- a/packages/SystemUI/res/layout/recents_tv_task_card_view.xml
+++ b/packages/SystemUI/res/layout/recents_tv_task_card_view.xml
@@ -21,9 +21,11 @@
     android:focusableInTouchMode="true"
     android:layout_gravity="center"
     android:layout_centerInParent="true"
+    android:orientation="vertical"
     android:layoutDirection="ltr">
 
     <LinearLayout
+            android:id="@+id/recents_tv_card"
             android:layout_width="@dimen/recents_tv_card_width"
             android:layout_height="wrap_content"
             android:layout_centerInParent="true"
@@ -66,4 +68,30 @@
                 android:layout_centerHorizontal="true"
                 android:layout_below="@id/card_title_text" />
     </LinearLayout>
+    <LinearLayout
+            android:id="@+id/card_dismiss"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            android:layout_gravity="center_horizontal"
+            android:layout_below="@id/recents_tv_card"
+            android:alpha="0.0">
+        <ImageView
+                android:id="@+id/card_dismiss_icon"
+                android:layout_width="@dimen/recents_tv_dismiss_icon_size"
+                android:layout_height="@dimen/recents_tv_dismiss_icon_size"
+                android:layout_gravity="center_horizontal"
+                android:layout_marginTop="@dimen/recents_tv_dismiss_icon_top_margin"
+                android:layout_marginBottom="@dimen/recents_tv_dismiss_icon_bottom_margin"
+                android:src="@drawable/ic_cancel_white_24dp" />
+        <TextView
+                android:id="@+id/card_dismiss_text"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textSize="@dimen/recents_tv_dismiss_text_size"
+                android:fontFamily="@string/font_roboto_light"
+                android:textColor="@color/recents_tv_dismiss_text_color"
+                android:text="@string/recents_tv_dismiss"
+                android:layout_gravity="center_horizontal" />
+    </LinearLayout>
 </com.android.systemui.recents.tv.views.TaskCardView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/colors_tv.xml b/packages/SystemUI/res/values/colors_tv.xml
index af99aae..4126d3c 100644
--- a/packages/SystemUI/res/values/colors_tv.xml
+++ b/packages/SystemUI/res/values/colors_tv.xml
@@ -19,4 +19,5 @@
 <resources>
     <color name="recents_tv_card_background_color">#FF37474F</color>
     <color name="recents_tv_card_title_text_color">#CCEEEEEE</color>
+    <color name="recents_tv_dismiss_text_color">#7FEEEEEE</color>
 </resources>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens_tv.xml b/packages/SystemUI/res/values/dimens_tv.xml
index 6b153d1..953dd65 100644
--- a/packages/SystemUI/res/values/dimens_tv.xml
+++ b/packages/SystemUI/res/values/dimens_tv.xml
@@ -18,8 +18,8 @@
 -->
 <resources>
     <!-- Dimens for recents card in the recents view on tv -->
-    <dimen name="recents_tv_card_width">268dip</dimen>
-    <dimen name="recents_tv_screenshot_height">151dip</dimen>
+    <dimen name="recents_tv_card_width">240dip</dimen>
+    <dimen name="recents_tv_screenshot_height">135dip</dimen>
     <dimen name="recents_tv_card_extra_badge_size">20dip</dimen>
     <dimen name="recents_tv_banner_width">114dip</dimen>
     <dimen name="recents_tv_banner_height">64dip</dimen>
@@ -29,10 +29,10 @@
     <dimen name="recents_tv_text_padding_bottom">12dip</dimen>
 
     <!-- Padding for grid view in recents view on tv -->
-    <dimen name="recents_tv_grid_row_padding">56dip</dimen>
-    <dimen name="recents_tv_gird_row_top_padding">57dip</dimen>
+    <dimen name="recents_tv_gird_row_top_margin">215dip</dimen>
     <dimen name="recents_tv_grid_max_row_height">268dip</dimen>
-    <dimen name="recents_tv_gird_card_spacing">20dip</dimen>
+    <dimen name="recents_tv_gird_card_spacing">8dip</dimen>
+    <dimen name="recents_tv_gird_focused_card_delta">44dip</dimen>
 
     <!-- Values for focus animation -->
     <dimen name="recents_tv_unselected_item_z">6dp</dimen>
@@ -43,4 +43,13 @@
 
     <!-- Values for text on recents cards on tv -->
     <dimen name="recents_tv_title_text_size">12sp</dimen>
+
+    <!-- Values for card dismiss state -->
+    <dimen name="recents_tv_dismiss_shift_down">48dip</dimen>
+    <dimen name="recents_tv_dismiss_top_margin">356dip</dimen>
+    <dimen name="recents_tv_dismiss_icon_size">24dip</dimen>
+    <dimen name="recents_tv_dismiss_icon_top_margin">38dip</dimen>
+    <dimen name="recents_tv_dismiss_icon_bottom_margin">1dip</dimen>
+    <dimen name="recents_tv_dismiss_text_size">12sp</dimen>
+
 </resources>
diff --git a/packages/SystemUI/res/values/integers_tv.xml b/packages/SystemUI/res/values/integers_tv.xml
index bfd8f8b..c60c245 100644
--- a/packages/SystemUI/res/values/integers_tv.xml
+++ b/packages/SystemUI/res/values/integers_tv.xml
@@ -15,4 +15,6 @@
 -->
 <resources>
     <integer name="item_scale_anim_duration">150</integer>
+    <integer name="dismiss_short_duration">200</integer>
+    <integer name="dismiss_long_duration">400</integer>
 </resources>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 5295ccb..2bde141 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -570,6 +570,9 @@
     <!-- Content description of the clear button in the notification panel for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
     <string name="accessibility_clear_all">Clear all notifications.</string>
 
+    <!-- The overflow indicator shown when a group has more notification inside the group than the visible ones. An example is "+ 3" [CHAR LIMIT=5] -->
+    <string name="notification_group_overflow_indicator">+ <xliff:g id="number" example="3">%s</xliff:g></string>
+
     <!-- Content description of button in notification inspector for system settings relating to
          notifications from this application [CHAR LIMIT=NONE] -->
     <string name="status_bar_notification_inspect_item_title">Notification settings</string>
diff --git a/packages/SystemUI/res/values/strings_tv.xml b/packages/SystemUI/res/values/strings_tv.xml
index 2957914..52aba0d 100644
--- a/packages/SystemUI/res/values/strings_tv.xml
+++ b/packages/SystemUI/res/values/strings_tv.xml
@@ -35,7 +35,11 @@
     <string name="pip_onboarding_description">Press and hold the HOME button to control PIP</string>
     <!-- Button to close picture-in-picture (PIP) onboarding screen. -->
     <string name="pip_onboarding_button">Got it</string>
+    <!-- Dismiss icon description -->
+    <string name="recents_tv_dismiss">Dismiss</string>
     <!-- Font for Recents -->
     <!-- DO NOT TRANSLATE -->
     <string name="font_roboto_regular" translatable="false">sans-serif</string>
+    <!-- DO NOT TRANSLATE -->
+    <string name="font_roboto_light" translatable="false">sans-serif-light</string>
 </resources>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index b0c1e95..21ad216 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -305,7 +305,7 @@
 
     <style name="TextAppearance.NotificationGuts">
         <item name="android:textSize">14sp</item>
-        <item name="android:fontFamily">sans-serif-medium</item>
+        <item name="android:fontFamily">roboto-regular</item>
         <item name="android:textColor">@android:color/black</item>
     </style>
 
diff --git a/packages/SystemUI/res/values/values_tv.xml b/packages/SystemUI/res/values/values_tv.xml
index 6a72e54..bd72c51 100644
--- a/packages/SystemUI/res/values/values_tv.xml
+++ b/packages/SystemUI/res/values/values_tv.xml
@@ -15,5 +15,5 @@
 -->
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
     <item format="float" type="integer" name="unselected_scale">1.0</item>
-    <item format="float" type="integer" name="selected_scale">1.1</item>
+    <item format="float" type="integer" name="selected_scale">1.259</item>
 </resources>
diff --git a/packages/SystemUI/src/com/android/systemui/recents/Recents.java b/packages/SystemUI/src/com/android/systemui/recents/Recents.java
index 2b6ed44..da07aec 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/Recents.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/Recents.java
@@ -17,6 +17,7 @@
 package com.android.systemui.recents;
 
 import android.app.ActivityManager;
+import android.app.UiModeManager;
 import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -53,6 +54,7 @@
 import com.android.systemui.recents.events.ui.RecentsDrawnEvent;
 import com.android.systemui.recents.misc.SystemServicesProxy;
 import com.android.systemui.recents.model.RecentsTaskLoader;
+import com.android.systemui.recents.tv.RecentsTvImpl;
 
 import java.util.ArrayList;
 
@@ -182,7 +184,13 @@
         sTaskLoader = new RecentsTaskLoader(mContext);
         sConfiguration = new RecentsConfiguration(mContext);
         mHandler = new Handler();
-        mImpl = new RecentsImpl(mContext);
+        UiModeManager uiModeManager = (UiModeManager) mContext.
+                getSystemService(Context.UI_MODE_SERVICE);
+        if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) {
+            mImpl = new RecentsTvImpl(mContext);
+        } else {
+            mImpl = new RecentsImpl(mContext);
+        }
 
         // Check if there is a recents override package
         if ("userdebug".equals(Build.TYPE) || "eng".equals(Build.TYPE)) {
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
index 9be24de..880fe10 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
@@ -21,12 +21,10 @@
 import android.app.ActivityManager;
 import android.app.ActivityOptions;
 import android.app.ITaskStackListener;
-import android.app.UiModeManager;
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.ActivityNotFoundException;
 import android.content.Context;
 import android.content.Intent;
-import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
@@ -66,7 +64,6 @@
 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.tv.views.TaskCardView;
 import com.android.systemui.recents.views.TaskStackLayoutAlgorithm;
 import com.android.systemui.recents.views.TaskStackView;
 import com.android.systemui.recents.views.TaskStackViewScroller;
@@ -96,10 +93,6 @@
 
     public final static String RECENTS_PACKAGE = "com.android.systemui";
     public final static String RECENTS_ACTIVITY = "com.android.systemui.recents.RecentsActivity";
-    public final static String RECENTS_TV_ACTIVITY = "com.android.systemui.recents.tv.RecentsTvActivity";
-
-    //Used to store tv or non-tv activty for use in creating intents.
-    private final String mRecentsIntentActivityName;
 
     /**
      * An implementation of ITaskStackListener, that allows us to listen for changes to the system
@@ -158,16 +151,15 @@
         }
     }
 
-    private static RecentsTaskLoadPlan sInstanceLoadPlan;
+    protected static RecentsTaskLoadPlan sInstanceLoadPlan;
 
-    Context mContext;
-    Handler mHandler;
+    protected Context mContext;
+    protected Handler mHandler;
     TaskStackListenerImpl mTaskStackListener;
     RecentsAppWidgetHost mAppWidgetHost;
-    boolean mCanReuseTaskStackViews = true;
+    protected boolean mCanReuseTaskStackViews = true;
     boolean mDraggingInRecents;
     boolean mLaunchedWhileDocking;
-    private boolean mIsRunningOnTv;
 
     // Task launching
     Rect mSearchBarBounds = new Rect();
@@ -182,11 +174,11 @@
     // Header (for transition)
     TaskViewHeader mHeaderBar;
     final Object mHeaderBarLock = new Object();
-    TaskStackView mDummyStackView;
+    protected TaskStackView mDummyStackView;
 
     // Variables to keep track of if we need to start recents after binding
-    boolean mTriggeredFromAltTab;
-    long mLastToggleTime;
+    protected boolean mTriggeredFromAltTab;
+    protected long mLastToggleTime;
     DozeTrigger mFastAltTabTrigger = new DozeTrigger(FAST_ALT_TAB_DELAY_MS, new Runnable() {
         @Override
         public void run() {
@@ -197,7 +189,7 @@
         }
     });
 
-    Bitmap mThumbnailTransitionBitmapCache;
+    protected Bitmap mThumbnailTransitionBitmapCache;
     Task mThumbnailTransitionBitmapCacheKey;
 
     public RecentsImpl(Context context) {
@@ -227,16 +219,6 @@
         launchOpts.numVisibleTaskThumbnails = loader.getThumbnailCacheSize();
         launchOpts.onlyLoadForCache = true;
         loader.loadTasks(mContext, plan, launchOpts);
-
-        //Manager used to determine if we are running on tv or not
-        UiModeManager uiModeManager = (UiModeManager) mContext.getSystemService(Context.UI_MODE_SERVICE);
-        if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) {
-            mRecentsIntentActivityName = RECENTS_TV_ACTIVITY;
-            mIsRunningOnTv = true;
-        } else {
-            mRecentsIntentActivityName = RECENTS_ACTIVITY;
-            mIsRunningOnTv = false;
-        }
     }
 
     public void onBootCompleted() {
@@ -729,7 +711,7 @@
     /**
      * Creates the activity options for a unknown state->recents transition.
      */
-    private ActivityOptions getUnknownTransitionActivityOptions() {
+    protected ActivityOptions getUnknownTransitionActivityOptions() {
         return ActivityOptions.makeCustomAnimation(mContext,
                 R.anim.recents_from_unknown_enter,
                 R.anim.recents_from_unknown_exit,
@@ -739,7 +721,7 @@
     /**
      * Creates the activity options for a home->recents transition.
      */
-    private ActivityOptions getHomeTransitionActivityOptions(boolean fromSearchHome) {
+    protected ActivityOptions getHomeTransitionActivityOptions(boolean fromSearchHome) {
         if (fromSearchHome) {
             return ActivityOptions.makeCustomAnimation(mContext,
                     R.anim.recents_from_search_launcher_enter,
@@ -797,22 +779,6 @@
         }
     }
 
-    /**
-     * Creates the activity options for an app->recents transition on TV.
-     */
-    private ActivityOptions getThumbnailTransitionActivityOptionsForTV(
-            ActivityManager.RunningTaskInfo topTask) {
-        Bitmap thumbnail = mThumbnailTransitionBitmapCache;
-        Rect rect = TaskCardView.getStartingCardThumbnailRect(mContext);
-        if (thumbnail != null) {
-            return ActivityOptions.makeThumbnailAspectScaleDownAnimation(mDummyStackView,
-                    null, (int) rect.left, (int) rect.top,
-                    (int) rect.width(), (int) rect.height(), mHandler, null);
-        }
-        // If both the screenshot and thumbnail fails, then just fall back to the default transition
-        return getUnknownTransitionActivityOptions();
-    }
-
     private Bitmap getThumbnailBitmap(ActivityManager.RunningTaskInfo topTask, Task toTask,
             TaskViewTransform toTransform) {
         Bitmap thumbnail;
@@ -888,15 +854,10 @@
     /**
      * Shows the recents activity
      */
-    private void startRecentsActivity(ActivityManager.RunningTaskInfo topTask,
+    protected void startRecentsActivity(ActivityManager.RunningTaskInfo topTask,
             boolean isTopTaskHome, boolean animate) {
         RecentsTaskLoader loader = Recents.getTaskLoader();
 
-        // If we are on TV, divert to a different helper method
-        if (mIsRunningOnTv) {
-            setUpAndStartTvRecents(topTask, isTopTaskHome, animate);
-            return;
-        }
         // In the case where alt-tab is triggered, we never get a preloadRecents() call, so we
         // should always preload the tasks now. If we are dragging in recents, reload them as
         // the stacks might have changed.
@@ -972,90 +933,6 @@
     }
 
     /**
-     * Used to set up the animations of Tv Recents, then start the Recents Activity.
-     * TODO: Add the Transitions for Home -> Recents TV
-     * TODO: Shift Transition code to separate class under /tv directory and access
-     *              from here
-     */
-    private void setUpAndStartTvRecents(ActivityManager.RunningTaskInfo topTask,
-                                      boolean isTopTaskHome, boolean animate) {
-        RecentsTaskLoader loader = Recents.getTaskLoader();
-
-        // In the case where alt-tab is triggered, we never get a preloadRecents() call, so we
-        // should always preload the tasks now. If we are dragging in recents, reload them as
-        // the stacks might have changed.
-        if (mLaunchedWhileDocking || mTriggeredFromAltTab || sInstanceLoadPlan == null) {
-            // Create a new load plan if preloadRecents() was never triggered
-            sInstanceLoadPlan = loader.createLoadPlan(mContext);
-        }
-        if (mLaunchedWhileDocking || mTriggeredFromAltTab || !sInstanceLoadPlan.hasTasks()) {
-            loader.preloadTasks(sInstanceLoadPlan, topTask.id, isTopTaskHome);
-        }
-        TaskStack stack = sInstanceLoadPlan.getTaskStack();
-
-        // Update the header bar if necessary
-        updateHeaderBarLayout(false /* tryAndBindSearchWidget */, stack);
-
-        // Prepare the dummy stack for the transition
-        TaskStackLayoutAlgorithm.VisibilityReport stackVr =
-                mDummyStackView.computeStackVisibilityReport();
-
-        if (!animate) {
-            ActivityOptions opts = ActivityOptions.makeCustomAnimation(mContext, -1, -1);
-            startRecentsActivity(topTask, opts, false /* fromHome */,
-                    false /* fromSearchHome */, false /* fromThumbnail*/, stackVr);
-            return;
-        }
-
-        boolean hasRecentTasks = stack.getTaskCount() > 0;
-        boolean useThumbnailTransition = (topTask != null) && !isTopTaskHome && hasRecentTasks;
-
-        if (useThumbnailTransition) {
-            // Try starting with a thumbnail transition
-            ActivityOptions opts = getThumbnailTransitionActivityOptionsForTV(topTask);
-            if (opts != null) {
-                startRecentsActivity(topTask, opts, false /* fromHome */,
-                        false /* fromSearchHome */, true /* fromThumbnail */, stackVr);
-            } else {
-                // Fall through below to the non-thumbnail transition
-                useThumbnailTransition = false;
-            }
-        }
-
-        if (!useThumbnailTransition) {
-            // If there is no thumbnail transition, but is launching from home into recents, then
-            // use a quick home transition and do the animation from home
-            if (hasRecentTasks) {
-                SystemServicesProxy ssp = Recents.getSystemServices();
-                String homeActivityPackage = ssp.getHomeActivityPackageName();
-                String searchWidgetPackage = null;
-                if (RecentsDebugFlags.Static.EnableSearchBar) {
-                    searchWidgetPackage = Prefs.getString(mContext,
-                            Prefs.Key.OVERVIEW_SEARCH_APP_WIDGET_PACKAGE, null);
-                } else {
-                    AppWidgetProviderInfo searchWidgetInfo = ssp.resolveSearchAppWidget();
-                    if (searchWidgetInfo != null) {
-                        searchWidgetPackage = searchWidgetInfo.provider.getPackageName();
-                    }
-                }
-
-                // Determine whether we are coming from a search owned home activity
-                boolean fromSearchHome = (homeActivityPackage != null) &&
-                        homeActivityPackage.equals(searchWidgetPackage);
-                ActivityOptions opts = getHomeTransitionActivityOptions(fromSearchHome);
-                startRecentsActivity(topTask, opts, true /* fromHome */, fromSearchHome,
-                        false /* fromThumbnail */, stackVr);
-            } else {
-                // Otherwise we do the normal fade from an unknown source
-                ActivityOptions opts = getUnknownTransitionActivityOptions();
-                startRecentsActivity(topTask, opts, true /* fromHome */,
-                        false /* fromSearchHome */, false /* fromThumbnail */, stackVr);
-            }
-        }
-        mLastToggleTime = SystemClock.elapsedRealtime();
-    }
-
-    /**
      * Starts the recents activity.
      */
     private void startRecentsActivity(ActivityManager.RunningTaskInfo topTask,
@@ -1078,7 +955,7 @@
         launchState.launchedWhileDocking = mLaunchedWhileDocking;
 
         Intent intent = new Intent();
-        intent.setClassName(RECENTS_PACKAGE, mRecentsIntentActivityName);
+        intent.setClassName(RECENTS_PACKAGE, RECENTS_ACTIVITY);
         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
                 | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
                 | Intent.FLAG_ACTIVITY_TASK_ON_HOME);
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 de3c2be..330d138 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java
@@ -47,7 +47,6 @@
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
-import android.hardware.display.DisplayManager;
 import android.os.Bundle;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
@@ -73,6 +72,7 @@
 import com.android.systemui.R;
 import com.android.systemui.recents.RecentsDebugFlags;
 import com.android.systemui.recents.RecentsImpl;
+import com.android.systemui.recents.tv.RecentsTvImpl;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -287,7 +287,7 @@
             // Check if the front most activity is recents
             if ((topActivity.getPackageName().equals(RecentsImpl.RECENTS_PACKAGE) &&
                     (topActivity.getClassName().equals(RecentsImpl.RECENTS_ACTIVITY) ||
-                    topActivity.getClassName().equals(RecentsImpl.RECENTS_TV_ACTIVITY)))) {
+                    topActivity.getClassName().equals(RecentsTvImpl.RECENTS_TV_ACTIVITY)))) {
                 if (isHomeTopMost != null) {
                     isHomeTopMost.value = false;
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/tv/RecentsTvImpl.java b/packages/SystemUI/src/com/android/systemui/recents/tv/RecentsTvImpl.java
new file mode 100644
index 0000000..9fd5d55
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/tv/RecentsTvImpl.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2016 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.tv;
+
+import android.app.ActivityManager;
+import android.app.ActivityOptions;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import com.android.systemui.recents.*;
+import com.android.systemui.recents.events.EventBus;
+import com.android.systemui.recents.events.activity.RecentsActivityStartingEvent;
+import com.android.systemui.recents.misc.SystemServicesProxy;
+import com.android.systemui.recents.model.RecentsTaskLoader;
+import com.android.systemui.recents.model.TaskStack;
+import com.android.systemui.recents.tv.views.TaskCardView;
+
+public class RecentsTvImpl extends RecentsImpl{
+    public final static String RECENTS_TV_ACTIVITY =
+            "com.android.systemui.recents.tv.RecentsTvActivity";
+
+    public RecentsTvImpl(Context context) {
+        super(context);
+    }
+
+    @Override
+    protected void startRecentsActivity(ActivityManager.RunningTaskInfo topTask,
+            boolean isTopTaskHome, boolean animate) {
+        RecentsTaskLoader loader = Recents.getTaskLoader();
+
+        // In the case where alt-tab is triggered, we never get a preloadRecents() call, so we
+        // should always preload the tasks now. If we are dragging in recents, reload them as
+        // the stacks might have changed.
+        if (mTriggeredFromAltTab || sInstanceLoadPlan == null) {
+            // Create a new load plan if preloadRecents() was never triggered
+            sInstanceLoadPlan = loader.createLoadPlan(mContext);
+        }
+        if (mTriggeredFromAltTab || !sInstanceLoadPlan.hasTasks()) {
+            loader.preloadTasks(sInstanceLoadPlan, topTask.id, isTopTaskHome);
+        }
+        TaskStack stack = sInstanceLoadPlan.getTaskStack();
+
+        if (!animate) {
+            ActivityOptions opts = ActivityOptions.makeCustomAnimation(mContext, -1, -1);
+            startRecentsActivity(topTask, opts, false /* fromHome */, false /* fromThumbnail*/);
+            return;
+        }
+
+        boolean hasRecentTasks = stack.getTaskCount() > 0;
+        boolean useThumbnailTransition = (topTask != null) && !isTopTaskHome && hasRecentTasks;
+
+        if (useThumbnailTransition) {
+            // Try starting with a thumbnail transition
+            ActivityOptions opts = getThumbnailTransitionActivityOptionsForTV(topTask);
+            if (opts != null) {
+                startRecentsActivity(topTask, opts, false /* fromHome */, true /* fromThumbnail */);
+            } else {
+                // Fall through below to the non-thumbnail transition
+                useThumbnailTransition = false;
+            }
+        }
+
+        if (!useThumbnailTransition) {
+            // If there is no thumbnail transition, but is launching from home into recents, then
+            // use a quick home transition and do the animation from home
+            if (hasRecentTasks) {
+                SystemServicesProxy ssp = Recents.getSystemServices();
+                ActivityOptions opts = getHomeTransitionActivityOptions(false);
+                startRecentsActivity(topTask, opts, true /* fromHome */, false /* fromThumbnail */);
+            } else {
+                // Otherwise we do the normal fade from an unknown source
+                ActivityOptions opts = getUnknownTransitionActivityOptions();
+                startRecentsActivity(topTask, opts, true /* fromHome */, false /* fromThumbnail */);
+            }
+        }
+        mLastToggleTime = SystemClock.elapsedRealtime();
+    }
+
+    protected void startRecentsActivity(ActivityManager.RunningTaskInfo topTask,
+            ActivityOptions opts, boolean fromHome, boolean fromThumbnail) {
+        // Update the configuration based on the launch options
+        RecentsConfiguration config = Recents.getConfiguration();
+        RecentsActivityLaunchState launchState = config.getLaunchState();
+        launchState.launchedFromHome = fromHome;
+        launchState.launchedFromSearchHome = false;
+        launchState.launchedFromApp = fromThumbnail;
+        launchState.launchedToTaskId = (topTask != null) ? topTask.id : -1;
+        launchState.launchedWithAltTab = mTriggeredFromAltTab;
+        launchState.launchedReuseTaskStackViews = mCanReuseTaskStackViews;
+        launchState.launchedHasConfigurationChanged = false;
+
+        Intent intent = new Intent();
+        intent.setClassName(RECENTS_PACKAGE, RECENTS_TV_ACTIVITY);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+                | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
+                | Intent.FLAG_ACTIVITY_TASK_ON_HOME);
+
+        if (opts != null) {
+            mContext.startActivityAsUser(intent, opts.toBundle(), UserHandle.CURRENT);
+        } else {
+            mContext.startActivityAsUser(intent, UserHandle.CURRENT);
+        }
+        mCanReuseTaskStackViews = true;
+        EventBus.getDefault().send(new RecentsActivityStartingEvent());
+    }
+
+    /**
+     * Creates the activity options for an app->recents transition on TV.
+     */
+    private ActivityOptions getThumbnailTransitionActivityOptionsForTV(
+            ActivityManager.RunningTaskInfo topTask) {
+        Bitmap thumbnail = mThumbnailTransitionBitmapCache;
+        Rect rect = TaskCardView.getStartingCardThumbnailRect(mContext);
+        if (thumbnail != null) {
+            return ActivityOptions.makeThumbnailAspectScaleDownAnimation(mDummyStackView,
+                    null, (int) rect.left, (int) rect.top,
+                    (int) rect.width(), (int) rect.height(), mHandler, null);
+        }
+        // If both the screenshot and thumbnail fails, then just fall back to the default transition
+        return getUnknownTransitionActivityOptions();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/tv/animations/DismissAnimationsHolder.java b/packages/SystemUI/src/com/android/systemui/recents/tv/animations/DismissAnimationsHolder.java
new file mode 100644
index 0000000..8996d0b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/tv/animations/DismissAnimationsHolder.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2016 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.tv.animations;
+
+
+import android.animation.Animator;
+import android.content.res.Resources;
+import android.support.v4.view.animation.FastOutSlowInInterpolator;
+import android.view.View;
+import android.widget.LinearLayout;
+import com.android.systemui.recents.tv.views.TaskCardView;
+
+import com.android.systemui.R;
+
+public class DismissAnimationsHolder {
+    private LinearLayout mDismissArea;
+    private LinearLayout mTaskCardView;
+    private FastOutSlowInInterpolator mFastOutSlowIn;
+    private int mCardYDelta;
+    private long mShortDuration;
+    private long mLongDuration;
+
+    public DismissAnimationsHolder(TaskCardView taskCardView) {
+        mTaskCardView = (LinearLayout) taskCardView.findViewById(R.id.recents_tv_card);
+        mDismissArea = (LinearLayout) taskCardView.findViewById(R.id.card_dismiss);
+        mFastOutSlowIn = new FastOutSlowInInterpolator();
+
+        Resources res = taskCardView.getResources();
+        mCardYDelta = res.getDimensionPixelOffset(R.dimen.recents_tv_dismiss_shift_down);
+        mShortDuration =  res.getInteger(R.integer.dismiss_short_duration);
+        mLongDuration =  res.getInteger(R.integer.dismiss_long_duration);
+    }
+
+    public void startEnterAnimation() {
+        mDismissArea.animate().setDuration(mShortDuration);
+        mDismissArea.animate().setInterpolator(mFastOutSlowIn);
+        mDismissArea.animate().alpha(1.0f);
+
+        mTaskCardView.animate().setDuration(mShortDuration);
+        mTaskCardView.animate().setInterpolator(mFastOutSlowIn);
+        mTaskCardView.animate().translationYBy(mCardYDelta);
+        mTaskCardView.animate().alpha(0.5f);
+    }
+
+    public void startExitAnimation() {
+        mDismissArea.animate().setDuration(mShortDuration);
+        mDismissArea.animate().setInterpolator(mFastOutSlowIn);
+        mDismissArea.animate().alpha(0.0f);
+
+        mTaskCardView.animate().setDuration(mShortDuration);
+        mTaskCardView.animate().setInterpolator(mFastOutSlowIn);
+        mTaskCardView.animate().translationYBy(-mCardYDelta);
+        mTaskCardView.animate().alpha(1.0f);
+    }
+
+    public void startDismissAnimation(Animator.AnimatorListener listener) {
+        mDismissArea.animate().setDuration(mShortDuration);
+        mDismissArea.animate().setInterpolator(mFastOutSlowIn);
+        mDismissArea.animate().alpha(0.0f);
+
+        mTaskCardView.animate().setDuration(mLongDuration);
+        mTaskCardView.animate().setInterpolator(mFastOutSlowIn);
+        mTaskCardView.animate().translationYBy(mCardYDelta);
+        mTaskCardView.animate().alpha(0.0f);
+        mTaskCardView.animate().setListener(listener);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/tv/animations/ViewFocusAnimator.java b/packages/SystemUI/src/com/android/systemui/recents/tv/animations/ViewFocusAnimator.java
index 365b29d..888561c 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/tv/animations/ViewFocusAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/tv/animations/ViewFocusAnimator.java
@@ -33,6 +33,8 @@
     private final float mSelectedScaleDelta;
     private final float mUnselectedZ;
     private final float mSelectedZDelta;
+    private final float mUnselectedSpacing;
+    private final float mSelectedSpacingDelta;
     private final int mAnimDuration;
     private final Interpolator mFocusInterpolator;
 
@@ -57,6 +59,9 @@
         mUnselectedZ = res.getDimensionPixelOffset(R.dimen.recents_tv_unselected_item_z);
         mSelectedZDelta = res.getDimensionPixelOffset(R.dimen.recents_tv_selected_item_z_delta);
 
+        mUnselectedSpacing = res.getDimensionPixelOffset(R.dimen.recents_tv_gird_card_spacing);
+        mSelectedSpacingDelta = res.getDimensionPixelOffset(R.dimen.recents_tv_gird_focused_card_delta);
+
         mAnimDuration = res.getInteger(R.integer.item_scale_anim_duration);
 
         mFocusInterpolator = new AccelerateDecelerateInterpolator();
@@ -85,10 +90,14 @@
 
         float scale = mUnselectedScale + (level * mSelectedScaleDelta);
         float z = mUnselectedZ + (level * mSelectedZDelta);
+        float spacing = mUnselectedSpacing + (level * mSelectedSpacingDelta);
 
         mTargetView.setScaleX(scale);
         mTargetView.setScaleY(scale);
         mTargetView.setZ(z);
+
+        mTargetView.setPadding((int) spacing, mTargetView.getPaddingTop(),
+                (int) spacing, mTargetView.getPaddingBottom());
     }
 
     public float getFocusProgress() {
diff --git a/packages/SystemUI/src/com/android/systemui/recents/tv/views/TaskCardView.java b/packages/SystemUI/src/com/android/systemui/recents/tv/views/TaskCardView.java
index 5775b60..3343aec 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/tv/views/TaskCardView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/tv/views/TaskCardView.java
@@ -15,6 +15,7 @@
  */
 package com.android.systemui.recents.tv.views;
 
+import android.animation.Animator;
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Point;
@@ -22,12 +23,14 @@
 import android.util.AttributeSet;
 import android.util.TypedValue;
 import android.view.Display;
+import android.view.KeyEvent;
 import android.view.WindowManager;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
 import com.android.systemui.R;
+import com.android.systemui.recents.tv.animations.DismissAnimationsHolder;
 import com.android.systemui.recents.tv.animations.ViewFocusAnimator;
 import com.android.systemui.recents.model.Task;
 
@@ -37,8 +40,10 @@
     private TextView mTitleTextView;
     private ImageView mBadgeView;
     private Task mTask;
+    private boolean mDismissState;
 
     private ViewFocusAnimator mViewFocusAnimator;
+    private DismissAnimationsHolder mDismissAnimationsHolder;
 
     public TaskCardView(Context context) {
         this(context, null);
@@ -51,6 +56,7 @@
     public TaskCardView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
         mViewFocusAnimator = new ViewFocusAnimator(this);
+        mDismissState = false;
     }
 
     @Override
@@ -58,6 +64,7 @@
         mThumbnailView = (ImageView) findViewById(R.id.card_view_thumbnail);
         mTitleTextView = (TextView) findViewById(R.id.card_title_text);
         mBadgeView = (ImageView) findViewById(R.id.card_extra_badge);
+        mDismissAnimationsHolder = new DismissAnimationsHolder(this);
     }
 
     public void init(Task task) {
@@ -98,13 +105,23 @@
 
         int width = res.getDimensionPixelOffset(R.dimen.recents_tv_card_width);
         int widthDelta = (int) (width * scale - width);
-        int height = (int) (res.getDimensionPixelOffset(
-                R.dimen.recents_tv_screenshot_height) * scale);
-        int padding = res.getDimensionPixelOffset(R.dimen.recents_tv_grid_row_padding);
+        int height = res.getDimensionPixelOffset(R.dimen.recents_tv_screenshot_height);
+        int heightDelta = (int) (height * scale - height);
+        int topMargin = res.getDimensionPixelOffset(R.dimen.recents_tv_gird_row_top_margin);
 
-        int headerHeight = (int) ((res.getDimensionPixelOffset(
-                R.dimen.recents_tv_card_extra_badge_size) +
-                res.getDimensionPixelOffset(R.dimen.recents_tv_icon_padding_bottom)) * scale);
+        int headerHeight = res.getDimensionPixelOffset(R.dimen.recents_tv_card_extra_badge_size) +
+                res.getDimensionPixelOffset(R.dimen.recents_tv_icon_padding_bottom);
+        int headerHeightDelta = (int) (headerHeight * scale - headerHeight);
+
+        int dismissAreaHeight =
+                res.getDimensionPixelOffset(R.dimen.recents_tv_dismiss_icon_top_margin) +
+                res.getDimensionPixelOffset(R.dimen.recents_tv_dismiss_icon_bottom_margin) +
+                res.getDimensionPixelOffset(R.dimen.recents_tv_dismiss_icon_size) +
+                res.getDimensionPixelOffset(R.dimen.recents_tv_dismiss_text_size);
+
+        int dismissAreaHeightDelta = (int) (dismissAreaHeight * scale - dismissAreaHeight);
+
+        int totalHeightDelta = heightDelta + headerHeightDelta + dismissAreaHeightDelta;
 
         WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
         Display display = wm.getDefaultDisplay();
@@ -113,9 +130,72 @@
         int screenWidth = size.x;
         int screenHeight = size.y;
 
-        return new Rect(screenWidth - width - padding - widthDelta / 2,
-                screenHeight / 2 - height / 2 + headerHeight / 2,
-                screenWidth - padding + widthDelta / 2,
-                screenHeight / 2 + height / 2 + headerHeight / 2);
+        return new Rect(screenWidth / 2 - width / 2 - widthDelta / 2,
+                topMargin - totalHeightDelta / 2 + (int) (headerHeight * scale),
+                screenWidth / 2 + width / 2 + widthDelta / 2,
+                topMargin - totalHeightDelta / 2 + (int) (headerHeight * scale) +
+                        (int) (height * scale));
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_DPAD_DOWN : {
+                if (!isInDismissState()) {
+                    setDismissState(true);
+                    return true;
+                }
+                break;
+            }
+            case KeyEvent.KEYCODE_DPAD_UP : {
+                if (isInDismissState()) {
+                    setDismissState(false);
+                    return true;
+                }
+                break;
+            }
+
+            //Eat right and left key presses when we are in dismiss state
+            case KeyEvent.KEYCODE_DPAD_LEFT : {
+                if (isInDismissState()) {
+                    return true;
+                }
+                break;
+            }
+            case KeyEvent.KEYCODE_DPAD_RIGHT : {
+                if (isInDismissState()) {
+                    return true;
+                }
+                break;
+            }
+            default:
+                break;
+        }
+        return super.onKeyDown(keyCode, event);
+    }
+
+    private void setDismissState(boolean dismissState) {
+        if (mDismissState != dismissState) {
+            mDismissState = dismissState;
+            if (dismissState) {
+                mDismissAnimationsHolder.startEnterAnimation();
+            } else {
+                mDismissAnimationsHolder.startExitAnimation();
+            }
+        }
+    }
+
+    public boolean isInDismissState() {
+        return mDismissState;
+    }
+
+    public void startDismissTaskAnimation(Animator.AnimatorListener listener) {
+        mDismissAnimationsHolder.startDismissAnimation(listener);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        setDismissState(false);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/tv/views/TaskStackHorizontalGridView.java b/packages/SystemUI/src/com/android/systemui/recents/tv/views/TaskStackHorizontalGridView.java
index cf8c9bb..5c2de8e 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/tv/views/TaskStackHorizontalGridView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/tv/views/TaskStackHorizontalGridView.java
@@ -41,7 +41,6 @@
     private ArrayList<TaskCardView> mTaskViews = new ArrayList<>();
     private Task mFocusedTask;
 
-
     public TaskStackHorizontalGridView(Context context) {
         this(context, null);
     }
@@ -53,7 +52,7 @@
     @Override
     protected void onAttachedToWindow() {
         EventBus.getDefault().register(this, RecentsActivity.EVENT_BUS_PRIORITY + 1);
-        setItemMargin((int) getResources().getDimension(R.dimen.recents_tv_gird_card_spacing));
+        setWindowAlignment(WINDOW_ALIGN_NO_EDGE);
         setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
         super.onAttachedToWindow();
     }
@@ -109,6 +108,13 @@
     }
 
     /**
+     * @return - The focused task card view.
+     */
+    public TaskCardView getFocusedTaskCardView() {
+        return ((TaskCardView)findFocus());
+    }
+
+    /**
      * @param task
      * @return Child view for given task
      */
diff --git a/packages/SystemUI/src/com/android/systemui/recents/tv/views/TaskStackHorizontalViewAdapter.java b/packages/SystemUI/src/com/android/systemui/recents/tv/views/TaskStackHorizontalViewAdapter.java
index fba424e..3788719 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/tv/views/TaskStackHorizontalViewAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/tv/views/TaskStackHorizontalViewAdapter.java
@@ -15,6 +15,7 @@
  */
 package com.android.systemui.recents.tv.views;
 
+import android.animation.Animator;
 import android.app.Activity;
 import android.support.v7.widget.RecyclerView;
 import android.util.Log;
@@ -25,6 +26,7 @@
 import com.android.systemui.R;
 import com.android.systemui.recents.events.EventBus;
 import com.android.systemui.recents.events.activity.LaunchTvTaskEvent;
+import com.android.systemui.recents.events.ui.DeleteTaskDataEvent;
 import com.android.systemui.recents.model.Task;
 
 import java.util.ArrayList;
@@ -39,7 +41,7 @@
     private static final String TAG = "TaskStackViewAdapter";
     private List<Task> mTaskList;
 
-    static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener{
+    public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener{
         private TaskCardView mTaskCardView;
         private Task mTask;
         public ViewHolder(View v) {
@@ -58,9 +60,14 @@
         @Override
         public void onClick(View v) {
             try {
-                EventBus.getDefault().send(new LaunchTvTaskEvent(mTaskCardView, mTask,
-                        null, INVALID_STACK_ID));
-                ((Activity)(v.getContext())).finish();
+                if (mTaskCardView.isInDismissState()) {
+                    mTaskCardView.startDismissTaskAnimation(
+                            getRemoveAtListener(getAdapterPosition(), mTaskCardView));
+                } else {
+                    EventBus.getDefault().send(new LaunchTvTaskEvent(mTaskCardView, mTask,
+                            null, INVALID_STACK_ID));
+                    ((Activity) (v.getContext())).finish();
+                }
             } catch (Exception e) {
                 Log.e(TAG, v.getContext()
                         .getString(R.string.recents_launch_error_message, mTask.title), e);
@@ -97,4 +104,31 @@
     public int getItemCount() {
         return mTaskList.size();
     }
+
+    private Animator.AnimatorListener getRemoveAtListener(final int position,
+                                                          final TaskCardView taskCardView) {
+        return new Animator.AnimatorListener() {
+
+            @Override
+            public void onAnimationStart(Animator animation) { }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                removeAt(position);
+                EventBus.getDefault().send(new DeleteTaskDataEvent(taskCardView.getTask()));
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) { }
+
+            @Override
+            public void onAnimationRepeat(Animator animation) { }
+        };
+
+    }
+
+    private void removeAt(int position) {
+        mTaskList.remove(position);
+        notifyItemRemoved(position);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java
index 7f61e7a..132c09f 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java
@@ -83,18 +83,6 @@
     private static final int TASK_POSITION_SAME = Integer.MAX_VALUE;
 
     /**
-     * Fraction of the divider position between two snap targets to switch to the full-screen
-     * target.
-     */
-    private static final float SWITCH_FULLSCREEN_FRACTION = 0.12f;
-
-    /**
-     * Fraction of the divider position between two snap targets to switch to the larger target
-     * for the bottom/right app layout.
-     */
-    private static final float BOTTOM_RIGHT_SWITCH_BIGGER_FRACTION = 0.2f;
-
-    /**
      * How much the background gets scaled when we are in the minimized dock state.
      */
     private static final float MINIMIZE_DOCK_SCALE = 0.375f;
@@ -653,12 +641,6 @@
                     restrictDismissingTaskPosition(taskPosition, mDockSide, taskSnapTarget);
             int taskPositionOther =
                     restrictDismissingTaskPosition(taskPosition, dockSideInverted, taskSnapTarget);
-
-            taskPositionDocked = minimizeHoles(position, taskPositionDocked, mDockSide,
-                    taskSnapTarget);
-            taskPositionOther = minimizeHoles(position, taskPositionOther, dockSideInverted,
-                    taskSnapTarget);
-
             calculateBoundsForPosition(taskPositionDocked, mDockSide, mDockedTaskRect);
             calculateBoundsForPosition(taskPositionOther, dockSideInverted, mOtherTaskRect);
             mDisplayRect.set(0, 0, mDisplayWidth, mDisplayHeight);
@@ -724,51 +706,6 @@
     }
 
     /**
-     * Given the current split position and the task position calculated by dragging, this
-     * method calculates a "better" task position in a sense so holes get smaller while dragging.
-     *
-     * @return the new task position
-     */
-    private int minimizeHoles(int position, int taskPosition, int dockSide,
-            SnapTarget taskSnapTarget) {
-        if (dockSideTopLeft(dockSide)) {
-            if (position > taskPosition) {
-                SnapTarget nextTarget = mSnapAlgorithm.getNextTarget(taskSnapTarget);
-
-                // If the next target is the dismiss end target, switch earlier to make the hole
-                // smaller.
-                if (nextTarget != taskSnapTarget
-                        && nextTarget == mSnapAlgorithm.getDismissEndTarget()) {
-                    float t = (float) (position - taskPosition)
-                            / (nextTarget.position - taskPosition);
-                    if (t > SWITCH_FULLSCREEN_FRACTION) {
-                        return nextTarget.position;
-                    }
-                }
-            }
-        } else if (dockSideBottomRight(dockSide)) {
-            if (position < taskPosition) {
-                SnapTarget previousTarget = mSnapAlgorithm.getPreviousTarget(taskSnapTarget);
-                if (previousTarget != taskSnapTarget) {
-                    float t = (float) (taskPosition - position)
-                            / (taskPosition - previousTarget.position);
-
-                    // In general, switch a bit earlier (at 20% instead of 50%), but if we are
-                    // dismissing the top, switch really early.
-                    float threshold = previousTarget == mSnapAlgorithm.getDismissStartTarget()
-                            ? SWITCH_FULLSCREEN_FRACTION
-                            : BOTTOM_RIGHT_SWITCH_BIGGER_FRACTION;
-                    if (t > threshold) {
-                        return previousTarget.position;
-                    }
-
-                }
-            }
-        }
-        return taskPosition;
-    }
-
-    /**
      * When the snap target is dismissing one side, make sure that the dismissing side doesn't get
      * 0 size.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
index a3af0a4..2446535 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
@@ -43,6 +43,7 @@
 
 import com.android.systemui.R;
 import com.android.systemui.classifier.FalsingManager;
+import com.android.systemui.statusbar.notification.HybridNotificationView;
 import com.android.systemui.statusbar.notification.NotificationViewWrapper;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
@@ -227,6 +228,7 @@
         updateClearability();
         if (mIsSummaryWithChildren) {
             recreateNotificationHeader();
+            mChildrenContainer.onNotificationUpdated();
         }
         if (mIconAnimationRunning) {
             setIconAnimationRunning(true);
@@ -584,6 +586,29 @@
         mPublicLayout.closeRemoteInput();
     }
 
+    /**
+     * Set by how much the single line view should be indented.
+     */
+    public void setSingleLineWidthIndention(int indention) {
+        mPrivateLayout.setSingleLineWidthIndention(indention);
+    }
+
+    public int getNotificationColor() {
+        int color = getStatusBarNotification().getNotification().color;
+        if (color == Notification.COLOR_DEFAULT) {
+            return mContext.getColor(com.android.internal.R.color.notification_icon_default_color);
+        }
+        return color;
+    }
+
+    public HybridNotificationView getSingleLineView() {
+        return mPrivateLayout.getSingleLineView();
+    }
+
+    public boolean isOnKeyguard() {
+        return mOnKeyguard;
+    }
+
     public interface ExpansionLogger {
         public void logNotificationExpansion(String key, boolean userAction, boolean expanded);
     }
@@ -677,6 +702,7 @@
             public void onInflate(ViewStub stub, View inflated) {
                 mChildrenContainer = (NotificationChildrenContainer) inflated;
                 mChildrenContainer.setNotificationParent(ExpandableNotificationRow.this);
+                mChildrenContainer.onNotificationUpdated();
                 mTranslateableViews.add(mChildrenContainer);
             }
         });
@@ -858,6 +884,7 @@
             showing.setDark(dark, fade, delay);
         }
         if (mIsSummaryWithChildren) {
+            mChildrenContainer.setDark(dark, fade, delay);
             mNotificationHeaderWrapper.setDark(dark, fade, delay);
         }
     }
@@ -954,6 +981,9 @@
             mIsSystemExpanded = expand;
             notifyHeightChanged(false /* needsAnimation */);
             logExpansionEvent(false, wasExpanded);
+            if (mChildrenContainer != null) {
+                mChildrenContainer.updateGroupOverflow();
+            }
         }
     }
 
@@ -966,6 +996,9 @@
             mOnKeyguard = onKeyguard;
             logExpansionEvent(false, wasExpanded);
             if (wasExpanded != isExpanded()) {
+                if (mIsSummaryWithChildren) {
+                    mChildrenContainer.updateGroupOverflow();
+                }
                 notifyHeightChanged(false /* needsAnimation */);
             }
         }
@@ -1260,7 +1293,7 @@
     @Override
     public int getMinExpandHeight() {
         if (mIsSummaryWithChildren && !mShowingPublic) {
-            return mChildrenContainer.getMinExpandHeight(mOnKeyguard);
+            return mChildrenContainer.getMinExpandHeight();
         }
         return getMinHeight();
     }
@@ -1347,7 +1380,7 @@
             if (isGroupExpanded()) {
                 return 1.0f;
             } else if (isUserLocked()) {
-                return mChildrenContainer.getChildExpandFraction();
+                return mChildrenContainer.getGroupExpandFraction();
             }
         }
         return 0.0f;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
index c2df292..0a41e42 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
@@ -31,7 +31,7 @@
 
 import com.android.systemui.R;
 import com.android.systemui.statusbar.notification.HybridNotificationView;
-import com.android.systemui.statusbar.notification.HybridNotificationViewManager;
+import com.android.systemui.statusbar.notification.HybridGroupManager;
 import com.android.systemui.statusbar.notification.NotificationCustomViewWrapper;
 import com.android.systemui.statusbar.notification.NotificationUtils;
 import com.android.systemui.statusbar.notification.NotificationViewWrapper;
@@ -75,7 +75,7 @@
     private NotificationViewWrapper mContractedWrapper;
     private NotificationViewWrapper mExpandedWrapper;
     private NotificationViewWrapper mHeadsUpWrapper;
-    private HybridNotificationViewManager mHybridViewManager;
+    private HybridGroupManager mHybridGroupManager;
     private int mClipTopAmount;
     private int mContentHeight;
     private int mUnrestrictedContentHeight;
@@ -116,10 +116,11 @@
     private ExpandableNotificationRow mContainingNotification;
     private int mTransformationStartVisibleType;
     private boolean mUserExpanding;
+    private int mSingleLineWidthIndention;
 
     public NotificationContentView(Context context, AttributeSet attrs) {
         super(context, attrs);
-        mHybridViewManager = new HybridNotificationViewManager(getContext(), this);
+        mHybridGroupManager = new HybridGroupManager(getContext(), this);
         mMinContractedHeight = getResources().getDimensionPixelSize(
                 R.dimen.min_notification_layout_height);
         mNotificationContentMarginEnd = getResources().getDimensionPixelSize(
@@ -139,6 +140,7 @@
         boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY;
         boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST;
         int maxSize = Integer.MAX_VALUE;
+        int width = MeasureSpec.getSize(widthMeasureSpec);
         if (hasFixedHeight || isHeightLimited) {
             maxSize = MeasureSpec.getSize(heightMeasureSpec);
         }
@@ -187,12 +189,18 @@
             maxChildHeight = Math.max(maxChildHeight, mHeadsUpChild.getMeasuredHeight());
         }
         if (mSingleLineView != null) {
-            mSingleLineView.measure(widthMeasureSpec,
+            int singleLineWidthSpec = widthMeasureSpec;
+            if (mSingleLineWidthIndention != 0
+                    && MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) {
+                singleLineWidthSpec = MeasureSpec.makeMeasureSpec(
+                        width - mSingleLineWidthIndention + mSingleLineView.getPaddingEnd(),
+                        MeasureSpec.AT_MOST);
+            }
+            mSingleLineView.measure(singleLineWidthSpec,
                     MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.AT_MOST));
             maxChildHeight = Math.max(maxChildHeight, mSingleLineView.getMeasuredHeight());
         }
         int ownHeight = Math.min(maxChildHeight, maxSize);
-        int width = MeasureSpec.getSize(widthMeasureSpec);
         setMeasuredDimension(width, ownHeight);
     }
 
@@ -715,7 +723,7 @@
 
     private void updateSingleLineView() {
         if (mIsChildInGroup) {
-            mSingleLineView = mHybridViewManager.bindFromNotification(
+            mSingleLineView = mHybridGroupManager.bindFromNotification(
                     mSingleLineView, mStatusBarNotification.getNotification());
         } else if (mSingleLineView != null) {
             removeView(mSingleLineView);
@@ -878,4 +886,20 @@
             updateBackgroundColor(false);
         }
     }
+
+    /**
+     * Set by how much the single line view should be indented. Used when a overflow indicator is
+     * present and only during measuring
+     */
+    public void setSingleLineWidthIndention(int singleLineWidthIndention) {
+        if (singleLineWidthIndention != mSingleLineWidthIndention) {
+            mSingleLineWidthIndention = singleLineWidthIndention;
+            mContainingNotification.forceLayout();
+            forceLayout();
+        }
+    }
+
+    public HybridNotificationView getSingleLineView() {
+        return mSingleLineView;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HybridGroupManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HybridGroupManager.java
new file mode 100644
index 0000000..8f2c81f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HybridGroupManager.java
@@ -0,0 +1,117 @@
+/*
+ * 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.statusbar.notification;
+
+import android.app.Notification;
+import android.content.Context;
+import android.text.BidiFormatter;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.text.style.TextAppearanceSpan;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+
+import java.util.List;
+
+/**
+ * A class managing hybrid groups that include {@link HybridNotificationView} and the notification
+ * group overflow.
+ */
+public class HybridGroupManager {
+
+    private final Context mContext;
+    private ViewGroup mParent;
+    private int mOverflowNumberColor;
+
+    public HybridGroupManager(Context ctx, ViewGroup parent) {
+        mContext = ctx;
+        mParent = parent;
+    }
+
+    private HybridNotificationView inflateHybridView() {
+        LayoutInflater inflater = mContext.getSystemService(LayoutInflater.class);
+        HybridNotificationView hybrid = (HybridNotificationView) inflater.inflate(
+                R.layout.hybrid_notification, mParent, false);
+        mParent.addView(hybrid);
+        return hybrid;
+    }
+
+    private TextView inflateOverflowNumber() {
+        LayoutInflater inflater = mContext.getSystemService(LayoutInflater.class);
+        TextView numberView = (TextView) inflater.inflate(
+                R.layout.hybrid_overflow_number, mParent, false);
+        mParent.addView(numberView);
+        updateOverFlowNumberColor(numberView);
+        return numberView;
+    }
+
+    private void updateOverFlowNumberColor(TextView numberView) {
+        numberView.setTextColor(mOverflowNumberColor);
+    }
+
+    public void setOverflowNumberColor(TextView numberView, int overflowNumberColor) {
+        mOverflowNumberColor = overflowNumberColor;
+        if (numberView != null) {
+            updateOverFlowNumberColor(numberView);
+        }
+    }
+
+    public HybridNotificationView bindFromNotification(HybridNotificationView reusableView,
+            Notification notification) {
+        if (reusableView == null) {
+            reusableView = inflateHybridView();
+        }
+        CharSequence titleText = resolveTitle(notification);
+        CharSequence contentText = resolveText(notification);
+        reusableView.bind(titleText, contentText);
+        return reusableView;
+    }
+
+    private CharSequence resolveText(Notification notification) {
+        CharSequence contentText = notification.extras.getCharSequence(Notification.EXTRA_TEXT);
+        if (contentText == null) {
+            contentText = notification.extras.getCharSequence(Notification.EXTRA_BIG_TEXT);
+        }
+        return contentText;
+    }
+
+    private CharSequence resolveTitle(Notification notification) {
+        CharSequence titleText = notification.extras.getCharSequence(Notification.EXTRA_TITLE);
+        if (titleText == null) {
+            titleText = notification.extras.getCharSequence(Notification.EXTRA_TITLE_BIG);
+        }
+        return titleText;
+    }
+
+    public TextView bindOverflowNumber(TextView reusableView, int number) {
+        if (reusableView == null) {
+            reusableView = inflateOverflowNumber();
+        }
+        String text = mContext.getResources().getString(
+                R.string.notification_group_overflow_indicator, number);
+        if (!text.equals(reusableView.getText())) {
+            reusableView.setText(text);
+        }
+        return reusableView;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HybridNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HybridNotificationView.java
index c80cad8..0a1795f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HybridNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HybridNotificationView.java
@@ -60,6 +60,14 @@
         super(context, attrs, defStyleAttr, defStyleRes);
     }
 
+    public TextView getTitleView() {
+        return mTitleView;
+    }
+
+    public TextView getTextView() {
+        return mTextView;
+    }
+
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HybridNotificationViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HybridNotificationViewManager.java
deleted file mode 100644
index a17501d..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HybridNotificationViewManager.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * 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.statusbar.notification;
-
-import android.app.Notification;
-import android.content.Context;
-import android.view.LayoutInflater;
-import android.view.ViewGroup;
-
-import com.android.systemui.R;
-
-/**
- * A class managing {@link HybridNotificationView} views
- */
-public class HybridNotificationViewManager {
-
-    private final Context mContext;
-    private ViewGroup mParent;
-    private String mDivider;
-
-    public HybridNotificationViewManager(Context ctx, ViewGroup parent) {
-        mContext = ctx;
-        mParent = parent;
-        mDivider = " • ";
-    }
-
-    private HybridNotificationView inflateHybridView() {
-        LayoutInflater inflater = mContext.getSystemService(LayoutInflater.class);
-        HybridNotificationView hybrid = (HybridNotificationView) inflater.inflate(
-                R.layout.hybrid_notification, mParent, false);
-        mParent.addView(hybrid);
-        return hybrid;
-    }
-
-    public HybridNotificationView bindFromNotification(HybridNotificationView reusableView,
-            Notification notification) {
-        if (reusableView == null) {
-            reusableView = inflateHybridView();
-        }
-        CharSequence titleText = resolveTitle(notification);
-        CharSequence contentText = resolveText(notification);
-        reusableView.bind(titleText, contentText);
-        return reusableView;
-    }
-
-    private CharSequence resolveText(Notification notification) {
-        CharSequence contentText = notification.extras.getCharSequence(Notification.EXTRA_TEXT);
-        if (contentText == null) {
-            contentText = notification.extras.getCharSequence(Notification.EXTRA_BIG_TEXT);
-        }
-        return contentText;
-    }
-
-    private CharSequence resolveTitle(Notification notification) {
-        CharSequence titleText = notification.extras.getCharSequence(Notification.EXTRA_TITLE);
-        if (titleText == null) {
-            titleText = notification.extras.getCharSequence(Notification.EXTRA_TITLE_BIG);
-        }
-        return titleText;
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java
index 6ef61ec..844a2c4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java
@@ -17,15 +17,19 @@
 package com.android.systemui.statusbar.notification;
 
 import android.graphics.Color;
+import android.view.View;
 import android.widget.ImageView;
 
 import com.android.internal.util.NotificationColorUtil;
 import com.android.systemui.R;
+import com.android.systemui.statusbar.stack.NotificationChildrenContainer;
 
 /**
  * A util class for various reusable functions
  */
 public class NotificationUtils {
+    private static final int[] sLocationBase = new int[2];
+    private static final int[] sLocationOffset = new int[2];
     public static boolean isGrayscale(ImageView v, NotificationColorUtil colorUtil) {
         Object isGrayscale = v.getTag(R.id.icon_is_grayscale);
         if (isGrayscale != null) {
@@ -47,4 +51,10 @@
                 (int) interpolate(Color.green(startColor), Color.green(endColor), amount),
                 (int) interpolate(Color.blue(startColor), Color.blue(endColor), amount));
     }
+
+    public static float getRelativeYOffset(View offsetView, View baseView) {
+        baseView.getLocationOnScreen(sLocationBase);
+        offsetView.getLocationOnScreen(sLocationOffset);
+        return sLocationOffset[1] - sLocationBase[1];
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java
index 03dd25e3c..8225dab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java
@@ -131,6 +131,9 @@
                     getContext(), v, ContactsContract.Profile.CONTENT_URI,
                     ContactsContract.QuickContact.MODE_LARGE, null);
             getContext().startActivityAsUser(intent, new UserHandle(UserHandle.USER_CURRENT));
+            if (mQsPanel != null) {
+                mQsPanel.getHost().collapsePanels();
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java
index 54959d9..f7a6b271 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java
@@ -35,7 +35,7 @@
     private final HashMap<String, NotificationGroup> mGroupMap = new HashMap<>();
     private OnGroupChangeListener mListener;
     private int mBarState = -1;
-    private HashMap<String, StatusBarNotification> mHeadsUpedEntries = new HashMap<>();
+    private HashMap<String, StatusBarNotification> mIsolatedEntries = new HashMap<>();
 
     public void setOnGroupChangeListener(OnGroupChangeListener listener) {
         mListener = listener;
@@ -142,7 +142,7 @@
 
     private int getNumberOfIsolatedChildren(String groupKey) {
         int count = 0;
-        for (StatusBarNotification sbn : mHeadsUpedEntries.values()) {
+        for (StatusBarNotification sbn : mIsolatedEntries.values()) {
             if (sbn.getGroupKey().equals(groupKey) && isIsolated(sbn)) {
                 count++;
             }
@@ -156,8 +156,8 @@
             onEntryRemovedInternal(entry, oldNotification);
         }
         onEntryAdded(entry);
-        if (mHeadsUpedEntries.containsKey(entry.key)) {
-            mHeadsUpedEntries.put(entry.key, entry.notification);
+        if (isIsolated(entry.notification)) {
+            mIsolatedEntries.put(entry.key, entry.notification);
             String oldKey = oldNotification.getGroupKey();
             String newKey = entry.notification.getGroupKey();
             if (!oldKey.equals(newKey)) {
@@ -269,8 +269,7 @@
     }
 
     private boolean isIsolated(StatusBarNotification sbn) {
-        return mHeadsUpedEntries.containsKey(sbn.getKey())
-                && sbn.getNotification().isGroupChild();
+        return mIsolatedEntries.containsKey(sbn.getKey());
     }
 
     private boolean isGroupSummary(StatusBarNotification sbn) {
@@ -309,13 +308,12 @@
     public void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) {
         final StatusBarNotification sbn = entry.notification;
         if (entry.row.isHeadsUp()) {
-            final boolean groupChild = sbn.getNotification().isGroupChild();
-            if (groupChild) {
+            if (shouldIsolate(sbn)) {
                 // We will be isolated now, so lets update the groups
                 onEntryRemovedInternal(entry, entry.notification);
-            }
-            mHeadsUpedEntries.put(sbn.getKey(), sbn);
-            if (groupChild) {
+
+                mIsolatedEntries.put(sbn.getKey(), sbn);
+
                 onEntryAdded(entry);
                 // We also need to update the suppression of the old group, because this call comes
                 // even before the groupManager knows about the notification at all.
@@ -325,21 +323,32 @@
                 mListener.onGroupsChanged();
             }
         } else {
-            if (mHeadsUpedEntries.containsKey(sbn.getKey())) {
-                boolean isolatedBefore = isIsolated(sbn);
-                if (isolatedBefore) {
-                    // not isolated anymore, we need to update the groups
-                    onEntryRemovedInternal(entry, entry.notification);
-                }
-                mHeadsUpedEntries.remove(sbn.getKey());
-                if (isolatedBefore) {
-                    onEntryAdded(entry);
-                    mListener.onGroupsChanged();
-                }
+            if (mIsolatedEntries.containsKey(sbn.getKey())) {
+                // not isolated anymore, we need to update the groups
+                onEntryRemovedInternal(entry, entry.notification);
+                mIsolatedEntries.remove(sbn.getKey());
+                onEntryAdded(entry);
+                mListener.onGroupsChanged();
             }
         }
     }
 
+    private boolean shouldIsolate(StatusBarNotification sbn) {
+        NotificationGroup notificationGroup = mGroupMap.get(sbn.getGroupKey());
+        return sbn.getNotification().isGroupChild()
+                && (sbn.getNotification().fullScreenIntent != null
+                        || notificationGroup == null
+                        || !notificationGroup.expanded
+                        || isGroupNotFullyVisible(notificationGroup));
+    }
+
+    private boolean isGroupNotFullyVisible(NotificationGroup notificationGroup) {
+        return notificationGroup.summary == null
+                || notificationGroup.summary.row.getClipTopOptimization() > 0
+                || notificationGroup.summary.row.getClipTopAmount() > 0
+                || notificationGroup.summary.row.getTranslationY() < 0;
+    }
+
     public static class NotificationGroup {
         public final HashSet<NotificationData.Entry> children = new HashSet<>();
         public NotificationData.Entry summary;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java
index 176788b..dc567fc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java
@@ -22,13 +22,14 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.TextView;
 
 import com.android.systemui.R;
 import com.android.systemui.ViewInvertHelper;
 import com.android.systemui.statusbar.CrossFadeHelper;
 import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.notification.HybridGroupManager;
 import com.android.systemui.statusbar.notification.HybridNotificationView;
-import com.android.systemui.statusbar.notification.HybridNotificationViewManager;
 import com.android.systemui.statusbar.notification.NotificationUtils;
 import com.android.systemui.statusbar.phone.NotificationPanelView;
 
@@ -46,18 +47,22 @@
 
     private final List<View> mDividers = new ArrayList<>();
     private final List<ExpandableNotificationRow> mChildren = new ArrayList<>();
+    private final HybridGroupManager mHybridGroupManager;
     private int mChildPadding;
     private int mDividerHeight;
     private int mMaxNotificationHeight;
     private int mNotificationHeaderHeight;
     private int mNotificatonTopPadding;
     private float mCollapsedBottompadding;
+    private ViewInvertHelper mOverflowInvertHelper;
     private boolean mChildrenExpanded;
     private ExpandableNotificationRow mNotificationParent;
+    private TextView mOverflowNumber;
+    private ViewState mGroupOverFlowState;
     private int mRealHeight;
-    private int mLayoutDirection = LAYOUT_DIRECTION_UNDEFINED;
     private boolean mUserLocked;
     private int mActualHeight;
+    private boolean mNeverAppliedGroupState;
 
     public NotificationChildrenContainer(Context context) {
         this(context, null);
@@ -75,6 +80,7 @@
             int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
         initDimens();
+        mHybridGroupManager = new HybridGroupManager(getContext(), this);
     }
 
     private void initDimens() {
@@ -100,9 +106,13 @@
             if (child.getVisibility() == View.GONE) {
                 continue;
             }
-            child.layout(0, 0, getWidth(), child.getMeasuredHeight());
+            child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
             mDividers.get(i).layout(0, 0, getWidth(), mDividerHeight);
         }
+        if (mOverflowNumber != null) {
+            mOverflowNumber.layout(getWidth() - mOverflowNumber.getMeasuredWidth(), 0, getWidth(),
+                    mOverflowNumber.getMeasuredHeight());
+        }
     }
 
     @Override
@@ -116,11 +126,20 @@
             ownMaxHeight = Math.min(ownMaxHeight, size);
         }
         int newHeightSpec = MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.AT_MOST);
+        int width = MeasureSpec.getSize(widthMeasureSpec);
+        if (mOverflowNumber != null) {
+            mOverflowNumber.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST),
+                    newHeightSpec);
+        }
         int dividerHeightSpec = MeasureSpec.makeMeasureSpec(mDividerHeight, MeasureSpec.EXACTLY);
         int height = mNotificationHeaderHeight + mNotificatonTopPadding;
         int childCount = Math.min(mChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED);
+        int collapsedChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
+        int overflowIndex = childCount > collapsedChildren ? collapsedChildren - 1 : -1;
         for (int i = 0; i < childCount; i++) {
-            View child = mChildren.get(i);
+            ExpandableNotificationRow child = mChildren.get(i);
+            boolean isOverflow = i == overflowIndex;
+            child.setSingleLineWidthIndention(isOverflow ? mOverflowNumber.getMeasuredWidth() : 0);
             child.measure(widthMeasureSpec, newHeightSpec);
             height += child.getMeasuredHeight();
 
@@ -129,7 +148,6 @@
             divider.measure(widthMeasureSpec, dividerHeightSpec);
             height += mDividerHeight;
         }
-        int width = MeasureSpec.getSize(widthMeasureSpec);
         mRealHeight = height;
         if (heightMode != MeasureSpec.UNSPECIFIED) {
             height = Math.min(height, size);
@@ -158,6 +176,8 @@
         View divider = inflateDivider();
         addView(divider);
         mDividers.add(newIndex, divider);
+
+        updateGroupOverflow();
     }
 
     public void removeNotification(ExpandableNotificationRow row) {
@@ -177,15 +197,45 @@
 
         row.setSystemChildExpanded(false);
         row.setUserLocked(false);
+        updateGroupOverflow();
+    }
+
+    public void updateGroupOverflow() {
+        int childCount = mChildren.size();
+        int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
+        if (childCount > maxAllowedVisibleChildren) {
+            mOverflowNumber = mHybridGroupManager.bindOverflowNumber(
+                    mOverflowNumber, childCount - maxAllowedVisibleChildren);
+            if (mOverflowInvertHelper == null) {
+                mOverflowInvertHelper= new ViewInvertHelper(mOverflowNumber,
+                        NotificationPanelView.DOZE_ANIMATION_DURATION);
+            }
+            if (mGroupOverFlowState == null) {
+                mGroupOverFlowState = new ViewState();
+                mNeverAppliedGroupState = true;
+            }
+        } else if (mOverflowNumber != null) {
+            removeView(mOverflowNumber);
+            if (isShown()) {
+                final View removedOverflowNumber = mOverflowNumber;
+                addTransientView(removedOverflowNumber, getTransientViewCount());
+                CrossFadeHelper.fadeOut(removedOverflowNumber, new Runnable() {
+                    @Override
+                    public void run() {
+                        removeTransientView(removedOverflowNumber);
+                    }
+                });
+            }
+            mOverflowNumber = null;
+            mOverflowInvertHelper = null;
+            mGroupOverFlowState = null;
+        }
     }
 
     @Override
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
-        int layoutDirection = getLayoutDirection();
-        if (layoutDirection != mLayoutDirection) {
-            mLayoutDirection = layoutDirection;
-        }
+        updateGroupOverflow();
     }
 
     private View inflateDivider() {
@@ -253,7 +303,7 @@
         boolean firstChild = true;
         float expandFactor = 0;
         if (mUserLocked) {
-            expandFactor = getChildExpandFraction();
+            expandFactor = getGroupExpandFraction();
         }
         for (int i = 0; i < childCount; i++) {
             if (visibleChildren >= maxAllowedVisibleChildren) {
@@ -304,9 +354,11 @@
         boolean firstChild = true;
         int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren();
         int lastVisibleIndex = maxAllowedVisibleChildren - 1;
+        int firstOverflowIndex = lastVisibleIndex + 1;
         float expandFactor = 0;
         if (mUserLocked) {
-            expandFactor = getChildExpandFraction();
+            expandFactor = getGroupExpandFraction();
+            firstOverflowIndex = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
         }
         for (int i = 0; i < childCount; i++) {
             ExpandableNotificationRow child = mChildren.get(i);
@@ -339,11 +391,39 @@
             childState.belowSpeedBump = parentState.belowSpeedBump;
             childState.clipTopAmount = 0;
             childState.topOverLap = 0;
-            boolean visible = i <= lastVisibleIndex;
-            childState.alpha = visible ? 1 : 0;
+            childState.alpha = 0;
+            if (i < firstOverflowIndex) {
+                childState.alpha = 1;
+            } else if (expandFactor == 1.0f && i <= lastVisibleIndex) {
+                childState.alpha = (mActualHeight - childState.yTranslation) / childState.height;
+                childState.alpha = Math.max(0.0f, Math.min(1.0f, childState.alpha));
+            }
             childState.location = parentState.location;
             yPosition += intrinsicHeight;
         }
+        if (mOverflowNumber != null) {
+            ExpandableNotificationRow overflowView = mChildren.get(Math.min(
+                    getMaxAllowedVisibleChildren(true /* likeCollpased */), childCount) - 1);
+            mGroupOverFlowState.copyFrom(resultState.getViewStateForView(overflowView));
+            if (!mChildrenExpanded) {
+                if (mUserLocked) {
+                    HybridNotificationView singleLineView = overflowView.getSingleLineView();
+                    View mirrorView = singleLineView.getTextView();
+                    if (mirrorView.getVisibility() == GONE) {
+                        mirrorView = singleLineView.getTitleView();
+                    }
+                    if (mirrorView.getVisibility() == GONE) {
+                        mirrorView = singleLineView;
+                    }
+                    mGroupOverFlowState.yTranslation += NotificationUtils.getRelativeYOffset(
+                            mirrorView, overflowView);
+                    mGroupOverFlowState.alpha = mirrorView.getAlpha();
+                }
+            } else {
+                mGroupOverFlowState.yTranslation += mNotificationHeaderHeight;
+                mGroupOverFlowState.alpha = 0.0f;
+            }
+        }
     }
 
     private int getMaxAllowedVisibleChildren() {
@@ -354,7 +434,8 @@
         if (!likeCollapsed && (mChildrenExpanded || mNotificationParent.isUserLocked())) {
             return NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED;
         }
-        if (mNotificationParent.isExpanded() || mNotificationParent.isHeadsUp()) {
+        if (!mNotificationParent.isOnKeyguard()
+                && (mNotificationParent.isExpanded() || mNotificationParent.isHeadsUp())) {
             return NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED;
         }
         return NUMBER_OF_CHILDREN_WHEN_COLLAPSED;
@@ -363,7 +444,10 @@
     public void applyState(StackScrollState state) {
         int childCount = mChildren.size();
         ViewState tmpState = new ViewState();
-        float expandFraction = getChildExpandFraction();
+        float expandFraction = 0.0f;
+        if (mUserLocked) {
+            expandFraction = getGroupExpandFraction();
+        }
         for (int i = 0; i < childCount; i++) {
             ExpandableNotificationRow child = mChildren.get(i);
             StackViewState viewState = state.getViewStateForView(child);
@@ -375,13 +459,18 @@
             tmpState.yTranslation = viewState.yTranslation - mDividerHeight;
             float alpha = mChildrenExpanded && viewState.alpha != 0 ? 0.5f : 0;
             if (mUserLocked && viewState.alpha != 0) {
-                alpha = NotificationUtils.interpolate(0, 0.5f, expandFraction);
+                alpha = NotificationUtils.interpolate(0, 0.5f,
+                        Math.min(viewState.alpha, expandFraction));
             }
             tmpState.alpha = alpha;
             state.applyViewState(divider, tmpState);
             // There is no fake shadow to be drawn on the children
             child.setFakeShadowIntensity(0.0f, 0.0f, 0, 0);
         }
+        if (mOverflowNumber != null) {
+            state.applyViewState(mOverflowNumber, mGroupOverFlowState);
+            mNeverAppliedGroupState = false;
+        }
     }
 
     /**
@@ -399,7 +488,7 @@
             long baseDelay, long duration) {
         int childCount = mChildren.size();
         ViewState tmpState = new ViewState();
-        float expandFraction = getChildExpandFraction();
+        float expandFraction = getGroupExpandFraction();
         for (int i = childCount - 1; i >= 0; i--) {
             ExpandableNotificationRow child = mChildren.get(i);
             StackViewState viewState = state.getViewStateForView(child);
@@ -411,13 +500,25 @@
             tmpState.yTranslation = viewState.yTranslation - mDividerHeight;
             float alpha = mChildrenExpanded && viewState.alpha != 0 ? 0.5f : 0;
             if (mUserLocked && viewState.alpha != 0) {
-                alpha = NotificationUtils.interpolate(0, 0.5f, expandFraction);
+                alpha = NotificationUtils.interpolate(0, 0.5f,
+                        Math.min(viewState.alpha, expandFraction));
             }
             tmpState.alpha = alpha;
             stateAnimator.startViewAnimations(divider, tmpState, baseDelay, duration);
             // There is no fake shadow to be drawn on the children
             child.setFakeShadowIntensity(0.0f, 0.0f, 0, 0);
         }
+        if (mOverflowNumber != null) {
+            if (mNeverAppliedGroupState) {
+                float alpha = mGroupOverFlowState.alpha;
+                mGroupOverFlowState.alpha = 0;
+                state.applyViewState(mOverflowNumber, mGroupOverFlowState);
+                mGroupOverFlowState.alpha = alpha;
+                mNeverAppliedGroupState = false;
+            }
+            stateAnimator.startViewAnimations(mOverflowNumber, mGroupOverFlowState,
+                    baseDelay, duration);
+        }
     }
 
     public ExpandableNotificationRow getViewAtPosition(float y) {
@@ -470,44 +571,49 @@
             return;
         }
         mActualHeight = actualHeight;
-        float fraction = getChildExpandFraction();
+        float fraction = getGroupExpandFraction();
+        int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */);
         int childCount = mChildren.size();
         for (int i = 0; i < childCount; i++) {
             ExpandableNotificationRow child = mChildren.get(i);
             float childHeight = child.isExpanded(true /* allowOnKeyguard */)
                     ? child.getMaxExpandHeight()
                     : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */);
-            float singleLineHeight = child.getShowingLayout().getMinHeight(
-                    false /* likeGroupExpanded */);
-            child.setActualHeight((int) NotificationUtils.interpolate(singleLineHeight, childHeight,
-                    fraction), false);
+            if (i < maxAllowedVisibleChildren) {
+                float singleLineHeight = child.getShowingLayout().getMinHeight(
+                        false /* likeGroupExpanded */);
+                child.setActualHeight((int) NotificationUtils.interpolate(singleLineHeight,
+                        childHeight, fraction), false);
+            } else {
+                child.setActualHeight((int) childHeight, false);
+            }
         }
     }
 
-    public float getChildExpandFraction() {
-        int allChildrenVisibleHeight = getChildrenExpandStartHeight();
-        int maxContentHeight = getMaxContentHeight();
-        float factor = (mActualHeight - allChildrenVisibleHeight)
-                / (float) (maxContentHeight - allChildrenVisibleHeight);
+    public float getGroupExpandFraction() {
+        int visibleChildrenExpandedHeight = getVisibleChildrenExpandHeight();
+        int minExpandHeight = getMinExpandHeight();
+        float factor = (mActualHeight - minExpandHeight)
+                / (float) (visibleChildrenExpandedHeight - minExpandHeight);
         return Math.max(0.0f, Math.min(1.0f, factor));
     }
 
-    private int getChildrenExpandStartHeight() {
-        int intrinsicHeight = mNotificationHeaderHeight;
+    private int getVisibleChildrenExpandHeight() {
+        int intrinsicHeight = mNotificationHeaderHeight + mNotificatonTopPadding + mDividerHeight;
         int visibleChildren = 0;
         int childCount = mChildren.size();
+        int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */);
         for (int i = 0; i < childCount; i++) {
-            if (visibleChildren >= NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED) {
+            if (visibleChildren >= maxAllowedVisibleChildren) {
                 break;
             }
             ExpandableNotificationRow child = mChildren.get(i);
-            intrinsicHeight += child.getMinHeight();
+            float childHeight = child.isExpanded(true /* allowOnKeyguard */)
+                    ? child.getMaxExpandHeight()
+                    : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */);
+            intrinsicHeight += childHeight;
             visibleChildren++;
         }
-        if (visibleChildren > 0) {
-            intrinsicHeight += (visibleChildren - 1) * mChildPadding;
-        }
-        intrinsicHeight += mCollapsedBottompadding;
         return intrinsicHeight;
     }
 
@@ -515,9 +621,8 @@
         return getIntrinsicHeight(NUMBER_OF_CHILDREN_WHEN_COLLAPSED);
     }
 
-    public int getMinExpandHeight(boolean onKeyguard) {
-        int maxAllowedVisibleChildren = onKeyguard ? NUMBER_OF_CHILDREN_WHEN_COLLAPSED
-                : getMaxAllowedVisibleChildren(true /* forceCollapsed */);
+    public int getMinExpandHeight() {
+        int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */);
         int minExpandHeight = mNotificationHeaderHeight;
         int visibleChildren = 0;
         boolean firstChild = true;
@@ -539,6 +644,12 @@
         return minExpandHeight;
     }
 
+    public void setDark(boolean dark, boolean fade, long delay) {
+        if (mOverflowNumber != null) {
+            mOverflowInvertHelper.setInverted(dark, fade, delay);
+        }
+    }
+
     public void reInflateViews() {
         initDimens();
         for (int i = 0; i < mDividers.size(); i++) {
@@ -559,4 +670,9 @@
             child.setUserLocked(userLocked);
         }
     }
+
+    public void onNotificationUpdated() {
+        mHybridGroupManager.setOverflowNumberColor(mOverflowNumber,
+                mNotificationParent.getNotificationColor());
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java
index cf4802d..dba5bbd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java
@@ -972,10 +972,10 @@
     }
 
     /**
-     * Get the end value of the height animation running on a view or the actualHeight
+     * Get the end value of the yTranslation animation running on a view or the yTranslation
      * if no animation is running.
      */
-    public static float getFinalTranslationY(ExpandableView view) {
+    public static float getFinalTranslationY(View view) {
         if (view == null) {
             return 0;
         }
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 274a73f..0f23fde 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -1852,16 +1852,11 @@
         }
 
         private boolean checkPolicyAccess(String pkg) {
-            if (PackageManager.PERMISSION_GRANTED == getContext().checkCallingPermission(
-                    android.Manifest.permission.MANAGE_NOTIFICATIONS)) {
+            if (PackageManager.PERMISSION_GRANTED == ActivityManager.checkComponentPermission(
+                    android.Manifest.permission.MANAGE_NOTIFICATIONS, Binder.getCallingUid(),
+                    -1, true)) {
                 return true;
             }
-            if (mAudioManagerInternal != null) {
-                final int vcuid = mAudioManagerInternal.getVolumeControllerUid();
-                if (vcuid > 0 && Binder.getCallingUid() == vcuid) {
-                    return true;
-                }
-            }
             return checkPackagePolicyAccess(pkg) || mListeners.isComponentEnabledForPackage(pkg);
         }
 
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index c2e0992..9335116 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -10451,10 +10451,10 @@
 
     void startCleaningPackages() {
         // reader
+        if (!isExternalMediaAvailable()) {
+            return;
+        }
         synchronized (mPackages) {
-            if (!isExternalMediaAvailable()) {
-                return;
-            }
             if (mSettings.mPackagesToBeCleaned.isEmpty()) {
                 return;
             }
diff --git a/services/core/java/com/android/server/pm/ShortcutLauncher.java b/services/core/java/com/android/server/pm/ShortcutLauncher.java
new file mode 100644
index 0000000..f1920c7
--- /dev/null
+++ b/services/core/java/com/android/server/pm/ShortcutLauncher.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2016 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.server.pm;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.content.pm.ShortcutInfo;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+
+/**
+ * Launcher information used by {@link ShortcutService}.
+ */
+class ShortcutLauncher {
+    private static final String TAG = ShortcutService.TAG;
+
+    static final String TAG_ROOT = "launcher-pins";
+
+    private static final String TAG_PACKAGE = "package";
+    private static final String TAG_PIN = "pin";
+
+    private static final String ATTR_VALUE = "value";
+    private static final String ATTR_PACKAGE_NAME = "package-name";
+
+    @UserIdInt
+    final int mUserId;
+
+    @NonNull
+    final String mPackageName;
+
+    /**
+     * Package name -> IDs.
+     */
+    final private ArrayMap<String, ArraySet<String>> mPinnedShortcuts = new ArrayMap<>();
+
+    ShortcutLauncher(@UserIdInt int userId, @NonNull String packageName) {
+        mUserId = userId;
+        mPackageName = packageName;
+    }
+
+    public void pinShortcuts(@NonNull ShortcutService s, @NonNull String packageName,
+            @NonNull List<String> ids) {
+        final int idSize = ids.size();
+        if (idSize == 0) {
+            mPinnedShortcuts.remove(packageName);
+        } else {
+            final ArraySet<String> prevSet = mPinnedShortcuts.get(packageName);
+
+            // Pin shortcuts.  Make sure only pin the ones that were visible to the caller.
+            // i.e. a non-dynamic, pinned shortcut by *other launchers* shouldn't be pinned here.
+
+            final ShortcutPackage packageShortcuts =
+                    s.getPackageShortcutsLocked(packageName, mUserId);
+            final ArraySet<String> newSet = new ArraySet<>();
+
+            for (int i = 0; i < idSize; i++) {
+                final String id = ids.get(i);
+                final ShortcutInfo si = packageShortcuts.findShortcutById(id);
+                if (si == null) {
+                    continue;
+                }
+                if (si.isDynamic() || (prevSet != null && prevSet.contains(id))) {
+                    newSet.add(id);
+                }
+            }
+            mPinnedShortcuts.put(packageName, newSet);
+        }
+        s.getPackageShortcutsLocked(packageName, mUserId).refreshPinnedFlags(s);
+    }
+
+    /**
+     * Return the pinned shortcut IDs for the publisher package.
+     */
+    public ArraySet<String> getPinnedShortcutIds(@NonNull String packageName) {
+        return mPinnedShortcuts.get(packageName);
+    }
+
+    boolean cleanUpPackage(String packageName) {
+        return mPinnedShortcuts.remove(packageName) != null;
+    }
+
+    /**
+     * Persist.
+     */
+    public void saveToXml(XmlSerializer out) throws IOException {
+        final int size = mPinnedShortcuts.size();
+        if (size == 0) {
+            return; // Nothing to write.
+        }
+
+        out.startTag(null, TAG_ROOT);
+        ShortcutService.writeAttr(out, ATTR_PACKAGE_NAME,
+                mPackageName);
+
+        for (int i = 0; i < size; i++) {
+            out.startTag(null, TAG_PACKAGE);
+            ShortcutService.writeAttr(out, ATTR_PACKAGE_NAME,
+                    mPinnedShortcuts.keyAt(i));
+
+            final ArraySet<String> ids = mPinnedShortcuts.valueAt(i);
+            final int idSize = ids.size();
+            for (int j = 0; j < idSize; j++) {
+                ShortcutService.writeTagValue(out, TAG_PIN, ids.valueAt(j));
+            }
+            out.endTag(null, TAG_PACKAGE);
+        }
+
+        out.endTag(null, TAG_ROOT);
+    }
+
+    /**
+     * Load.
+     */
+    public static ShortcutLauncher loadFromXml(XmlPullParser parser, int userId)
+            throws IOException, XmlPullParserException {
+        final String launcherPackageName = ShortcutService.parseStringAttribute(parser,
+                ATTR_PACKAGE_NAME);
+
+        final ShortcutLauncher ret = new ShortcutLauncher(userId, launcherPackageName);
+
+        ArraySet<String> ids = null;
+        final int outerDepth = parser.getDepth();
+        int type;
+        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+            if (type != XmlPullParser.START_TAG) {
+                continue;
+            }
+            final int depth = parser.getDepth();
+            final String tag = parser.getName();
+            switch (tag) {
+                case TAG_PACKAGE: {
+                    final String packageName = ShortcutService.parseStringAttribute(parser,
+                            ATTR_PACKAGE_NAME);
+                    ids = new ArraySet<>();
+                    ret.mPinnedShortcuts.put(packageName, ids);
+                    continue;
+                }
+                case TAG_PIN: {
+                    ids.add(ShortcutService.parseStringAttribute(parser,
+                            ATTR_VALUE));
+                    continue;
+                }
+            }
+            throw ShortcutService.throwForInvalidTag(depth, tag);
+        }
+        return ret;
+    }
+
+    public void dump(@NonNull ShortcutService s, @NonNull PrintWriter pw, @NonNull String prefix) {
+        pw.println();
+
+        pw.print(prefix);
+        pw.print("Launcher: ");
+        pw.print(mPackageName);
+        pw.println();
+
+        final int size = mPinnedShortcuts.size();
+        for (int i = 0; i < size; i++) {
+            pw.println();
+
+            pw.print(prefix);
+            pw.print("  ");
+            pw.print("Package: ");
+            pw.println(mPinnedShortcuts.keyAt(i));
+
+            final ArraySet<String> ids = mPinnedShortcuts.valueAt(i);
+            final int idSize = ids.size();
+
+            for (int j = 0; j < idSize; j++) {
+                pw.print(prefix);
+                pw.print("    ");
+                pw.print(ids.valueAt(j));
+                pw.println();
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
new file mode 100644
index 0000000..d614251
--- /dev/null
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -0,0 +1,520 @@
+/*
+ * Copyright (C) 2016 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.server.pm;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ShortcutInfo;
+import android.os.PersistableBundle;
+import android.text.format.Formatter;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Slog;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Predicate;
+
+/**
+ * Package information used by {@link ShortcutService}.
+ */
+class ShortcutPackage {
+    private static final String TAG = ShortcutService.TAG;
+
+    static final String TAG_ROOT = "package";
+    private static final String TAG_INTENT_EXTRAS = "intent-extras";
+    private static final String TAG_EXTRAS = "extras";
+    private static final String TAG_SHORTCUT = "shortcut";
+
+    private static final String ATTR_NAME = "name";
+    private static final String ATTR_DYNAMIC_COUNT = "dynamic-count";
+    private static final String ATTR_CALL_COUNT = "call-count";
+    private static final String ATTR_LAST_RESET = "last-reset";
+    private static final String ATTR_ID = "id";
+    private static final String ATTR_ACTIVITY = "activity";
+    private static final String ATTR_TITLE = "title";
+    private static final String ATTR_INTENT = "intent";
+    private static final String ATTR_WEIGHT = "weight";
+    private static final String ATTR_TIMESTAMP = "timestamp";
+    private static final String ATTR_FLAGS = "flags";
+    private static final String ATTR_ICON_RES = "icon-res";
+    private static final String ATTR_BITMAP_PATH = "bitmap-path";
+
+    @UserIdInt
+    final int mUserId;
+
+    @NonNull
+    final String mPackageName;
+
+    /**
+     * All the shortcuts from the package, keyed on IDs.
+     */
+    final private ArrayMap<String, ShortcutInfo> mShortcuts = new ArrayMap<>();
+
+    /**
+     * # of dynamic shortcuts.
+     */
+    private int mDynamicShortcutCount = 0;
+
+    /**
+     * # of times the package has called rate-limited APIs.
+     */
+    private int mApiCallCount;
+
+    /**
+     * When {@link #mApiCallCount} was reset last time.
+     */
+    private long mLastResetTime;
+
+    ShortcutPackage(int userId, String packageName) {
+        mUserId = userId;
+        mPackageName = packageName;
+    }
+
+    @Nullable
+    public ShortcutInfo findShortcutById(String id) {
+        return mShortcuts.get(id);
+    }
+
+    private ShortcutInfo deleteShortcut(@NonNull ShortcutService s,
+            @NonNull String id) {
+        final ShortcutInfo shortcut = mShortcuts.remove(id);
+        if (shortcut != null) {
+            s.removeIcon(mUserId, shortcut);
+            shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED);
+        }
+        return shortcut;
+    }
+
+    void addShortcut(@NonNull ShortcutService s, @NonNull ShortcutInfo newShortcut) {
+        deleteShortcut(s, newShortcut.getId());
+        s.saveIconAndFixUpShortcut(mUserId, newShortcut);
+        mShortcuts.put(newShortcut.getId(), newShortcut);
+    }
+
+    /**
+     * Add a shortcut, or update one with the same ID, with taking over existing flags.
+     *
+     * It checks the max number of dynamic shortcuts.
+     */
+    public void addDynamicShortcut(@NonNull ShortcutService s,
+            @NonNull ShortcutInfo newShortcut) {
+        newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC);
+
+        final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId());
+
+        final boolean wasPinned;
+        final int newDynamicCount;
+
+        if (oldShortcut == null) {
+            wasPinned = false;
+            newDynamicCount = mDynamicShortcutCount + 1; // adding a dynamic shortcut.
+        } else {
+            wasPinned = oldShortcut.isPinned();
+            if (oldShortcut.isDynamic()) {
+                newDynamicCount = mDynamicShortcutCount; // not adding a dynamic shortcut.
+            } else {
+                newDynamicCount = mDynamicShortcutCount + 1; // adding a dynamic shortcut.
+            }
+        }
+
+        // Make sure there's still room.
+        s.enforceMaxDynamicShortcuts(newDynamicCount);
+
+        // Okay, make it dynamic and add.
+        if (wasPinned) {
+            newShortcut.addFlags(ShortcutInfo.FLAG_PINNED);
+        }
+
+        addShortcut(s, newShortcut);
+        mDynamicShortcutCount = newDynamicCount;
+    }
+
+    /**
+     * Remove all shortcuts that aren't pinned nor dynamic.
+     */
+    private void removeOrphans(@NonNull ShortcutService s) {
+        ArrayList<String> removeList = null; // Lazily initialize.
+
+        for (int i = mShortcuts.size() - 1; i >= 0; i--) {
+            final ShortcutInfo si = mShortcuts.valueAt(i);
+
+            if (si.isPinned() || si.isDynamic()) continue;
+
+            if (removeList == null) {
+                removeList = new ArrayList<>();
+            }
+            removeList.add(si.getId());
+        }
+        if (removeList != null) {
+            for (int i = removeList.size() - 1; i >= 0; i--) {
+                deleteShortcut(s, removeList.get(i));
+            }
+        }
+    }
+
+    /**
+     * Remove all dynamic shortcuts.
+     */
+    public void deleteAllDynamicShortcuts(@NonNull ShortcutService s) {
+        for (int i = mShortcuts.size() - 1; i >= 0; i--) {
+            mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_DYNAMIC);
+        }
+        removeOrphans(s);
+        mDynamicShortcutCount = 0;
+    }
+
+    /**
+     * Remove a dynamic shortcut by ID.
+     */
+    public void deleteDynamicWithId(@NonNull ShortcutService s, @NonNull String shortcutId) {
+        final ShortcutInfo oldShortcut = mShortcuts.get(shortcutId);
+
+        if (oldShortcut == null) {
+            return;
+        }
+        if (oldShortcut.isDynamic()) {
+            mDynamicShortcutCount--;
+        }
+        if (oldShortcut.isPinned()) {
+            oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC);
+        } else {
+            deleteShortcut(s, shortcutId);
+        }
+    }
+
+    /**
+     * Called after a launcher updates the pinned set.  For each shortcut in this package,
+     * set FLAG_PINNED if any launcher has pinned it.  Otherwise, clear it.
+     *
+     * <p>Then remove all shortcuts that are not dynamic and no longer pinned either.
+     */
+    public void refreshPinnedFlags(@NonNull ShortcutService s) {
+        // First, un-pin all shortcuts
+        for (int i = mShortcuts.size() - 1; i >= 0; i--) {
+            mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_PINNED);
+        }
+
+        // Then, for the pinned set for each launcher, set the pin flag one by one.
+        final ArrayMap<String, ShortcutLauncher> launchers =
+                s.getUserShortcutsLocked(mUserId).getLaunchers();
+
+        for (int l = launchers.size() - 1; l >= 0; l--) {
+            final ShortcutLauncher launcherShortcuts = launchers.valueAt(l);
+            final ArraySet<String> pinned = launcherShortcuts.getPinnedShortcutIds(mPackageName);
+
+            if (pinned == null || pinned.size() == 0) {
+                continue;
+            }
+            for (int i = pinned.size() - 1; i >= 0; i--) {
+                final ShortcutInfo si = mShortcuts.get(pinned.valueAt(i));
+                if (si == null) {
+                    s.wtf("Shortcut not found");
+                } else {
+                    si.addFlags(ShortcutInfo.FLAG_PINNED);
+                }
+            }
+        }
+
+        // Lastly, remove the ones that are no longer pinned nor dynamic.
+        removeOrphans(s);
+    }
+
+    /**
+     * Number of calls that the caller has made, since the last reset.
+     */
+    public int getApiCallCount(@NonNull ShortcutService s) {
+        final long last = s.getLastResetTimeLocked();
+
+        final long now = s.injectCurrentTimeMillis();
+        if (ShortcutService.isClockValid(now) && mLastResetTime > now) {
+            Slog.w(TAG, "Clock rewound");
+            // Clock rewound.
+            mLastResetTime = now;
+            mApiCallCount = 0;
+            return mApiCallCount;
+        }
+
+        // If not reset yet, then reset.
+        if (mLastResetTime < last) {
+            if (ShortcutService.DEBUG) {
+                Slog.d(TAG, String.format("My last reset=%d, now=%d, last=%d: resetting",
+                        mLastResetTime, now, last));
+            }
+            mApiCallCount = 0;
+            mLastResetTime = last;
+        }
+        return mApiCallCount;
+    }
+
+    /**
+     * If the caller app hasn't been throttled yet, increment {@link #mApiCallCount}
+     * and return true.  Otherwise just return false.
+     */
+    public boolean tryApiCall(@NonNull ShortcutService s) {
+        if (getApiCallCount(s) >= s.mMaxDailyUpdates) {
+            return false;
+        }
+        mApiCallCount++;
+        return true;
+    }
+
+    public void resetRateLimitingForCommandLine() {
+        mApiCallCount = 0;
+        mLastResetTime = 0;
+    }
+
+    /**
+     * Find all shortcuts that match {@code query}.
+     */
+    public void findAll(@NonNull ShortcutService s, @NonNull List<ShortcutInfo> result,
+            @Nullable Predicate<ShortcutInfo> query, int cloneFlag,
+            @Nullable String callingLauncher) {
+
+        // Set of pinned shortcuts by the calling launcher.
+        final ArraySet<String> pinnedByCallerSet = (callingLauncher == null) ? null
+                : s.getLauncherShortcuts(callingLauncher, mUserId)
+                    .getPinnedShortcutIds(mPackageName);
+
+        for (int i = 0; i < mShortcuts.size(); i++) {
+            final ShortcutInfo si = mShortcuts.valueAt(i);
+
+            // If it's called by non-launcher (i.e. publisher, always include -> true.
+            // Otherwise, only include non-dynamic pinned one, if the calling launcher has pinned
+            // it.
+            final boolean isPinnedByCaller = (callingLauncher == null)
+                    || ((pinnedByCallerSet != null) && pinnedByCallerSet.contains(si.getId()));
+            if (!si.isDynamic()) {
+                if (!si.isPinned()) {
+                    s.wtf("Shortcut not pinned here");
+                    continue;
+                }
+                if (!isPinnedByCaller) {
+                    continue;
+                }
+            }
+            final ShortcutInfo clone = si.clone(cloneFlag);
+            // Fix up isPinned for the caller.  Note we need to do it before the "test" callback,
+            // since it may check isPinned.
+            if (!isPinnedByCaller) {
+                clone.clearFlags(ShortcutInfo.FLAG_PINNED);
+            }
+            if (query == null || query.test(clone)) {
+                result.add(clone);
+            }
+        }
+    }
+
+    public void resetThrottling() {
+        mApiCallCount = 0;
+    }
+
+    public void dump(@NonNull ShortcutService s, @NonNull PrintWriter pw, @NonNull String prefix) {
+        pw.println();
+
+        pw.print(prefix);
+        pw.print("Package: ");
+        pw.print(mPackageName);
+        pw.println();
+
+        pw.print(prefix);
+        pw.print("  ");
+        pw.print("Calls: ");
+        pw.print(getApiCallCount(s));
+        pw.println();
+
+        // This should be after getApiCallCount(), which may update it.
+        pw.print(prefix);
+        pw.print("  ");
+        pw.print("Last reset: [");
+        pw.print(mLastResetTime);
+        pw.print("] ");
+        pw.print(s.formatTime(mLastResetTime));
+        pw.println();
+
+        pw.println("      Shortcuts:");
+        long totalBitmapSize = 0;
+        final ArrayMap<String, ShortcutInfo> shortcuts = mShortcuts;
+        final int size = shortcuts.size();
+        for (int i = 0; i < size; i++) {
+            final ShortcutInfo si = shortcuts.valueAt(i);
+            pw.print("        ");
+            pw.println(si.toInsecureString());
+            if (si.getBitmapPath() != null) {
+                final long len = new File(si.getBitmapPath()).length();
+                pw.print("          ");
+                pw.print("bitmap size=");
+                pw.println(len);
+
+                totalBitmapSize += len;
+            }
+        }
+        pw.print(prefix);
+        pw.print("  ");
+        pw.print("Total bitmap size: ");
+        pw.print(totalBitmapSize);
+        pw.print(" (");
+        pw.print(Formatter.formatFileSize(s.mContext, totalBitmapSize));
+        pw.println(")");
+    }
+
+    public void saveToXml(@NonNull XmlSerializer out) throws IOException, XmlPullParserException {
+        final int size = mShortcuts.size();
+
+        if (size == 0 && mApiCallCount == 0) {
+            return; // nothing to write.
+        }
+
+        out.startTag(null, TAG_ROOT);
+
+        ShortcutService.writeAttr(out, ATTR_NAME, mPackageName);
+        ShortcutService.writeAttr(out, ATTR_DYNAMIC_COUNT, mDynamicShortcutCount);
+        ShortcutService.writeAttr(out, ATTR_CALL_COUNT, mApiCallCount);
+        ShortcutService.writeAttr(out, ATTR_LAST_RESET, mLastResetTime);
+
+        for (int j = 0; j < size; j++) {
+            saveShortcut(out, mShortcuts.valueAt(j));
+        }
+
+        out.endTag(null, TAG_ROOT);
+    }
+
+    private static void saveShortcut(XmlSerializer out, ShortcutInfo si)
+            throws IOException, XmlPullParserException {
+        out.startTag(null, TAG_SHORTCUT);
+        ShortcutService.writeAttr(out, ATTR_ID, si.getId());
+        // writeAttr(out, "package", si.getPackageName()); // not needed
+        ShortcutService.writeAttr(out, ATTR_ACTIVITY, si.getActivityComponent());
+        // writeAttr(out, "icon", si.getIcon());  // We don't save it.
+        ShortcutService.writeAttr(out, ATTR_TITLE, si.getTitle());
+        ShortcutService.writeAttr(out, ATTR_INTENT, si.getIntentNoExtras());
+        ShortcutService.writeAttr(out, ATTR_WEIGHT, si.getWeight());
+        ShortcutService.writeAttr(out, ATTR_TIMESTAMP,
+                si.getLastChangedTimestamp());
+        ShortcutService.writeAttr(out, ATTR_FLAGS, si.getFlags());
+        ShortcutService.writeAttr(out, ATTR_ICON_RES, si.getIconResourceId());
+        ShortcutService.writeAttr(out, ATTR_BITMAP_PATH, si.getBitmapPath());
+
+        ShortcutService.writeTagExtra(out, TAG_INTENT_EXTRAS,
+                si.getIntentPersistableExtras());
+        ShortcutService.writeTagExtra(out, TAG_EXTRAS, si.getExtras());
+
+        out.endTag(null, TAG_SHORTCUT);
+    }
+
+    public static ShortcutPackage loadFromXml(XmlPullParser parser, int userId)
+            throws IOException, XmlPullParserException {
+
+        final String packageName = ShortcutService.parseStringAttribute(parser,
+                ATTR_NAME);
+
+        final ShortcutPackage ret = new ShortcutPackage(userId, packageName);
+
+        ret.mDynamicShortcutCount =
+                ShortcutService.parseIntAttribute(parser, ATTR_DYNAMIC_COUNT);
+        ret.mApiCallCount =
+                ShortcutService.parseIntAttribute(parser, ATTR_CALL_COUNT);
+        ret.mLastResetTime =
+                ShortcutService.parseLongAttribute(parser, ATTR_LAST_RESET);
+
+        final int outerDepth = parser.getDepth();
+        int type;
+        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+            if (type != XmlPullParser.START_TAG) {
+                continue;
+            }
+            final int depth = parser.getDepth();
+            final String tag = parser.getName();
+            switch (tag) {
+                case TAG_SHORTCUT:
+                    final ShortcutInfo si = parseShortcut(parser, packageName);
+
+                    // Don't use addShortcut(), we don't need to save the icon.
+                    ret.mShortcuts.put(si.getId(), si);
+                    continue;
+            }
+            throw ShortcutService.throwForInvalidTag(depth, tag);
+        }
+        return ret;
+    }
+
+    private static ShortcutInfo parseShortcut(XmlPullParser parser, String packageName)
+            throws IOException, XmlPullParserException {
+        String id;
+        ComponentName activityComponent;
+        // Icon icon;
+        String title;
+        Intent intent;
+        PersistableBundle intentPersistableExtras = null;
+        int weight;
+        PersistableBundle extras = null;
+        long lastChangedTimestamp;
+        int flags;
+        int iconRes;
+        String bitmapPath;
+
+        id = ShortcutService.parseStringAttribute(parser, ATTR_ID);
+        activityComponent = ShortcutService.parseComponentNameAttribute(parser,
+                ATTR_ACTIVITY);
+        title = ShortcutService.parseStringAttribute(parser, ATTR_TITLE);
+        intent = ShortcutService.parseIntentAttribute(parser, ATTR_INTENT);
+        weight = (int) ShortcutService.parseLongAttribute(parser, ATTR_WEIGHT);
+        lastChangedTimestamp = (int) ShortcutService.parseLongAttribute(parser,
+                ATTR_TIMESTAMP);
+        flags = (int) ShortcutService.parseLongAttribute(parser, ATTR_FLAGS);
+        iconRes = (int) ShortcutService.parseLongAttribute(parser, ATTR_ICON_RES);
+        bitmapPath = ShortcutService.parseStringAttribute(parser, ATTR_BITMAP_PATH);
+
+        final int outerDepth = parser.getDepth();
+        int type;
+        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+            if (type != XmlPullParser.START_TAG) {
+                continue;
+            }
+            final int depth = parser.getDepth();
+            final String tag = parser.getName();
+            if (ShortcutService.DEBUG_LOAD) {
+                Slog.d(TAG, String.format("  depth=%d type=%d name=%s",
+                        depth, type, tag));
+            }
+            switch (tag) {
+                case TAG_INTENT_EXTRAS:
+                    intentPersistableExtras = PersistableBundle.restoreFromXml(parser);
+                    continue;
+                case TAG_EXTRAS:
+                    extras = PersistableBundle.restoreFromXml(parser);
+                    continue;
+            }
+            throw ShortcutService.throwForInvalidTag(depth, tag);
+        }
+        return new ShortcutInfo(
+                id, packageName, activityComponent, /* icon =*/ null, title, intent,
+                intentPersistableExtras, weight, extras, lastChangedTimestamp, flags,
+                iconRes, bitmapPath);
+    }
+}
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 64e76f0..42954f5 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -222,7 +222,7 @@
      * User ID -> UserShortcuts
      */
     @GuardedBy("mLock")
-    private final SparseArray<UserShortcuts> mUsers = new SparseArray<>();
+    private final SparseArray<ShortcutUser> mUsers = new SparseArray<>();
 
     /**
      * Max number of dynamic shortcuts that each application can have at a time.
@@ -633,7 +633,7 @@
     }
 
     @Nullable
-    private UserShortcuts loadUserLocked(@UserIdInt int userId) {
+    private ShortcutUser loadUserLocked(@UserIdInt int userId) {
         final File path = new File(injectUserDataPath(userId), FILENAME_USER_PACKAGES);
         if (DEBUG) {
             Slog.d(TAG, "Loading from " + path);
@@ -649,7 +649,7 @@
             }
             return null;
         }
-        UserShortcuts ret = null;
+        ShortcutUser ret = null;
         try {
             XmlPullParser parser = Xml.newPullParser();
             parser.setInput(in, StandardCharsets.UTF_8.name());
@@ -666,8 +666,8 @@
                     Slog.d(TAG, String.format("depth=%d type=%d name=%s",
                             depth, type, tag));
                 }
-                if ((depth == 1) && UserShortcuts.TAG_ROOT.equals(tag)) {
-                    ret = UserShortcuts.loadFromXml(parser, userId);
+                if ((depth == 1) && ShortcutUser.TAG_ROOT.equals(tag)) {
+                    ret = ShortcutUser.loadFromXml(parser, userId);
                     continue;
                 }
                 throwForInvalidTag(depth, tag);
@@ -779,12 +779,12 @@
     /** Return the per-user state. */
     @GuardedBy("mLock")
     @NonNull
-    UserShortcuts getUserShortcutsLocked(@UserIdInt int userId) {
-        UserShortcuts userPackages = mUsers.get(userId);
+    ShortcutUser getUserShortcutsLocked(@UserIdInt int userId) {
+        ShortcutUser userPackages = mUsers.get(userId);
         if (userPackages == null) {
             userPackages = loadUserLocked(userId);
             if (userPackages == null) {
-                userPackages = new UserShortcuts(userId);
+                userPackages = new ShortcutUser(userId);
             }
             mUsers.put(userId, userPackages);
         }
@@ -794,14 +794,14 @@
     /** Return the per-user per-package state. */
     @GuardedBy("mLock")
     @NonNull
-    PackageShortcuts getPackageShortcutsLocked(
+    ShortcutPackage getPackageShortcutsLocked(
             @NonNull String packageName, @UserIdInt int userId) {
         return getUserShortcutsLocked(userId).getPackageShortcuts(packageName);
     }
 
     @GuardedBy("mLock")
     @NonNull
-    LauncherShortcuts getLauncherShortcuts(
+    ShortcutLauncher getLauncherShortcuts(
             @NonNull String packageName, @UserIdInt int userId) {
         return getUserShortcutsLocked(userId).getLauncherShortcuts(packageName);
     }
@@ -1169,7 +1169,7 @@
         final int size = newShortcuts.size();
 
         synchronized (mLock) {
-            final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId);
+            final ShortcutPackage ps = getPackageShortcutsLocked(packageName, userId);
 
             // Throttling.
             if (!ps.tryApiCall(this)) {
@@ -1204,7 +1204,7 @@
         final int size = newShortcuts.size();
 
         synchronized (mLock) {
-            final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId);
+            final ShortcutPackage ps = getPackageShortcutsLocked(packageName, userId);
 
             // Throttling.
             if (!ps.tryApiCall(this)) {
@@ -1241,7 +1241,7 @@
         verifyCaller(packageName, userId);
 
         synchronized (mLock) {
-            final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId);
+            final ShortcutPackage ps = getPackageShortcutsLocked(packageName, userId);
 
             // Throttling.
             if (!ps.tryApiCall(this)) {
@@ -1381,7 +1381,7 @@
                 start = System.currentTimeMillis();
             }
 
-            final UserShortcuts user = getUserShortcutsLocked(userId);
+            final ShortcutUser user = getUserShortcutsLocked(userId);
 
             final List<ResolveInfo> allHomeCandidates = new ArrayList<>();
 
@@ -1453,7 +1453,7 @@
     void cleanUpPackageLocked(String packageName, int userId) {
         final boolean wasUserLoaded = isUserLoadedLocked(userId);
 
-        final UserShortcuts mUser = getUserShortcutsLocked(userId);
+        final ShortcutUser mUser = getUserShortcutsLocked(userId);
         boolean doNotify = false;
 
         // First, remove the package from the package list (if the package is a publisher).
@@ -1506,7 +1506,7 @@
                             callingPackage, packageName, changedSince,
                             componentName, queryFlags, userId, ret, cloneFlag);
                 } else {
-                    final ArrayMap<String, PackageShortcuts> packages =
+                    final ArrayMap<String, ShortcutPackage> packages =
                             getUserShortcutsLocked(userId).getPackages();
                     for (int i = packages.size() - 1; i >= 0; i--) {
                         getShortcutsInnerLocked(
@@ -1932,25 +1932,29 @@
     // === Unit test support ===
 
     // Injection point.
+    @VisibleForTesting
     long injectCurrentTimeMillis() {
         return System.currentTimeMillis();
     }
 
     // Injection point.
+    @VisibleForTesting
     int injectBinderCallingUid() {
         return getCallingUid();
     }
 
-    final int getCallingUserId() {
+    private int getCallingUserId() {
         return UserHandle.getUserId(injectBinderCallingUid());
     }
 
     // Injection point.
+    @VisibleForTesting
     long injectClearCallingIdentity() {
         return Binder.clearCallingIdentity();
     }
 
     // Injection point.
+    @VisibleForTesting
     void injectRestoreCallingIdentity(long token) {
         Binder.restoreCallingIdentity(token);
     }
@@ -1963,10 +1967,12 @@
         Slog.wtf(TAG, message, e);
     }
 
+    @VisibleForTesting
     File injectSystemDataPath() {
         return Environment.getDataSystemDirectory();
     }
 
+    @VisibleForTesting
     File injectUserDataPath(@UserIdInt int userId) {
         return new File(Environment.getDataSystemCeDirectory(userId), DIRECTORY_PER_USER);
     }
@@ -1976,16 +1982,18 @@
         return ActivityManager.isLowRamDeviceStatic();
     }
 
+    @VisibleForTesting
     PackageManagerInternal injectPackageManagerInternal() {
         return mPackageManagerInternal;
     }
 
+    @VisibleForTesting
     File getUserBitmapFilePath(@UserIdInt int userId) {
         return new File(injectUserDataPath(userId), DIRECTORY_BITMAPS);
     }
 
     @VisibleForTesting
-    SparseArray<UserShortcuts> getShortcutsForTest() {
+    SparseArray<ShortcutUser> getShortcutsForTest() {
         return mUsers;
     }
 
@@ -2022,809 +2030,13 @@
     @VisibleForTesting
     ShortcutInfo getPackageShortcutForTest(String packageName, String shortcutId, int userId) {
         synchronized (mLock) {
-            final UserShortcuts user = mUsers.get(userId);
+            final ShortcutUser user = mUsers.get(userId);
             if (user == null) return null;
 
-            final PackageShortcuts pkg = user.getPackages().get(packageName);
+            final ShortcutPackage pkg = user.getPackages().get(packageName);
             if (pkg == null) return null;
 
             return pkg.findShortcutById(shortcutId);
         }
     }
 }
-
-/**
- * Per-user information.
- */
-class UserShortcuts {
-    private static final String TAG = ShortcutService.TAG;
-
-    static final String TAG_ROOT = "user";
-    private static final String TAG_LAUNCHER = "launcher";
-
-    private static final String ATTR_VALUE = "value";
-
-    @UserIdInt
-    final int mUserId;
-
-    private final ArrayMap<String, PackageShortcuts> mPackages = new ArrayMap<>();
-
-    private final ArrayMap<String, LauncherShortcuts> mLaunchers = new ArrayMap<>();
-
-    private ComponentName mLauncherComponent;
-
-    public UserShortcuts(int userId) {
-        mUserId = userId;
-    }
-
-    public ArrayMap<String, PackageShortcuts> getPackages() {
-        return mPackages;
-    }
-
-    public ArrayMap<String, LauncherShortcuts> getLaunchers() {
-        return mLaunchers;
-    }
-
-    public PackageShortcuts getPackageShortcuts(@NonNull String packageName) {
-        PackageShortcuts ret = mPackages.get(packageName);
-        if (ret == null) {
-            ret = new PackageShortcuts(mUserId, packageName);
-            mPackages.put(packageName, ret);
-        }
-        return ret;
-    }
-
-    public LauncherShortcuts getLauncherShortcuts(@NonNull String packageName) {
-        LauncherShortcuts ret = mLaunchers.get(packageName);
-        if (ret == null) {
-            ret = new LauncherShortcuts(mUserId, packageName);
-            mLaunchers.put(packageName, ret);
-        }
-        return ret;
-    }
-
-    public void saveToXml(XmlSerializer out) throws IOException, XmlPullParserException {
-        out.startTag(null, TAG_ROOT);
-
-        ShortcutService.writeTagValue(out, TAG_LAUNCHER,
-                mLauncherComponent);
-
-        final int lsize = mLaunchers.size();
-        for (int i = 0; i < lsize; i++) {
-            mLaunchers.valueAt(i).saveToXml(out);
-        }
-
-        final int psize = mPackages.size();
-        for (int i = 0; i < psize; i++) {
-            mPackages.valueAt(i).saveToXml(out);
-        }
-
-        out.endTag(null, TAG_ROOT);
-    }
-
-    public static UserShortcuts loadFromXml(XmlPullParser parser, int userId)
-            throws IOException, XmlPullParserException {
-        final UserShortcuts ret = new UserShortcuts(userId);
-
-        final int outerDepth = parser.getDepth();
-        int type;
-        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
-                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
-            if (type != XmlPullParser.START_TAG) {
-                continue;
-            }
-            final int depth = parser.getDepth();
-            final String tag = parser.getName();
-            switch (tag) {
-                case TAG_LAUNCHER: {
-                    ret.mLauncherComponent = ShortcutService.parseComponentNameAttribute(
-                            parser, ATTR_VALUE);
-                    continue;
-                }
-                case PackageShortcuts.TAG_ROOT: {
-                    final PackageShortcuts shortcuts = PackageShortcuts.loadFromXml(parser, userId);
-
-                    // Don't use addShortcut(), we don't need to save the icon.
-                    ret.getPackages().put(shortcuts.mPackageName, shortcuts);
-                    continue;
-                }
-
-                case LauncherShortcuts.TAG_ROOT: {
-                    final LauncherShortcuts shortcuts =
-                            LauncherShortcuts.loadFromXml(parser, userId);
-
-                    ret.getLaunchers().put(shortcuts.mPackageName, shortcuts);
-                    continue;
-                }
-            }
-            throw ShortcutService.throwForInvalidTag(depth, tag);
-        }
-        return ret;
-    }
-
-    public ComponentName getLauncherComponent() {
-        return mLauncherComponent;
-    }
-
-    public void setLauncherComponent(ShortcutService s, ComponentName launcherComponent) {
-        if (Objects.equal(mLauncherComponent, launcherComponent)) {
-            return;
-        }
-        mLauncherComponent = launcherComponent;
-        s.scheduleSaveUser(mUserId);
-    }
-
-    public void resetThrottling() {
-        for (int i = mPackages.size() - 1; i >= 0; i--) {
-            mPackages.valueAt(i).resetThrottling();
-        }
-    }
-
-    public void dump(@NonNull ShortcutService s, @NonNull PrintWriter pw, @NonNull String prefix) {
-        pw.print(prefix);
-        pw.print("User: ");
-        pw.print(mUserId);
-        pw.println();
-
-        pw.print(prefix);
-        pw.print("  ");
-        pw.print("Default launcher: ");
-        pw.print(mLauncherComponent);
-        pw.println();
-
-        for (int i = 0; i < mLaunchers.size(); i++) {
-            mLaunchers.valueAt(i).dump(s, pw, prefix + "  ");
-        }
-
-        for (int i = 0; i < mPackages.size(); i++) {
-            mPackages.valueAt(i).dump(s, pw, prefix + "  ");
-        }
-    }
-}
-
-class LauncherShortcuts {
-    private static final String TAG = ShortcutService.TAG;
-
-    static final String TAG_ROOT = "launcher-pins";
-
-    private static final String TAG_PACKAGE = "package";
-    private static final String TAG_PIN = "pin";
-
-    private static final String ATTR_VALUE = "value";
-    private static final String ATTR_PACKAGE_NAME = "package-name";
-
-    @UserIdInt
-    final int mUserId;
-
-    @NonNull
-    final String mPackageName;
-
-    /**
-     * Package name -> IDs.
-     */
-    final private ArrayMap<String, ArraySet<String>> mPinnedShortcuts = new ArrayMap<>();
-
-    LauncherShortcuts(@UserIdInt int userId, @NonNull String packageName) {
-        mUserId = userId;
-        mPackageName = packageName;
-    }
-
-    public void pinShortcuts(@NonNull ShortcutService s, @NonNull String packageName,
-            @NonNull List<String> ids) {
-        final int idSize = ids.size();
-        if (idSize == 0) {
-            mPinnedShortcuts.remove(packageName);
-        } else {
-            final ArraySet<String> prevSet = mPinnedShortcuts.get(packageName);
-
-            // Pin shortcuts.  Make sure only pin the ones that were visible to the caller.
-            // i.e. a non-dynamic, pinned shortcut by *other launchers* shouldn't be pinned here.
-
-            final PackageShortcuts packageShortcuts =
-                    s.getPackageShortcutsLocked(packageName, mUserId);
-            final ArraySet<String> newSet = new ArraySet<>();
-
-            for (int i = 0; i < idSize; i++) {
-                final String id = ids.get(i);
-                final ShortcutInfo si = packageShortcuts.findShortcutById(id);
-                if (si == null) {
-                    continue;
-                }
-                if (si.isDynamic() || (prevSet != null && prevSet.contains(id))) {
-                    newSet.add(id);
-                }
-            }
-            mPinnedShortcuts.put(packageName, newSet);
-        }
-        s.getPackageShortcutsLocked(packageName, mUserId).refreshPinnedFlags(s);
-    }
-
-    /**
-     * Return the pinned shortcut IDs for the publisher package.
-     */
-    public ArraySet<String> getPinnedShortcutIds(@NonNull String packageName) {
-        return mPinnedShortcuts.get(packageName);
-    }
-
-    boolean cleanUpPackage(String packageName) {
-        return mPinnedShortcuts.remove(packageName) != null;
-    }
-
-    /**
-     * Persist.
-     */
-    public void saveToXml(XmlSerializer out) throws IOException {
-        final int size = mPinnedShortcuts.size();
-        if (size == 0) {
-            return; // Nothing to write.
-        }
-
-        out.startTag(null, TAG_ROOT);
-        ShortcutService.writeAttr(out, ATTR_PACKAGE_NAME,
-                mPackageName);
-
-        for (int i = 0; i < size; i++) {
-            out.startTag(null, TAG_PACKAGE);
-            ShortcutService.writeAttr(out, ATTR_PACKAGE_NAME,
-                    mPinnedShortcuts.keyAt(i));
-
-            final ArraySet<String> ids = mPinnedShortcuts.valueAt(i);
-            final int idSize = ids.size();
-            for (int j = 0; j < idSize; j++) {
-                ShortcutService.writeTagValue(out, TAG_PIN, ids.valueAt(j));
-            }
-            out.endTag(null, TAG_PACKAGE);
-        }
-
-        out.endTag(null, TAG_ROOT);
-    }
-
-    /**
-     * Load.
-     */
-    public static LauncherShortcuts loadFromXml(XmlPullParser parser, int userId)
-            throws IOException, XmlPullParserException {
-        final String launcherPackageName = ShortcutService.parseStringAttribute(parser,
-                ATTR_PACKAGE_NAME);
-
-        final LauncherShortcuts ret = new LauncherShortcuts(userId, launcherPackageName);
-
-        ArraySet<String> ids = null;
-        final int outerDepth = parser.getDepth();
-        int type;
-        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
-                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
-            if (type != XmlPullParser.START_TAG) {
-                continue;
-            }
-            final int depth = parser.getDepth();
-            final String tag = parser.getName();
-            switch (tag) {
-                case TAG_PACKAGE: {
-                    final String packageName = ShortcutService.parseStringAttribute(parser,
-                            ATTR_PACKAGE_NAME);
-                    ids = new ArraySet<>();
-                    ret.mPinnedShortcuts.put(packageName, ids);
-                    continue;
-                }
-                case TAG_PIN: {
-                    ids.add(ShortcutService.parseStringAttribute(parser,
-                            ATTR_VALUE));
-                    continue;
-                }
-            }
-            throw ShortcutService.throwForInvalidTag(depth, tag);
-        }
-        return ret;
-    }
-
-    public void dump(@NonNull ShortcutService s, @NonNull PrintWriter pw, @NonNull String prefix) {
-        pw.println();
-
-        pw.print(prefix);
-        pw.print("Launcher: ");
-        pw.print(mPackageName);
-        pw.println();
-
-        final int size = mPinnedShortcuts.size();
-        for (int i = 0; i < size; i++) {
-            pw.println();
-
-            pw.print(prefix);
-            pw.print("  ");
-            pw.print("Package: ");
-            pw.println(mPinnedShortcuts.keyAt(i));
-
-            final ArraySet<String> ids = mPinnedShortcuts.valueAt(i);
-            final int idSize = ids.size();
-
-            for (int j = 0; j < idSize; j++) {
-                pw.print(prefix);
-                pw.print("    ");
-                pw.print(ids.valueAt(j));
-                pw.println();
-            }
-        }
-    }
-}
-
-/**
- * All the information relevant to shortcuts from a single package (per-user).
- */
-class PackageShortcuts {
-    private static final String TAG = ShortcutService.TAG;
-
-    static final String TAG_ROOT = "package";
-    private static final String TAG_INTENT_EXTRAS = "intent-extras";
-    private static final String TAG_EXTRAS = "extras";
-    private static final String TAG_SHORTCUT = "shortcut";
-
-    private static final String ATTR_NAME = "name";
-    private static final String ATTR_DYNAMIC_COUNT = "dynamic-count";
-    private static final String ATTR_CALL_COUNT = "call-count";
-    private static final String ATTR_LAST_RESET = "last-reset";
-    private static final String ATTR_ID = "id";
-    private static final String ATTR_ACTIVITY = "activity";
-    private static final String ATTR_TITLE = "title";
-    private static final String ATTR_INTENT = "intent";
-    private static final String ATTR_WEIGHT = "weight";
-    private static final String ATTR_TIMESTAMP = "timestamp";
-    private static final String ATTR_FLAGS = "flags";
-    private static final String ATTR_ICON_RES = "icon-res";
-    private static final String ATTR_BITMAP_PATH = "bitmap-path";
-
-    @UserIdInt
-    final int mUserId;
-
-    @NonNull
-    final String mPackageName;
-
-    /**
-     * All the shortcuts from the package, keyed on IDs.
-     */
-    final private ArrayMap<String, ShortcutInfo> mShortcuts = new ArrayMap<>();
-
-    /**
-     * # of dynamic shortcuts.
-     */
-    private int mDynamicShortcutCount = 0;
-
-    /**
-     * # of times the package has called rate-limited APIs.
-     */
-    private int mApiCallCount;
-
-    /**
-     * When {@link #mApiCallCount} was reset last time.
-     */
-    private long mLastResetTime;
-
-    PackageShortcuts(int userId, String packageName) {
-        mUserId = userId;
-        mPackageName = packageName;
-    }
-
-    @Nullable
-    public ShortcutInfo findShortcutById(String id) {
-        return mShortcuts.get(id);
-    }
-
-    private ShortcutInfo deleteShortcut(@NonNull ShortcutService s,
-            @NonNull String id) {
-        final ShortcutInfo shortcut = mShortcuts.remove(id);
-        if (shortcut != null) {
-            s.removeIcon(mUserId, shortcut);
-            shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED);
-        }
-        return shortcut;
-    }
-
-    void addShortcut(@NonNull ShortcutService s, @NonNull ShortcutInfo newShortcut) {
-        deleteShortcut(s, newShortcut.getId());
-        s.saveIconAndFixUpShortcut(mUserId, newShortcut);
-        mShortcuts.put(newShortcut.getId(), newShortcut);
-    }
-
-    /**
-     * Add a shortcut, or update one with the same ID, with taking over existing flags.
-     *
-     * It checks the max number of dynamic shortcuts.
-     */
-    public void addDynamicShortcut(@NonNull ShortcutService s,
-            @NonNull ShortcutInfo newShortcut) {
-        newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC);
-
-        final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId());
-
-        final boolean wasPinned;
-        final int newDynamicCount;
-
-        if (oldShortcut == null) {
-            wasPinned = false;
-            newDynamicCount = mDynamicShortcutCount + 1; // adding a dynamic shortcut.
-        } else {
-            wasPinned = oldShortcut.isPinned();
-            if (oldShortcut.isDynamic()) {
-                newDynamicCount = mDynamicShortcutCount; // not adding a dynamic shortcut.
-            } else {
-                newDynamicCount = mDynamicShortcutCount + 1; // adding a dynamic shortcut.
-            }
-        }
-
-        // Make sure there's still room.
-        s.enforceMaxDynamicShortcuts(newDynamicCount);
-
-        // Okay, make it dynamic and add.
-        if (wasPinned) {
-            newShortcut.addFlags(ShortcutInfo.FLAG_PINNED);
-        }
-
-        addShortcut(s, newShortcut);
-        mDynamicShortcutCount = newDynamicCount;
-    }
-
-    /**
-     * Remove all shortcuts that aren't pinned nor dynamic.
-     */
-    private void removeOrphans(@NonNull ShortcutService s) {
-        ArrayList<String> removeList = null; // Lazily initialize.
-
-        for (int i = mShortcuts.size() - 1; i >= 0; i--) {
-            final ShortcutInfo si = mShortcuts.valueAt(i);
-
-            if (si.isPinned() || si.isDynamic()) continue;
-
-            if (removeList == null) {
-                removeList = new ArrayList<>();
-            }
-            removeList.add(si.getId());
-        }
-        if (removeList != null) {
-            for (int i = removeList.size() - 1; i >= 0; i--) {
-                deleteShortcut(s, removeList.get(i));
-            }
-        }
-    }
-
-    /**
-     * Remove all dynamic shortcuts.
-     */
-    public void deleteAllDynamicShortcuts(@NonNull ShortcutService s) {
-        for (int i = mShortcuts.size() - 1; i >= 0; i--) {
-            mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_DYNAMIC);
-        }
-        removeOrphans(s);
-        mDynamicShortcutCount = 0;
-    }
-
-    /**
-     * Remove a dynamic shortcut by ID.
-     */
-    public void deleteDynamicWithId(@NonNull ShortcutService s, @NonNull String shortcutId) {
-        final ShortcutInfo oldShortcut = mShortcuts.get(shortcutId);
-
-        if (oldShortcut == null) {
-            return;
-        }
-        if (oldShortcut.isDynamic()) {
-            mDynamicShortcutCount--;
-        }
-        if (oldShortcut.isPinned()) {
-            oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC);
-        } else {
-            deleteShortcut(s, shortcutId);
-        }
-    }
-
-    /**
-     * Called after a launcher updates the pinned set.  For each shortcut in this package,
-     * set FLAG_PINNED if any launcher has pinned it.  Otherwise, clear it.
-     *
-     * <p>Then remove all shortcuts that are not dynamic and no longer pinned either.
-     */
-    public void refreshPinnedFlags(@NonNull ShortcutService s) {
-        // First, un-pin all shortcuts
-        for (int i = mShortcuts.size() - 1; i >= 0; i--) {
-            mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_PINNED);
-        }
-
-        // Then, for the pinned set for each launcher, set the pin flag one by one.
-        final ArrayMap<String, LauncherShortcuts> launchers =
-                s.getUserShortcutsLocked(mUserId).getLaunchers();
-
-        for (int l = launchers.size() - 1; l >= 0; l--) {
-            final LauncherShortcuts launcherShortcuts = launchers.valueAt(l);
-            final ArraySet<String> pinned = launcherShortcuts.getPinnedShortcutIds(mPackageName);
-
-            if (pinned == null || pinned.size() == 0) {
-                continue;
-            }
-            for (int i = pinned.size() - 1; i >= 0; i--) {
-                final ShortcutInfo si = mShortcuts.get(pinned.valueAt(i));
-                if (si == null) {
-                    s.wtf("Shortcut not found");
-                } else {
-                    si.addFlags(ShortcutInfo.FLAG_PINNED);
-                }
-            }
-        }
-
-        // Lastly, remove the ones that are no longer pinned nor dynamic.
-        removeOrphans(s);
-    }
-
-    /**
-     * Number of calls that the caller has made, since the last reset.
-     */
-    public int getApiCallCount(@NonNull ShortcutService s) {
-        final long last = s.getLastResetTimeLocked();
-
-        final long now = s.injectCurrentTimeMillis();
-        if (ShortcutService.isClockValid(now) && mLastResetTime > now) {
-            Slog.w(TAG, "Clock rewound");
-            // Clock rewound.
-            mLastResetTime = now;
-            mApiCallCount = 0;
-            return mApiCallCount;
-        }
-
-        // If not reset yet, then reset.
-        if (mLastResetTime < last) {
-            if (ShortcutService.DEBUG) {
-                Slog.d(TAG, String.format("My last reset=%d, now=%d, last=%d: resetting",
-                        mLastResetTime, now, last));
-            }
-            mApiCallCount = 0;
-            mLastResetTime = last;
-        }
-        return mApiCallCount;
-    }
-
-    /**
-     * If the caller app hasn't been throttled yet, increment {@link #mApiCallCount}
-     * and return true.  Otherwise just return false.
-     */
-    public boolean tryApiCall(@NonNull ShortcutService s) {
-        if (getApiCallCount(s) >= s.mMaxDailyUpdates) {
-            return false;
-        }
-        mApiCallCount++;
-        return true;
-    }
-
-    public void resetRateLimitingForCommandLine() {
-        mApiCallCount = 0;
-        mLastResetTime = 0;
-    }
-
-    /**
-     * Find all shortcuts that match {@code query}.
-     */
-    public void findAll(@NonNull ShortcutService s, @NonNull List<ShortcutInfo> result,
-            @Nullable Predicate<ShortcutInfo> query, int cloneFlag,
-            @Nullable String callingLauncher) {
-
-        // Set of pinned shortcuts by the calling launcher.
-        final ArraySet<String> pinnedByCallerSet = (callingLauncher == null) ? null
-                : s.getLauncherShortcuts(callingLauncher, mUserId)
-                    .getPinnedShortcutIds(mPackageName);
-
-        for (int i = 0; i < mShortcuts.size(); i++) {
-            final ShortcutInfo si = mShortcuts.valueAt(i);
-
-            // If it's called by non-launcher (i.e. publisher, always include -> true.
-            // Otherwise, only include non-dynamic pinned one, if the calling launcher has pinned
-            // it.
-            final boolean isPinnedByCaller = (callingLauncher == null)
-                    || ((pinnedByCallerSet != null) && pinnedByCallerSet.contains(si.getId()));
-            if (!si.isDynamic()) {
-                if (!si.isPinned()) {
-                    s.wtf("Shortcut not pinned here");
-                    continue;
-                }
-                if (!isPinnedByCaller) {
-                    continue;
-                }
-            }
-            final ShortcutInfo clone = si.clone(cloneFlag);
-            // Fix up isPinned for the caller.  Note we need to do it before the "test" callback,
-            // since it may check isPinned.
-            if (!isPinnedByCaller) {
-                clone.clearFlags(ShortcutInfo.FLAG_PINNED);
-            }
-            if (query == null || query.test(clone)) {
-                result.add(clone);
-            }
-        }
-    }
-
-    public void resetThrottling() {
-        mApiCallCount = 0;
-    }
-
-    public void dump(@NonNull ShortcutService s, @NonNull PrintWriter pw, @NonNull String prefix) {
-        pw.println();
-
-        pw.print(prefix);
-        pw.print("Package: ");
-        pw.print(mPackageName);
-        pw.println();
-
-        pw.print(prefix);
-        pw.print("  ");
-        pw.print("Calls: ");
-        pw.print(getApiCallCount(s));
-        pw.println();
-
-        // This should be after getApiCallCount(), which may update it.
-        pw.print(prefix);
-        pw.print("  ");
-        pw.print("Last reset: [");
-        pw.print(mLastResetTime);
-        pw.print("] ");
-        pw.print(s.formatTime(mLastResetTime));
-        pw.println();
-
-        pw.println("      Shortcuts:");
-        long totalBitmapSize = 0;
-        final ArrayMap<String, ShortcutInfo> shortcuts = mShortcuts;
-        final int size = shortcuts.size();
-        for (int i = 0; i < size; i++) {
-            final ShortcutInfo si = shortcuts.valueAt(i);
-            pw.print("        ");
-            pw.println(si.toInsecureString());
-            if (si.getBitmapPath() != null) {
-                final long len = new File(si.getBitmapPath()).length();
-                pw.print("          ");
-                pw.print("bitmap size=");
-                pw.println(len);
-
-                totalBitmapSize += len;
-            }
-        }
-        pw.print(prefix);
-        pw.print("  ");
-        pw.print("Total bitmap size: ");
-        pw.print(totalBitmapSize);
-        pw.print(" (");
-        pw.print(Formatter.formatFileSize(s.mContext, totalBitmapSize));
-        pw.println(")");
-    }
-
-    public void saveToXml(@NonNull XmlSerializer out) throws IOException, XmlPullParserException {
-        final int size = mShortcuts.size();
-
-        if (size == 0 && mApiCallCount == 0) {
-            return; // nothing to write.
-        }
-
-        out.startTag(null, TAG_ROOT);
-
-        ShortcutService.writeAttr(out, ATTR_NAME, mPackageName);
-        ShortcutService.writeAttr(out, ATTR_DYNAMIC_COUNT, mDynamicShortcutCount);
-        ShortcutService.writeAttr(out, ATTR_CALL_COUNT, mApiCallCount);
-        ShortcutService.writeAttr(out, ATTR_LAST_RESET, mLastResetTime);
-
-        for (int j = 0; j < size; j++) {
-            saveShortcut(out, mShortcuts.valueAt(j));
-        }
-
-        out.endTag(null, TAG_ROOT);
-    }
-
-    private static void saveShortcut(XmlSerializer out, ShortcutInfo si)
-            throws IOException, XmlPullParserException {
-        out.startTag(null, TAG_SHORTCUT);
-        ShortcutService.writeAttr(out, ATTR_ID, si.getId());
-        // writeAttr(out, "package", si.getPackageName()); // not needed
-        ShortcutService.writeAttr(out, ATTR_ACTIVITY, si.getActivityComponent());
-        // writeAttr(out, "icon", si.getIcon());  // We don't save it.
-        ShortcutService.writeAttr(out, ATTR_TITLE, si.getTitle());
-        ShortcutService.writeAttr(out, ATTR_INTENT, si.getIntentNoExtras());
-        ShortcutService.writeAttr(out, ATTR_WEIGHT, si.getWeight());
-        ShortcutService.writeAttr(out, ATTR_TIMESTAMP,
-                si.getLastChangedTimestamp());
-        ShortcutService.writeAttr(out, ATTR_FLAGS, si.getFlags());
-        ShortcutService.writeAttr(out, ATTR_ICON_RES, si.getIconResourceId());
-        ShortcutService.writeAttr(out, ATTR_BITMAP_PATH, si.getBitmapPath());
-
-        ShortcutService.writeTagExtra(out, TAG_INTENT_EXTRAS,
-                si.getIntentPersistableExtras());
-        ShortcutService.writeTagExtra(out, TAG_EXTRAS, si.getExtras());
-
-        out.endTag(null, TAG_SHORTCUT);
-    }
-
-    public static PackageShortcuts loadFromXml(XmlPullParser parser, int userId)
-            throws IOException, XmlPullParserException {
-
-        final String packageName = ShortcutService.parseStringAttribute(parser,
-                ATTR_NAME);
-
-        final PackageShortcuts ret = new PackageShortcuts(userId, packageName);
-
-        ret.mDynamicShortcutCount =
-                ShortcutService.parseIntAttribute(parser, ATTR_DYNAMIC_COUNT);
-        ret.mApiCallCount =
-                ShortcutService.parseIntAttribute(parser, ATTR_CALL_COUNT);
-        ret.mLastResetTime =
-                ShortcutService.parseLongAttribute(parser, ATTR_LAST_RESET);
-
-        final int outerDepth = parser.getDepth();
-        int type;
-        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
-                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
-            if (type != XmlPullParser.START_TAG) {
-                continue;
-            }
-            final int depth = parser.getDepth();
-            final String tag = parser.getName();
-            switch (tag) {
-                case TAG_SHORTCUT:
-                    final ShortcutInfo si = parseShortcut(parser, packageName);
-
-                    // Don't use addShortcut(), we don't need to save the icon.
-                    ret.mShortcuts.put(si.getId(), si);
-                    continue;
-            }
-            throw ShortcutService.throwForInvalidTag(depth, tag);
-        }
-        return ret;
-    }
-
-    private static ShortcutInfo parseShortcut(XmlPullParser parser, String packageName)
-            throws IOException, XmlPullParserException {
-        String id;
-        ComponentName activityComponent;
-        // Icon icon;
-        String title;
-        Intent intent;
-        PersistableBundle intentPersistableExtras = null;
-        int weight;
-        PersistableBundle extras = null;
-        long lastChangedTimestamp;
-        int flags;
-        int iconRes;
-        String bitmapPath;
-
-        id = ShortcutService.parseStringAttribute(parser, ATTR_ID);
-        activityComponent = ShortcutService.parseComponentNameAttribute(parser,
-                ATTR_ACTIVITY);
-        title = ShortcutService.parseStringAttribute(parser, ATTR_TITLE);
-        intent = ShortcutService.parseIntentAttribute(parser, ATTR_INTENT);
-        weight = (int) ShortcutService.parseLongAttribute(parser, ATTR_WEIGHT);
-        lastChangedTimestamp = (int) ShortcutService.parseLongAttribute(parser,
-                ATTR_TIMESTAMP);
-        flags = (int) ShortcutService.parseLongAttribute(parser, ATTR_FLAGS);
-        iconRes = (int) ShortcutService.parseLongAttribute(parser, ATTR_ICON_RES);
-        bitmapPath = ShortcutService.parseStringAttribute(parser, ATTR_BITMAP_PATH);
-
-        final int outerDepth = parser.getDepth();
-        int type;
-        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
-                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
-            if (type != XmlPullParser.START_TAG) {
-                continue;
-            }
-            final int depth = parser.getDepth();
-            final String tag = parser.getName();
-            if (ShortcutService.DEBUG_LOAD) {
-                Slog.d(TAG, String.format("  depth=%d type=%d name=%s",
-                        depth, type, tag));
-            }
-            switch (tag) {
-                case TAG_INTENT_EXTRAS:
-                    intentPersistableExtras = PersistableBundle.restoreFromXml(parser);
-                    continue;
-                case TAG_EXTRAS:
-                    extras = PersistableBundle.restoreFromXml(parser);
-                    continue;
-            }
-            throw ShortcutService.throwForInvalidTag(depth, tag);
-        }
-        return new ShortcutInfo(
-                id, packageName, activityComponent, /* icon =*/ null, title, intent,
-                intentPersistableExtras, weight, extras, lastChangedTimestamp, flags,
-                iconRes, bitmapPath);
-    }
-}
diff --git a/services/core/java/com/android/server/pm/ShortcutUser.java b/services/core/java/com/android/server/pm/ShortcutUser.java
new file mode 100644
index 0000000..4a6b1e4
--- /dev/null
+++ b/services/core/java/com/android/server/pm/ShortcutUser.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2016 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.server.pm;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.content.ComponentName;
+import android.util.ArrayMap;
+
+import libcore.util.Objects;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+/**
+ * User information used by {@link ShortcutService}.
+ */
+class ShortcutUser {
+    private static final String TAG = ShortcutService.TAG;
+
+    static final String TAG_ROOT = "user";
+    private static final String TAG_LAUNCHER = "launcher";
+
+    private static final String ATTR_VALUE = "value";
+
+    @UserIdInt
+    final int mUserId;
+
+    private final ArrayMap<String, ShortcutPackage> mPackages = new ArrayMap<>();
+
+    private final ArrayMap<String, ShortcutLauncher> mLaunchers = new ArrayMap<>();
+
+    private ComponentName mLauncherComponent;
+
+    public ShortcutUser(int userId) {
+        mUserId = userId;
+    }
+
+    public ArrayMap<String, ShortcutPackage> getPackages() {
+        return mPackages;
+    }
+
+    public ArrayMap<String, ShortcutLauncher> getLaunchers() {
+        return mLaunchers;
+    }
+
+    public ShortcutPackage getPackageShortcuts(@NonNull String packageName) {
+        ShortcutPackage ret = mPackages.get(packageName);
+        if (ret == null) {
+            ret = new ShortcutPackage(mUserId, packageName);
+            mPackages.put(packageName, ret);
+        }
+        return ret;
+    }
+
+    public ShortcutLauncher getLauncherShortcuts(@NonNull String packageName) {
+        ShortcutLauncher ret = mLaunchers.get(packageName);
+        if (ret == null) {
+            ret = new ShortcutLauncher(mUserId, packageName);
+            mLaunchers.put(packageName, ret);
+        }
+        return ret;
+    }
+
+    public void saveToXml(XmlSerializer out) throws IOException, XmlPullParserException {
+        out.startTag(null, TAG_ROOT);
+
+        ShortcutService.writeTagValue(out, TAG_LAUNCHER,
+                mLauncherComponent);
+
+        final int lsize = mLaunchers.size();
+        for (int i = 0; i < lsize; i++) {
+            mLaunchers.valueAt(i).saveToXml(out);
+        }
+
+        final int psize = mPackages.size();
+        for (int i = 0; i < psize; i++) {
+            mPackages.valueAt(i).saveToXml(out);
+        }
+
+        out.endTag(null, TAG_ROOT);
+    }
+
+    public static ShortcutUser loadFromXml(XmlPullParser parser, int userId)
+            throws IOException, XmlPullParserException {
+        final ShortcutUser ret = new ShortcutUser(userId);
+
+        final int outerDepth = parser.getDepth();
+        int type;
+        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+            if (type != XmlPullParser.START_TAG) {
+                continue;
+            }
+            final int depth = parser.getDepth();
+            final String tag = parser.getName();
+            switch (tag) {
+                case TAG_LAUNCHER: {
+                    ret.mLauncherComponent = ShortcutService.parseComponentNameAttribute(
+                            parser, ATTR_VALUE);
+                    continue;
+                }
+                case ShortcutPackage.TAG_ROOT: {
+                    final ShortcutPackage shortcuts = ShortcutPackage.loadFromXml(parser, userId);
+
+                    // Don't use addShortcut(), we don't need to save the icon.
+                    ret.getPackages().put(shortcuts.mPackageName, shortcuts);
+                    continue;
+                }
+
+                case ShortcutLauncher.TAG_ROOT: {
+                    final ShortcutLauncher shortcuts =
+                            ShortcutLauncher.loadFromXml(parser, userId);
+
+                    ret.getLaunchers().put(shortcuts.mPackageName, shortcuts);
+                    continue;
+                }
+            }
+            throw ShortcutService.throwForInvalidTag(depth, tag);
+        }
+        return ret;
+    }
+
+    public ComponentName getLauncherComponent() {
+        return mLauncherComponent;
+    }
+
+    public void setLauncherComponent(ShortcutService s, ComponentName launcherComponent) {
+        if (Objects.equal(mLauncherComponent, launcherComponent)) {
+            return;
+        }
+        mLauncherComponent = launcherComponent;
+        s.scheduleSaveUser(mUserId);
+    }
+
+    public void resetThrottling() {
+        for (int i = mPackages.size() - 1; i >= 0; i--) {
+            mPackages.valueAt(i).resetThrottling();
+        }
+    }
+
+    public void dump(@NonNull ShortcutService s, @NonNull PrintWriter pw, @NonNull String prefix) {
+        pw.print(prefix);
+        pw.print("User: ");
+        pw.print(mUserId);
+        pw.println();
+
+        pw.print(prefix);
+        pw.print("  ");
+        pw.print("Default launcher: ");
+        pw.print(mLauncherComponent);
+        pw.println();
+
+        for (int i = 0; i < mLaunchers.size(); i++) {
+            mLaunchers.valueAt(i).dump(s, pw, prefix + "  ");
+        }
+
+        for (int i = 0; i < mPackages.size(); i++) {
+            mPackages.valueAt(i).dump(s, pw, prefix + "  ");
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index a589f89..c0c1ed8 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -195,8 +195,10 @@
 
     @Override
     public void repositionChild(IWindow window, int left, int top, int right, int bottom,
-             long deferTransactionUntilFrame, Rect outFrame) {
+            int requestedWidth, int requestedHeight,
+            long deferTransactionUntilFrame, Rect outFrame) {
         mService.repositionChild(this, window, left, top, right, bottom,
+                requestedWidth, requestedHeight,
                 deferTransactionUntilFrame, outFrame);
     }
 
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index b84ed7b..14291ca 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -2522,6 +2522,7 @@
 
     void repositionChild(Session session, IWindow client,
             int left, int top, int right, int bottom,
+            int requestedWidth, int requestedHeight,
             long deferTransactionUntilFrame, Rect outFrame) {
         Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "repositionChild");
         long origId = Binder.clearCallingIdentity();
@@ -2537,6 +2538,7 @@
                             "repositionChild called but window is not"
                             + "attached to a parent win=" + win);
                 }
+                win.setRequestedSize(requestedWidth, requestedHeight);
 
                 win.mAttrs.x = left;
                 win.mAttrs.y = top;
@@ -2593,7 +2595,8 @@
                         == PackageManager.PERMISSION_GRANTED;
 
         long origId = Binder.clearCallingIdentity();
-
+        final boolean preserveGeometry = (attrs != null) && (attrs.privateFlags &
+                WindowManager.LayoutParams.PRIVATE_FLAG_PRESERVE_GEOMETRY) != 0;
         synchronized(mWindowMap) {
             WindowState win = windowForClientLocked(session, client, false);
             if (win == null) {
@@ -2601,7 +2604,7 @@
             }
 
             WindowStateAnimator winAnimator = win.mWinAnimator;
-            if (viewVisibility != View.GONE) {
+            if (!preserveGeometry && viewVisibility != View.GONE) {
                 win.setRequestedSize(requestedWidth, requestedHeight);
             }
 
@@ -2650,7 +2653,9 @@
             if ((attrChanges & WindowManager.LayoutParams.ALPHA_CHANGED) != 0) {
                 winAnimator.mAlpha = attrs.alpha;
             }
-            win.setWindowScale(requestedWidth, requestedHeight);
+            if (!preserveGeometry) {
+                win.setWindowScale(win.mRequestedWidth, win.mRequestedHeight);
+            }
 
             boolean imMayMove = (flagChanges & (FLAG_ALT_FOCUSABLE_IM | FLAG_NOT_FOCUSABLE)) != 0;
             final boolean isDefaultDisplay = win.isDefaultDisplay();
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest.java
index 68199b4..28966ca 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest.java
@@ -2605,13 +2605,13 @@
         });
 
 
-        final SparseArray<UserShortcuts> users =  mService.getShortcutsForTest();
+        final SparseArray<ShortcutUser> users =  mService.getShortcutsForTest();
         assertEquals(2, users.size());
         assertEquals(USER_0, users.keyAt(0));
         assertEquals(USER_10, users.keyAt(1));
 
-        final UserShortcuts user0 =  users.get(USER_0);
-        final UserShortcuts user10 =  users.get(USER_10);
+        final ShortcutUser user0 =  users.get(USER_0);
+        final ShortcutUser user10 =  users.get(USER_10);
 
 
         // Check the registered packages.
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindowSession.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindowSession.java
index fe05b0e..53adb41 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindowSession.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindowSession.java
@@ -96,7 +96,8 @@
     }
 
     @Override
-    public void repositionChild(IWindow childWindow, int x, int y, int width, int height,
+    public void repositionChild(IWindow window, int left, int top, int right, int bottom,
+            int requestedWidth, int requestedHeight,
             long deferTransactionUntilFrame, Rect outFrame) {
         // pass for now.
         return;
diff --git a/wifi/java/android/net/wifi/WifiManager.java b/wifi/java/android/net/wifi/WifiManager.java
index 823fd26..8c1fbc3 100644
--- a/wifi/java/android/net/wifi/WifiManager.java
+++ b/wifi/java/android/net/wifi/WifiManager.java
@@ -671,9 +671,7 @@
 
     private AsyncChannel mAsyncChannel;
     private CountDownLatch mConnected;
-
-    /* TODO(b/27432949): Use a common connectivity thread for this. */
-    private HandlerThread mHandlerThread;
+    private Looper mLooper;
 
     /**
      * Create a new WifiManager instance.
@@ -685,11 +683,11 @@
      * @hide - hide this because it takes in a parameter of type IWifiManager, which
      * is a system private class.
      */
-    public WifiManager(Context context, IWifiManager service) {
+    public WifiManager(Context context, IWifiManager service, Looper looper) {
         mContext = context;
         mService = service;
+        mLooper = looper;
         mTargetSdkVersion = context.getApplicationInfo().targetSdkVersion;
-        init();
     }
 
     /**
@@ -1478,8 +1476,7 @@
      * @hide for CTS test only
      */
     public void getTxPacketCount(TxPacketCountListener listener) {
-        validateChannel();
-        mAsyncChannel.sendMessage(RSSI_PKTCNT_FETCH, 0, putListener(listener));
+        getChannel().sendMessage(RSSI_PKTCNT_FETCH, 0, putListener(listener));
     }
 
     /**
@@ -1972,30 +1969,26 @@
         }
     }
 
-    private void init() {
-        Messenger messenger = getWifiServiceMessenger();
-        if (messenger == null) {
-            mAsyncChannel = null;
-            return;
+    private synchronized AsyncChannel getChannel() {
+        if (mAsyncChannel == null) {
+            Messenger messenger = getWifiServiceMessenger();
+            if (messenger == null) {
+                throw new IllegalStateException(
+                        "getWifiServiceMessenger() returned null!  This is invalid.");
+            }
+
+            mAsyncChannel = new AsyncChannel();
+            mConnected = new CountDownLatch(1);
+
+            Handler handler = new ServiceHandler(mLooper);
+            mAsyncChannel.connect(mContext, handler, messenger);
+            try {
+                mConnected.await();
+            } catch (InterruptedException e) {
+                Log.e(TAG, "interrupted wait at init");
+            }
         }
-
-        mHandlerThread = new HandlerThread("WifiManager");
-        mAsyncChannel = new AsyncChannel();
-        mConnected = new CountDownLatch(1);
-
-        mHandlerThread.start();
-        Handler handler = new ServiceHandler(mHandlerThread.getLooper());
-        mAsyncChannel.connect(mContext, handler, messenger);
-        try {
-            mConnected.await();
-        } catch (InterruptedException e) {
-            Log.e(TAG, "interrupted wait at init");
-        }
-    }
-
-    private void validateChannel() {
-        if (mAsyncChannel == null) throw new IllegalStateException(
-                "No permission to access and change wifi or a bad initialization");
+        return mAsyncChannel;
     }
 
     /**
@@ -2016,10 +2009,9 @@
      */
     public void connect(WifiConfiguration config, ActionListener listener) {
         if (config == null) throw new IllegalArgumentException("config cannot be null");
-        validateChannel();
         // Use INVALID_NETWORK_ID for arg1 when passing a config object
         // arg1 is used to pass network id when the network already exists
-        mAsyncChannel.sendMessage(CONNECT_NETWORK, WifiConfiguration.INVALID_NETWORK_ID,
+        getChannel().sendMessage(CONNECT_NETWORK, WifiConfiguration.INVALID_NETWORK_ID,
                 putListener(listener), config);
     }
 
@@ -2038,8 +2030,7 @@
      */
     public void connect(int networkId, ActionListener listener) {
         if (networkId < 0) throw new IllegalArgumentException("Network id cannot be negative");
-        validateChannel();
-        mAsyncChannel.sendMessage(CONNECT_NETWORK, networkId, putListener(listener));
+        getChannel().sendMessage(CONNECT_NETWORK, networkId, putListener(listener));
     }
 
     /**
@@ -2062,8 +2053,7 @@
      */
     public void save(WifiConfiguration config, ActionListener listener) {
         if (config == null) throw new IllegalArgumentException("config cannot be null");
-        validateChannel();
-        mAsyncChannel.sendMessage(SAVE_NETWORK, 0, putListener(listener), config);
+        getChannel().sendMessage(SAVE_NETWORK, 0, putListener(listener), config);
     }
 
     /**
@@ -2081,8 +2071,7 @@
      */
     public void forget(int netId, ActionListener listener) {
         if (netId < 0) throw new IllegalArgumentException("Network id cannot be negative");
-        validateChannel();
-        mAsyncChannel.sendMessage(FORGET_NETWORK, netId, putListener(listener));
+        getChannel().sendMessage(FORGET_NETWORK, netId, putListener(listener));
     }
 
     /**
@@ -2096,8 +2085,7 @@
      */
     public void disable(int netId, ActionListener listener) {
         if (netId < 0) throw new IllegalArgumentException("Network id cannot be negative");
-        validateChannel();
-        mAsyncChannel.sendMessage(DISABLE_NETWORK, netId, putListener(listener));
+        getChannel().sendMessage(DISABLE_NETWORK, netId, putListener(listener));
     }
 
     /**
@@ -2125,8 +2113,7 @@
      */
     public void startWps(WpsInfo config, WpsCallback listener) {
         if (config == null) throw new IllegalArgumentException("config cannot be null");
-        validateChannel();
-        mAsyncChannel.sendMessage(START_WPS, 0, putListener(listener), config);
+        getChannel().sendMessage(START_WPS, 0, putListener(listener), config);
     }
 
     /**
@@ -2137,8 +2124,7 @@
      * initialized again
      */
     public void cancelWps(WpsCallback listener) {
-        validateChannel();
-        mAsyncChannel.sendMessage(CANCEL_WPS, 0, putListener(listener));
+        getChannel().sendMessage(CANCEL_WPS, 0, putListener(listener));
     }
 
     /**
@@ -2153,8 +2139,6 @@
             return mService.getWifiServiceMessenger();
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
-        } catch (SecurityException e) {
-            return null;
         }
     }