Merge "Update bmgr cmd line tool to use requestBackup() API in BackupManager"
diff --git a/api/current.txt b/api/current.txt
index 9a5ffdc..e50eba1 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -5807,6 +5807,7 @@
     method public java.lang.String getLongSupportMessage(android.content.ComponentName);
     method public int getMaximumFailedPasswordsForWipe(android.content.ComponentName);
     method public long getMaximumTimeToLock(android.content.ComponentName);
+    method public int getOrganizationColor(android.content.ComponentName);
     method public boolean getPackageSuspended(android.content.ComponentName, java.lang.String);
     method public android.app.admin.DevicePolicyManager getParentProfileInstance(android.content.ComponentName);
     method public long getPasswordExpiration(android.content.ComponentName);
@@ -5875,6 +5876,7 @@
     method public void setMasterVolumeMuted(android.content.ComponentName, boolean);
     method public void setMaximumFailedPasswordsForWipe(android.content.ComponentName, int);
     method public void setMaximumTimeToLock(android.content.ComponentName, long);
+    method public void setOrganizationColor(android.content.ComponentName, int);
     method public boolean setPackageSuspended(android.content.ComponentName, java.lang.String, boolean);
     method public void setPasswordExpirationTimeout(android.content.ComponentName, long);
     method public void setPasswordHistoryLength(android.content.ComponentName, int);
@@ -19297,6 +19299,7 @@
     method public void adjustVolume(int, int);
     method public void dispatchMediaKeyEvent(android.view.KeyEvent);
     method public int generateAudioSessionId();
+    method public android.media.AudioRecordConfiguration[] getActiveRecordConfigurations();
     method public android.media.AudioDeviceInfo[] getDevices(int);
     method public int getMode();
     method public java.lang.String getParameters(java.lang.String);
@@ -19319,6 +19322,7 @@
     method public void playSoundEffect(int);
     method public void playSoundEffect(int, float);
     method public void registerAudioDeviceCallback(android.media.AudioDeviceCallback, android.os.Handler);
+    method public void registerAudioRecordingCallback(android.media.AudioManager.AudioRecordingCallback, android.os.Handler);
     method public deprecated void registerMediaButtonEventReceiver(android.content.ComponentName);
     method public deprecated void registerMediaButtonEventReceiver(android.app.PendingIntent);
     method public deprecated void registerRemoteControlClient(android.media.RemoteControlClient);
@@ -19342,6 +19346,7 @@
     method public void stopBluetoothSco();
     method public void unloadSoundEffects();
     method public void unregisterAudioDeviceCallback(android.media.AudioDeviceCallback);
+    method public void unregisterAudioRecordingCallback(android.media.AudioManager.AudioRecordingCallback);
     method public deprecated void unregisterMediaButtonEventReceiver(android.content.ComponentName);
     method public deprecated void unregisterMediaButtonEventReceiver(android.app.PendingIntent);
     method public deprecated void unregisterRemoteControlClient(android.media.RemoteControlClient);
@@ -19438,6 +19443,11 @@
     field public static final deprecated int VIBRATE_TYPE_RINGER = 0; // 0x0
   }
 
+  public static abstract class AudioManager.AudioRecordingCallback {
+    ctor public AudioManager.AudioRecordingCallback();
+    method public void onRecordConfigChanged();
+  }
+
   public static abstract interface AudioManager.OnAudioFocusChangeListener {
     method public abstract void onAudioFocusChange(int);
   }
@@ -19508,6 +19518,14 @@
     method public abstract void onRoutingChanged(android.media.AudioRecord);
   }
 
+  public class AudioRecordConfiguration implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getAudioSessionId();
+    method public int getClientAudioSource();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.media.AudioRecordConfiguration> CREATOR;
+  }
+
   public abstract interface AudioRouting {
     method public abstract void addOnRoutingListener(android.media.AudioRouting.OnRoutingChangedListener, android.os.Handler);
     method public abstract android.media.AudioDeviceInfo getPreferredDevice();
@@ -43590,6 +43608,7 @@
     field public android.os.Bundle extras;
     field public int fieldId;
     field public java.lang.String fieldName;
+    field public android.util.LocaleList hintLocales;
     field public java.lang.CharSequence hintText;
     field public int imeOptions;
     field public int initialCapsMode;
@@ -43597,7 +43616,6 @@
     field public int initialSelStart;
     field public int inputType;
     field public java.lang.CharSequence label;
-    field public android.util.LocaleList locales;
     field public java.lang.String packageName;
     field public java.lang.String privateImeOptions;
   }
@@ -46844,6 +46862,7 @@
     method public int getHyphenationFrequency();
     method public int getImeActionId();
     method public java.lang.CharSequence getImeActionLabel();
+    method public android.util.LocaleList getImeHintLocales();
     method public int getImeOptions();
     method public boolean getIncludeFontPadding();
     method public android.os.Bundle getInputExtras(boolean);
@@ -46950,6 +46969,7 @@
     method public void setHorizontallyScrolling(boolean);
     method public void setHyphenationFrequency(int);
     method public void setImeActionLabel(java.lang.CharSequence, int);
+    method public void setImeHintLocales(android.util.LocaleList);
     method public void setImeOptions(int);
     method public void setIncludeFontPadding(boolean);
     method public void setInputExtras(int) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
@@ -47903,10 +47923,8 @@
   }
 
   public final class Console implements java.io.Flushable {
-    method public static java.io.Console console();
     method public void flush();
     method public java.io.Console format(java.lang.String, java.lang.Object...);
-    method public static synchronized java.io.Console getConsole();
     method public java.io.Console printf(java.lang.String, java.lang.Object...);
     method public java.lang.String readLine(java.lang.String, java.lang.Object...);
     method public java.lang.String readLine();
@@ -48190,7 +48208,6 @@
   public class InterruptedIOException extends java.io.IOException {
     ctor public InterruptedIOException();
     ctor public InterruptedIOException(java.lang.String);
-    ctor public InterruptedIOException(java.lang.Throwable);
     field public int bytesTransferred;
   }
 
@@ -48861,7 +48878,6 @@
     method public long longValue();
     method public static byte parseByte(java.lang.String, int) throws java.lang.NumberFormatException;
     method public static byte parseByte(java.lang.String) throws java.lang.NumberFormatException;
-    method public static java.lang.String toHexString(byte, boolean);
     method public static java.lang.String toString(byte);
     method public static java.lang.Byte valueOf(byte);
     method public static java.lang.Byte valueOf(java.lang.String, int) throws java.lang.NumberFormatException;
@@ -51013,7 +51029,6 @@
   public class BindException extends java.net.SocketException {
     ctor public BindException(java.lang.String);
     ctor public BindException();
-    ctor public BindException(java.lang.String, java.lang.Throwable);
   }
 
   public abstract class CacheRequest {
@@ -51031,7 +51046,6 @@
   public class ConnectException extends java.net.SocketException {
     ctor public ConnectException(java.lang.String);
     ctor public ConnectException();
-    ctor public ConnectException(java.lang.String, java.lang.Throwable);
   }
 
   public abstract class ContentHandler {
@@ -51111,7 +51125,6 @@
     method public void disconnect();
     method public synchronized boolean getBroadcast() throws java.net.SocketException;
     method public java.nio.channels.DatagramChannel getChannel();
-    method public final java.io.FileDescriptor getFileDescriptor$();
     method public java.net.InetAddress getInetAddress();
     method public java.net.InetAddress getLocalAddress();
     method public int getLocalPort();
@@ -51188,7 +51201,6 @@
     method public boolean hasExpired();
     method public boolean isHttpOnly();
     method public static java.util.List<java.net.HttpCookie> parse(java.lang.String);
-    method public static java.util.List<java.net.HttpCookie> parse(java.lang.String, boolean);
     method public void setComment(java.lang.String);
     method public void setCommentURL(java.lang.String);
     method public void setDiscard(boolean);
@@ -51281,9 +51293,6 @@
   }
 
   public final class Inet4Address extends java.net.InetAddress {
-    field public static final java.net.InetAddress ALL;
-    field public static final java.net.InetAddress ANY;
-    field public static final java.net.InetAddress LOOPBACK;
   }
 
   public final class Inet6Address extends java.net.InetAddress {
@@ -51292,8 +51301,6 @@
     method public int getScopeId();
     method public java.net.NetworkInterface getScopedInterface();
     method public boolean isIPv4CompatibleAddress();
-    field public static final java.net.InetAddress ANY;
-    field public static final java.net.InetAddress LOOPBACK;
   }
 
   public class InetAddress implements java.io.Serializable {
@@ -51421,13 +51428,11 @@
   public class PortUnreachableException extends java.net.SocketException {
     ctor public PortUnreachableException(java.lang.String);
     ctor public PortUnreachableException();
-    ctor public PortUnreachableException(java.lang.String, java.lang.Throwable);
   }
 
   public class ProtocolException extends java.io.IOException {
     ctor public ProtocolException(java.lang.String);
     ctor public ProtocolException();
-    ctor public ProtocolException(java.lang.String, java.lang.Throwable);
   }
 
   public abstract interface ProtocolFamily {
@@ -51562,8 +51567,6 @@
   public class SocketException extends java.io.IOException {
     ctor public SocketException(java.lang.String);
     ctor public SocketException();
-    ctor public SocketException(java.lang.Throwable);
-    ctor public SocketException(java.lang.String, java.lang.Throwable);
   }
 
   public abstract class SocketImpl implements java.net.SocketOptions {
@@ -51633,8 +51636,6 @@
   public class SocketTimeoutException extends java.io.InterruptedIOException {
     ctor public SocketTimeoutException(java.lang.String);
     ctor public SocketTimeoutException();
-    ctor public SocketTimeoutException(java.lang.Throwable);
-    ctor public SocketTimeoutException(java.lang.String, java.lang.Throwable);
   }
 
   public final class StandardProtocolFamily extends java.lang.Enum implements java.net.ProtocolFamily {
@@ -59579,7 +59580,6 @@
     ctor public JarFile(java.io.File, boolean, int) throws java.io.IOException;
     method public java.util.jar.JarEntry getJarEntry(java.lang.String);
     method public java.util.jar.Manifest getManifest() throws java.io.IOException;
-    method public boolean hasClassPathAttribute() throws java.io.IOException;
     field public static final java.lang.String MANIFEST_NAME = "META-INF/MANIFEST.MF";
   }
 
diff --git a/api/system-current.txt b/api/system-current.txt
index cdbe014..2d358bf 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -5941,11 +5941,13 @@
     method public deprecated android.content.ComponentName getDeviceInitializerComponent();
     method public java.lang.String getDeviceOwner();
     method public java.lang.String getDeviceOwnerLockScreenInfo();
+    method public java.lang.String getDeviceOwnerNameOnAnyUser();
     method public java.util.List<byte[]> getInstalledCaCerts(android.content.ComponentName);
     method public int getKeyguardDisabledFeatures(android.content.ComponentName);
     method public java.lang.String getLongSupportMessage(android.content.ComponentName);
     method public int getMaximumFailedPasswordsForWipe(android.content.ComponentName);
     method public long getMaximumTimeToLock(android.content.ComponentName);
+    method public int getOrganizationColor(android.content.ComponentName);
     method public boolean getPackageSuspended(android.content.ComponentName, java.lang.String);
     method public android.app.admin.DevicePolicyManager getParentProfileInstance(android.content.ComponentName);
     method public long getPasswordExpiration(android.content.ComponentName);
@@ -6020,6 +6022,7 @@
     method public void setMasterVolumeMuted(android.content.ComponentName, boolean);
     method public void setMaximumFailedPasswordsForWipe(android.content.ComponentName, int);
     method public void setMaximumTimeToLock(android.content.ComponentName, long);
+    method public void setOrganizationColor(android.content.ComponentName, int);
     method public boolean setPackageSuspended(android.content.ComponentName, java.lang.String, boolean);
     method public void setPasswordExpirationTimeout(android.content.ComponentName, long);
     method public void setPasswordHistoryLength(android.content.ComponentName, int);
@@ -6292,6 +6295,7 @@
     method public static void dataChanged(java.lang.String);
     method public long getAvailableRestoreToken(java.lang.String);
     method public java.lang.String getCurrentTransport();
+    method public boolean isAppEligibleForBackup(java.lang.String);
     method public boolean isBackupEnabled();
     method public java.lang.String[] listAllTransports();
     method public int requestBackup(java.lang.String[], android.app.backup.BackupObserver);
@@ -6341,6 +6345,7 @@
     method public int getNextFullRestoreDataChunk(android.os.ParcelFileDescriptor);
     method public int getRestoreData(android.os.ParcelFileDescriptor);
     method public int initializeDevice();
+    method public boolean isAppEligibleForBackup(android.content.pm.PackageInfo, boolean);
     method public java.lang.String name();
     method public android.app.backup.RestoreDescription nextRestorePackage();
     method public int performBackup(android.content.pm.PackageInfo, android.os.ParcelFileDescriptor, int);
@@ -20622,6 +20627,7 @@
     method public void adjustVolume(int, int);
     method public void dispatchMediaKeyEvent(android.view.KeyEvent);
     method public int generateAudioSessionId();
+    method public android.media.AudioRecordConfiguration[] getActiveRecordConfigurations();
     method public android.media.AudioDeviceInfo[] getDevices(int);
     method public int getMode();
     method public java.lang.String getParameters(java.lang.String);
@@ -20646,6 +20652,7 @@
     method public void playSoundEffect(int, float);
     method public void registerAudioDeviceCallback(android.media.AudioDeviceCallback, android.os.Handler);
     method public int registerAudioPolicy(android.media.audiopolicy.AudioPolicy);
+    method public void registerAudioRecordingCallback(android.media.AudioManager.AudioRecordingCallback, android.os.Handler);
     method public deprecated void registerMediaButtonEventReceiver(android.content.ComponentName);
     method public deprecated void registerMediaButtonEventReceiver(android.app.PendingIntent);
     method public deprecated void registerRemoteControlClient(android.media.RemoteControlClient);
@@ -20672,6 +20679,7 @@
     method public void unloadSoundEffects();
     method public void unregisterAudioDeviceCallback(android.media.AudioDeviceCallback);
     method public void unregisterAudioPolicyAsync(android.media.audiopolicy.AudioPolicy);
+    method public void unregisterAudioRecordingCallback(android.media.AudioManager.AudioRecordingCallback);
     method public deprecated void unregisterMediaButtonEventReceiver(android.content.ComponentName);
     method public deprecated void unregisterMediaButtonEventReceiver(android.app.PendingIntent);
     method public deprecated void unregisterRemoteControlClient(android.media.RemoteControlClient);
@@ -20771,6 +20779,11 @@
     field public static final deprecated int VIBRATE_TYPE_RINGER = 0; // 0x0
   }
 
+  public static abstract class AudioManager.AudioRecordingCallback {
+    ctor public AudioManager.AudioRecordingCallback();
+    method public void onRecordConfigChanged();
+  }
+
   public static abstract interface AudioManager.OnAudioFocusChangeListener {
     method public abstract void onAudioFocusChange(int);
   }
@@ -20844,6 +20857,14 @@
     method public abstract void onRoutingChanged(android.media.AudioRecord);
   }
 
+  public class AudioRecordConfiguration implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getAudioSessionId();
+    method public int getClientAudioSource();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.media.AudioRecordConfiguration> CREATOR;
+  }
+
   public abstract interface AudioRouting {
     method public abstract void addOnRoutingListener(android.media.AudioRouting.OnRoutingChangedListener, android.os.Handler);
     method public abstract android.media.AudioDeviceInfo getPreferredDevice();
@@ -46017,6 +46038,7 @@
     field public android.os.Bundle extras;
     field public int fieldId;
     field public java.lang.String fieldName;
+    field public android.util.LocaleList hintLocales;
     field public java.lang.CharSequence hintText;
     field public int imeOptions;
     field public int initialCapsMode;
@@ -46024,7 +46046,6 @@
     field public int initialSelStart;
     field public int inputType;
     field public java.lang.CharSequence label;
-    field public android.util.LocaleList locales;
     field public java.lang.String packageName;
     field public java.lang.String privateImeOptions;
   }
@@ -49597,6 +49618,7 @@
     method public int getHyphenationFrequency();
     method public int getImeActionId();
     method public java.lang.CharSequence getImeActionLabel();
+    method public android.util.LocaleList getImeHintLocales();
     method public int getImeOptions();
     method public boolean getIncludeFontPadding();
     method public android.os.Bundle getInputExtras(boolean);
@@ -49703,6 +49725,7 @@
     method public void setHorizontallyScrolling(boolean);
     method public void setHyphenationFrequency(int);
     method public void setImeActionLabel(java.lang.CharSequence, int);
+    method public void setImeHintLocales(android.util.LocaleList);
     method public void setImeOptions(int);
     method public void setIncludeFontPadding(boolean);
     method public void setInputExtras(int) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
@@ -50656,10 +50679,8 @@
   }
 
   public final class Console implements java.io.Flushable {
-    method public static java.io.Console console();
     method public void flush();
     method public java.io.Console format(java.lang.String, java.lang.Object...);
-    method public static synchronized java.io.Console getConsole();
     method public java.io.Console printf(java.lang.String, java.lang.Object...);
     method public java.lang.String readLine(java.lang.String, java.lang.Object...);
     method public java.lang.String readLine();
@@ -50943,7 +50964,6 @@
   public class InterruptedIOException extends java.io.IOException {
     ctor public InterruptedIOException();
     ctor public InterruptedIOException(java.lang.String);
-    ctor public InterruptedIOException(java.lang.Throwable);
     field public int bytesTransferred;
   }
 
@@ -51614,7 +51634,6 @@
     method public long longValue();
     method public static byte parseByte(java.lang.String, int) throws java.lang.NumberFormatException;
     method public static byte parseByte(java.lang.String) throws java.lang.NumberFormatException;
-    method public static java.lang.String toHexString(byte, boolean);
     method public static java.lang.String toString(byte);
     method public static java.lang.Byte valueOf(byte);
     method public static java.lang.Byte valueOf(java.lang.String, int) throws java.lang.NumberFormatException;
@@ -53766,7 +53785,6 @@
   public class BindException extends java.net.SocketException {
     ctor public BindException(java.lang.String);
     ctor public BindException();
-    ctor public BindException(java.lang.String, java.lang.Throwable);
   }
 
   public abstract class CacheRequest {
@@ -53784,7 +53802,6 @@
   public class ConnectException extends java.net.SocketException {
     ctor public ConnectException(java.lang.String);
     ctor public ConnectException();
-    ctor public ConnectException(java.lang.String, java.lang.Throwable);
   }
 
   public abstract class ContentHandler {
@@ -53864,7 +53881,6 @@
     method public void disconnect();
     method public synchronized boolean getBroadcast() throws java.net.SocketException;
     method public java.nio.channels.DatagramChannel getChannel();
-    method public final java.io.FileDescriptor getFileDescriptor$();
     method public java.net.InetAddress getInetAddress();
     method public java.net.InetAddress getLocalAddress();
     method public int getLocalPort();
@@ -53941,7 +53957,6 @@
     method public boolean hasExpired();
     method public boolean isHttpOnly();
     method public static java.util.List<java.net.HttpCookie> parse(java.lang.String);
-    method public static java.util.List<java.net.HttpCookie> parse(java.lang.String, boolean);
     method public void setComment(java.lang.String);
     method public void setCommentURL(java.lang.String);
     method public void setDiscard(boolean);
@@ -54034,9 +54049,6 @@
   }
 
   public final class Inet4Address extends java.net.InetAddress {
-    field public static final java.net.InetAddress ALL;
-    field public static final java.net.InetAddress ANY;
-    field public static final java.net.InetAddress LOOPBACK;
   }
 
   public final class Inet6Address extends java.net.InetAddress {
@@ -54045,8 +54057,6 @@
     method public int getScopeId();
     method public java.net.NetworkInterface getScopedInterface();
     method public boolean isIPv4CompatibleAddress();
-    field public static final java.net.InetAddress ANY;
-    field public static final java.net.InetAddress LOOPBACK;
   }
 
   public class InetAddress implements java.io.Serializable {
@@ -54174,13 +54184,11 @@
   public class PortUnreachableException extends java.net.SocketException {
     ctor public PortUnreachableException(java.lang.String);
     ctor public PortUnreachableException();
-    ctor public PortUnreachableException(java.lang.String, java.lang.Throwable);
   }
 
   public class ProtocolException extends java.io.IOException {
     ctor public ProtocolException(java.lang.String);
     ctor public ProtocolException();
-    ctor public ProtocolException(java.lang.String, java.lang.Throwable);
   }
 
   public abstract interface ProtocolFamily {
@@ -54315,8 +54323,6 @@
   public class SocketException extends java.io.IOException {
     ctor public SocketException(java.lang.String);
     ctor public SocketException();
-    ctor public SocketException(java.lang.Throwable);
-    ctor public SocketException(java.lang.String, java.lang.Throwable);
   }
 
   public abstract class SocketImpl implements java.net.SocketOptions {
@@ -54386,8 +54392,6 @@
   public class SocketTimeoutException extends java.io.InterruptedIOException {
     ctor public SocketTimeoutException(java.lang.String);
     ctor public SocketTimeoutException();
-    ctor public SocketTimeoutException(java.lang.Throwable);
-    ctor public SocketTimeoutException(java.lang.String, java.lang.Throwable);
   }
 
   public final class StandardProtocolFamily extends java.lang.Enum implements java.net.ProtocolFamily {
@@ -62332,7 +62336,6 @@
     ctor public JarFile(java.io.File, boolean, int) throws java.io.IOException;
     method public java.util.jar.JarEntry getJarEntry(java.lang.String);
     method public java.util.jar.Manifest getManifest() throws java.io.IOException;
-    method public boolean hasClassPathAttribute() throws java.io.IOException;
     field public static final java.lang.String MANIFEST_NAME = "META-INF/MANIFEST.MF";
   }
 
diff --git a/api/test-current.txt b/api/test-current.txt
index 5f76739..bcdf5c1 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -5809,6 +5809,7 @@
     method public java.lang.String getLongSupportMessage(android.content.ComponentName);
     method public int getMaximumFailedPasswordsForWipe(android.content.ComponentName);
     method public long getMaximumTimeToLock(android.content.ComponentName);
+    method public int getOrganizationColor(android.content.ComponentName);
     method public boolean getPackageSuspended(android.content.ComponentName, java.lang.String);
     method public android.app.admin.DevicePolicyManager getParentProfileInstance(android.content.ComponentName);
     method public long getPasswordExpiration(android.content.ComponentName);
@@ -5877,6 +5878,7 @@
     method public void setMasterVolumeMuted(android.content.ComponentName, boolean);
     method public void setMaximumFailedPasswordsForWipe(android.content.ComponentName, int);
     method public void setMaximumTimeToLock(android.content.ComponentName, long);
+    method public void setOrganizationColor(android.content.ComponentName, int);
     method public boolean setPackageSuspended(android.content.ComponentName, java.lang.String, boolean);
     method public void setPasswordExpirationTimeout(android.content.ComponentName, long);
     method public void setPasswordHistoryLength(android.content.ComponentName, int);
@@ -19305,6 +19307,7 @@
     method public void adjustVolume(int, int);
     method public void dispatchMediaKeyEvent(android.view.KeyEvent);
     method public int generateAudioSessionId();
+    method public android.media.AudioRecordConfiguration[] getActiveRecordConfigurations();
     method public android.media.AudioDeviceInfo[] getDevices(int);
     method public int getMode();
     method public java.lang.String getParameters(java.lang.String);
@@ -19327,6 +19330,7 @@
     method public void playSoundEffect(int);
     method public void playSoundEffect(int, float);
     method public void registerAudioDeviceCallback(android.media.AudioDeviceCallback, android.os.Handler);
+    method public void registerAudioRecordingCallback(android.media.AudioManager.AudioRecordingCallback, android.os.Handler);
     method public deprecated void registerMediaButtonEventReceiver(android.content.ComponentName);
     method public deprecated void registerMediaButtonEventReceiver(android.app.PendingIntent);
     method public deprecated void registerRemoteControlClient(android.media.RemoteControlClient);
@@ -19350,6 +19354,7 @@
     method public void stopBluetoothSco();
     method public void unloadSoundEffects();
     method public void unregisterAudioDeviceCallback(android.media.AudioDeviceCallback);
+    method public void unregisterAudioRecordingCallback(android.media.AudioManager.AudioRecordingCallback);
     method public deprecated void unregisterMediaButtonEventReceiver(android.content.ComponentName);
     method public deprecated void unregisterMediaButtonEventReceiver(android.app.PendingIntent);
     method public deprecated void unregisterRemoteControlClient(android.media.RemoteControlClient);
@@ -19446,6 +19451,11 @@
     field public static final deprecated int VIBRATE_TYPE_RINGER = 0; // 0x0
   }
 
+  public static abstract class AudioManager.AudioRecordingCallback {
+    ctor public AudioManager.AudioRecordingCallback();
+    method public void onRecordConfigChanged();
+  }
+
   public static abstract interface AudioManager.OnAudioFocusChangeListener {
     method public abstract void onAudioFocusChange(int);
   }
@@ -19516,6 +19526,14 @@
     method public abstract void onRoutingChanged(android.media.AudioRecord);
   }
 
+  public class AudioRecordConfiguration implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getAudioSessionId();
+    method public int getClientAudioSource();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.media.AudioRecordConfiguration> CREATOR;
+  }
+
   public abstract interface AudioRouting {
     method public abstract void addOnRoutingListener(android.media.AudioRouting.OnRoutingChangedListener, android.os.Handler);
     method public abstract android.media.AudioDeviceInfo getPreferredDevice();
@@ -43606,6 +43624,7 @@
     field public android.os.Bundle extras;
     field public int fieldId;
     field public java.lang.String fieldName;
+    field public android.util.LocaleList hintLocales;
     field public java.lang.CharSequence hintText;
     field public int imeOptions;
     field public int initialCapsMode;
@@ -43613,7 +43632,6 @@
     field public int initialSelStart;
     field public int inputType;
     field public java.lang.CharSequence label;
-    field public android.util.LocaleList locales;
     field public java.lang.String packageName;
     field public java.lang.String privateImeOptions;
   }
@@ -46860,6 +46878,7 @@
     method public int getHyphenationFrequency();
     method public int getImeActionId();
     method public java.lang.CharSequence getImeActionLabel();
+    method public android.util.LocaleList getImeHintLocales();
     method public int getImeOptions();
     method public boolean getIncludeFontPadding();
     method public android.os.Bundle getInputExtras(boolean);
@@ -46966,6 +46985,7 @@
     method public void setHorizontallyScrolling(boolean);
     method public void setHyphenationFrequency(int);
     method public void setImeActionLabel(java.lang.CharSequence, int);
+    method public void setImeHintLocales(android.util.LocaleList);
     method public void setImeOptions(int);
     method public void setIncludeFontPadding(boolean);
     method public void setInputExtras(int) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
@@ -47919,10 +47939,8 @@
   }
 
   public final class Console implements java.io.Flushable {
-    method public static java.io.Console console();
     method public void flush();
     method public java.io.Console format(java.lang.String, java.lang.Object...);
-    method public static synchronized java.io.Console getConsole();
     method public java.io.Console printf(java.lang.String, java.lang.Object...);
     method public java.lang.String readLine(java.lang.String, java.lang.Object...);
     method public java.lang.String readLine();
@@ -48206,7 +48224,6 @@
   public class InterruptedIOException extends java.io.IOException {
     ctor public InterruptedIOException();
     ctor public InterruptedIOException(java.lang.String);
-    ctor public InterruptedIOException(java.lang.Throwable);
     field public int bytesTransferred;
   }
 
@@ -48877,7 +48894,6 @@
     method public long longValue();
     method public static byte parseByte(java.lang.String, int) throws java.lang.NumberFormatException;
     method public static byte parseByte(java.lang.String) throws java.lang.NumberFormatException;
-    method public static java.lang.String toHexString(byte, boolean);
     method public static java.lang.String toString(byte);
     method public static java.lang.Byte valueOf(byte);
     method public static java.lang.Byte valueOf(java.lang.String, int) throws java.lang.NumberFormatException;
@@ -51029,7 +51045,6 @@
   public class BindException extends java.net.SocketException {
     ctor public BindException(java.lang.String);
     ctor public BindException();
-    ctor public BindException(java.lang.String, java.lang.Throwable);
   }
 
   public abstract class CacheRequest {
@@ -51047,7 +51062,6 @@
   public class ConnectException extends java.net.SocketException {
     ctor public ConnectException(java.lang.String);
     ctor public ConnectException();
-    ctor public ConnectException(java.lang.String, java.lang.Throwable);
   }
 
   public abstract class ContentHandler {
@@ -51127,7 +51141,6 @@
     method public void disconnect();
     method public synchronized boolean getBroadcast() throws java.net.SocketException;
     method public java.nio.channels.DatagramChannel getChannel();
-    method public final java.io.FileDescriptor getFileDescriptor$();
     method public java.net.InetAddress getInetAddress();
     method public java.net.InetAddress getLocalAddress();
     method public int getLocalPort();
@@ -51204,7 +51217,6 @@
     method public boolean hasExpired();
     method public boolean isHttpOnly();
     method public static java.util.List<java.net.HttpCookie> parse(java.lang.String);
-    method public static java.util.List<java.net.HttpCookie> parse(java.lang.String, boolean);
     method public void setComment(java.lang.String);
     method public void setCommentURL(java.lang.String);
     method public void setDiscard(boolean);
@@ -51297,9 +51309,6 @@
   }
 
   public final class Inet4Address extends java.net.InetAddress {
-    field public static final java.net.InetAddress ALL;
-    field public static final java.net.InetAddress ANY;
-    field public static final java.net.InetAddress LOOPBACK;
   }
 
   public final class Inet6Address extends java.net.InetAddress {
@@ -51308,8 +51317,6 @@
     method public int getScopeId();
     method public java.net.NetworkInterface getScopedInterface();
     method public boolean isIPv4CompatibleAddress();
-    field public static final java.net.InetAddress ANY;
-    field public static final java.net.InetAddress LOOPBACK;
   }
 
   public class InetAddress implements java.io.Serializable {
@@ -51437,13 +51444,11 @@
   public class PortUnreachableException extends java.net.SocketException {
     ctor public PortUnreachableException(java.lang.String);
     ctor public PortUnreachableException();
-    ctor public PortUnreachableException(java.lang.String, java.lang.Throwable);
   }
 
   public class ProtocolException extends java.io.IOException {
     ctor public ProtocolException(java.lang.String);
     ctor public ProtocolException();
-    ctor public ProtocolException(java.lang.String, java.lang.Throwable);
   }
 
   public abstract interface ProtocolFamily {
@@ -51578,8 +51583,6 @@
   public class SocketException extends java.io.IOException {
     ctor public SocketException(java.lang.String);
     ctor public SocketException();
-    ctor public SocketException(java.lang.Throwable);
-    ctor public SocketException(java.lang.String, java.lang.Throwable);
   }
 
   public abstract class SocketImpl implements java.net.SocketOptions {
@@ -51649,8 +51652,6 @@
   public class SocketTimeoutException extends java.io.InterruptedIOException {
     ctor public SocketTimeoutException(java.lang.String);
     ctor public SocketTimeoutException();
-    ctor public SocketTimeoutException(java.lang.Throwable);
-    ctor public SocketTimeoutException(java.lang.String, java.lang.Throwable);
   }
 
   public final class StandardProtocolFamily extends java.lang.Enum implements java.net.ProtocolFamily {
@@ -59595,7 +59596,6 @@
     ctor public JarFile(java.io.File, boolean, int) throws java.io.IOException;
     method public java.util.jar.JarEntry getJarEntry(java.lang.String);
     method public java.util.jar.Manifest getManifest() throws java.io.IOException;
-    method public boolean hasClassPathAttribute() throws java.io.IOException;
     field public static final java.lang.String MANIFEST_NAME = "META-INF/MANIFEST.MF";
   }
 
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index 0afca9d..220fb607 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -1099,13 +1099,24 @@
 
     @Override
     public Drawable getUserBadgeForDensity(UserHandle user, int density) {
+        return getManagedProfileIconForDensity(user, density,
+                com.android.internal.R.drawable.ic_corp_badge);
+    }
+
+    @Override
+    public Drawable getUserBadgeForDensityNoBackground(UserHandle user, int density) {
+        return getManagedProfileIconForDensity(user, density,
+                com.android.internal.R.drawable.ic_corp_badge_no_background);
+    }
+
+    private Drawable getManagedProfileIconForDensity(UserHandle user, int density,
+            int drawableId) {
         UserInfo userInfo = getUserIfProfile(user.getIdentifier());
         if (userInfo != null && userInfo.isManagedProfile()) {
             if (density <= 0) {
                 density = mContext.getResources().getDisplayMetrics().densityDpi;
             }
-            return Resources.getSystem().getDrawableForDensity(
-                    com.android.internal.R.drawable.ic_corp_badge, density);
+            return Resources.getSystem().getDrawableForDensity(drawableId, density);
         }
         return null;
     }
diff --git a/core/java/android/app/IntentService.java b/core/java/android/app/IntentService.java
index 3cda973..f33af39 100644
--- a/core/java/android/app/IntentService.java
+++ b/core/java/android/app/IntentService.java
@@ -17,6 +17,7 @@
 package android.app;
 
 import android.annotation.WorkerThread;
+import android.annotation.Nullable;
 import android.content.Intent;
 import android.os.Handler;
 import android.os.HandlerThread;
@@ -113,7 +114,7 @@
     }
 
     @Override
-    public void onStart(Intent intent, int startId) {
+    public void onStart(@Nullable Intent intent, int startId) {
         Message msg = mServiceHandler.obtainMessage();
         msg.arg1 = startId;
         msg.obj = intent;
@@ -127,7 +128,7 @@
      * @see android.app.Service#onStartCommand
      */
     @Override
-    public int onStartCommand(Intent intent, int flags, int startId) {
+    public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
         onStart(intent, startId);
         return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
     }
@@ -139,10 +140,11 @@
 
     /**
      * Unless you provide binding for your service, you don't need to implement this
-     * method, because the default implementation returns null. 
+     * method, because the default implementation returns null.
      * @see android.app.Service#onBind
      */
     @Override
+    @Nullable
     public IBinder onBind(Intent intent) {
         return null;
     }
@@ -158,7 +160,11 @@
      *
      * @param intent The value passed to {@link
      *               android.content.Context#startService(Intent)}.
+     *               This may be null if the service is being restarted after
+     *               its process has gone away; see
+     *               {@link android.app.Service#onStartCommand}
+     *               for details.
      */
     @WorkerThread
-    protected abstract void onHandleIntent(Intent intent);
+    protected abstract void onHandleIntent(@Nullable Intent intent);
 }
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index a392abd..0f3aad9 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -2998,7 +2998,7 @@
         private Drawable getProfileBadgeDrawable() {
             // Note: This assumes that the current user can read the profile badge of the
             // originating user.
-            return mContext.getPackageManager().getUserBadgeForDensity(
+            return mContext.getPackageManager().getUserBadgeForDensityNoBackground(
                     new UserHandle(mContext.getUserId()), 0);
         }
 
@@ -3016,24 +3016,13 @@
             return bitmap;
         }
 
-        private boolean addProfileBadge(RemoteViews contentView, int resId) {
+        private void bindProfileBadge(RemoteViews contentView) {
             Bitmap profileBadge = getProfileBadge();
 
-            contentView.setViewVisibility(R.id.profile_badge_large_template, View.GONE);
-            contentView.setViewVisibility(R.id.profile_badge_line3, View.GONE);
-
             if (profileBadge != null) {
-                contentView.setImageViewBitmap(resId, profileBadge);
-                contentView.setViewVisibility(resId, View.VISIBLE);
-
-                // Make sure Line 3 is visible. As badge will be here if there
-                // is no text to display.
-                if (resId == R.id.profile_badge_line3) {
-                    contentView.setViewVisibility(R.id.line3, View.VISIBLE);
-                }
-                return true;
+                contentView.setImageViewBitmap(R.id.profile_badge, profileBadge);
+                contentView.setViewVisibility(R.id.profile_badge, View.VISIBLE);
             }
-            return false;
         }
 
         private void resetStandardTemplate(RemoteViews contentView) {
@@ -3042,9 +3031,10 @@
             contentView.setViewVisibility(R.id.right_icon, View.GONE);
             contentView.setViewVisibility(R.id.title, View.GONE);
             contentView.setTextViewText(R.id.title, null);
+            contentView.setViewVisibility(R.id.text, View.GONE);
             contentView.setTextViewText(R.id.text, null);
-            contentView.setViewVisibility(R.id.line3, View.GONE);
             contentView.setViewVisibility(R.id.text_line_1, View.GONE);
+            contentView.setTextViewText(R.id.text_line_1, null);
             contentView.setViewVisibility(R.id.progress, View.GONE);
         }
 
@@ -3062,11 +3052,13 @@
             contentView.setViewVisibility(R.id.sub_text_divider, View.GONE);
             contentView.setViewVisibility(R.id.content_info_divider, View.GONE);
             contentView.setViewVisibility(R.id.time_divider, View.GONE);
+            contentView.setImageViewIcon(R.id.profile_badge, null);
+            contentView.setViewVisibility(R.id.profile_badge, View.GONE);
         }
 
         private void resetContentMargins(RemoteViews contentView) {
             contentView.setViewLayoutMarginEnd(R.id.line1, 0);
-            contentView.setViewLayoutMarginEnd(R.id.line3, 0);
+            contentView.setViewLayoutMarginEnd(R.id.text, 0);
         }
 
         private RemoteViews applyStandardTemplate(int resId) {
@@ -3081,7 +3073,6 @@
 
             resetStandardTemplate(contentView);
 
-            boolean showLine3 = false;
             final Bundle ex = mN.extras;
 
             bindNotificationHeader(contentView);
@@ -3093,19 +3084,13 @@
             }
             boolean showProgress = handleProgressBar(hasProgress, contentView, ex);
             if (ex.getCharSequence(EXTRA_TEXT) != null) {
-                contentView.setTextViewText(showProgress ? R.id.text_line_1 : R.id.text,
-                        processLegacyText(ex.getCharSequence(EXTRA_TEXT)));
-                if (showProgress) {
-                    contentView.setViewVisibility(R.id.text_line_1, View.VISIBLE);
-                }
-                showLine3 = !showProgress;
+                int textId = showProgress ? com.android.internal.R.id.text_line_1
+                        : com.android.internal.R.id.text;
+                contentView.setTextViewText(textId, processLegacyText(
+                        ex.getCharSequence(EXTRA_TEXT)));
+                contentView.setViewVisibility(textId, View.VISIBLE);
             }
-            // We want to add badge to first line of text.
-            if (addProfileBadge(contentView, R.id.profile_badge_line3)) {
-                showLine3 = true;
-            }
-            // Note getStandardView may hide line 3 again.
-            contentView.setViewVisibility(R.id.line3, showLine3 ? View.VISIBLE : View.GONE);
+
             setContentMinHeight(contentView, showProgress || mN.mLargeIcon != null);
 
             return contentView;
@@ -3157,7 +3142,7 @@
                 int endMargin = mContext.getResources().getDimensionPixelSize(
                         R.dimen.notification_content_picture_margin);
                 contentView.setViewLayoutMarginEnd(R.id.line1, endMargin);
-                contentView.setViewLayoutMarginEnd(R.id.line3, endMargin);
+                contentView.setViewLayoutMarginEnd(R.id.text, endMargin);
                 contentView.setViewLayoutMarginEnd(R.id.progress, endMargin);
             }
         }
@@ -3170,6 +3155,7 @@
             bindContentInfo(contentView);
             bindHeaderChronometerAndTime(contentView);
             bindExpandButton(contentView);
+            bindProfileBadge(contentView);
         }
 
         private void bindChildCountColor(RemoteViews contentView) {
@@ -3729,10 +3715,6 @@
                 contentView.setViewVisibility(R.id.line1, View.VISIBLE);
             }
 
-            // Clear text in case we use the line to show the profile badge.
-            contentView.setTextViewText(com.android.internal.R.id.text, "");
-            contentView.setViewVisibility(com.android.internal.R.id.line3, View.GONE);
-
             return contentView;
         }
 
@@ -3939,7 +3921,7 @@
             RemoteViews contentView = getStandardView(mBuilder.getBigPictureLayoutResource());
             if (mSummaryTextSet) {
                 contentView.setTextViewText(R.id.text, mBuilder.processLegacyText(mSummaryText));
-                contentView.setViewVisibility(R.id.line3, View.VISIBLE);
+                contentView.setViewVisibility(R.id.text, View.VISIBLE);
             }
             mBuilder.setContentMinHeight(contentView, mBuilder.mN.mLargeIcon != null);
 
@@ -3948,8 +3930,6 @@
             }
 
             contentView.setImageViewBitmap(R.id.big_picture, mPicture);
-
-            mBuilder.addProfileBadge(contentView, R.id.profile_badge_line3);
             return contentView;
         }
 
@@ -4083,9 +4063,6 @@
             contentView.setViewVisibility(R.id.big_text,
                     TextUtils.isEmpty(bigTextText) ? View.GONE : View.VISIBLE);
             contentView.setInt(R.id.big_text, "setMaxLines", calculateMaxLines());
-
-            mBuilder.addProfileBadge(contentView, R.id.profile_badge_large_template);
-
             contentView.setBoolean(R.id.big_text, "setHasImage", mBuilder.mN.mLargeIcon != null);
 
             return contentView;
@@ -4183,7 +4160,7 @@
          * @hide
          */
         public RemoteViews makeBigContentView() {
-            // Remove the content text so line3 disappears unless you have a summary
+            // Remove the content text so it disappears unless you have a summary
             // Nasty
             CharSequence oldBuilderContentText = mBuilder.mN.extras.getCharSequence(EXTRA_TEXT);
             mBuilder.getAllExtras().putCharSequence(EXTRA_TEXT, null);
@@ -4222,7 +4199,6 @@
                 }
                 i++;
             }
-            mBuilder.addProfileBadge(contentView, R.id.profile_badge_large_template);
 
             handleInboxImageMargin(contentView, rowIds[0]);
 
@@ -4433,7 +4409,7 @@
         private void handleImage(RemoteViews contentView) {
             if (mBuilder.mN.mLargeIcon != null) {
                 contentView.setViewLayoutMarginEnd(R.id.line1, 0);
-                contentView.setViewLayoutMarginEnd(R.id.line3, 0);
+                contentView.setViewLayoutMarginEnd(R.id.text, 0);
             }
         }
 
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 0161119..a655bfd 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -3057,6 +3057,7 @@
      *
      * @hide
      */
+    @SystemApi
     public String getDeviceOwnerNameOnAnyUser() {
         if (mService != null) {
             try {
@@ -5141,4 +5142,55 @@
             return null;
         }
     }
+
+    /**
+     * Called by a profile owner of a managed profile to set the color used for customization.
+     * This color is used as background color of the confirm credentials screen for that user.
+     * The default color is {@link android.graphics.Color#GRAY}.
+     *
+     * <p>The confirm credentials screen can be created using
+     * {@link android.app.KeyguardManager#createConfirmDeviceCredentialIntent}.
+     *
+     * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+     * @param color The 32bit representation of the color to be used.
+     */
+    public void setOrganizationColor(@NonNull ComponentName admin, int color) {
+        try {
+            mService.setOrganizationColor(admin, color);
+        } catch (RemoteException re) {
+            Log.w(TAG, REMOTE_EXCEPTION_MESSAGE, re);
+        }
+    }
+
+    /**
+     * Called by a profile owner of a managed profile to retrieve the color used for customization.
+     * This color is used as background color of the confirm credentials screen for that user.
+     *
+     * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+     * @return The 32bit representation of the color to be used.
+     */
+    public int getOrganizationColor(@NonNull ComponentName admin) {
+        try {
+            return mService.getOrganizationColor(admin);
+        } catch (RemoteException re) {
+            Log.w(TAG, REMOTE_EXCEPTION_MESSAGE, re);
+            return 0;
+        }
+    }
+
+    /**
+     * @hide
+     * Retrieve the customization color for a given user.
+     *
+     * @param userHandle The user id of the user we're interested in.
+     * @return The 32bit representation of the color to be used.
+     */
+    public int getOrganizationColorForUser(int userHandle) {
+        try {
+            return mService.getOrganizationColorForUser(userHandle);
+        } catch (RemoteException re) {
+            Log.w(TAG, REMOTE_EXCEPTION_MESSAGE, re);
+            return 0;
+        }
+    }
 }
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index 08cab88..82115a2 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -263,4 +263,8 @@
     String getLongSupportMessageForUser(in ComponentName admin, int userHandle);
 
     boolean isSeparateProfileChallengeAllowed(int userHandle);
+
+    void setOrganizationColor(in ComponentName admin, in int color);
+    int getOrganizationColor(in ComponentName admin);
+    int getOrganizationColorForUser(int userHandle);
 }
diff --git a/core/java/android/app/backup/BackupManager.java b/core/java/android/app/backup/BackupManager.java
index 193a0b2c..c27eaa4f 100644
--- a/core/java/android/app/backup/BackupManager.java
+++ b/core/java/android/app/backup/BackupManager.java
@@ -426,6 +426,29 @@
     }
 
     /**
+     * Ask the framework whether this app is eligible for backup.
+     *
+     * <p>Callers must hold the android.permission.BACKUP permission to use this method.
+     *
+     * @param packageName The name of the package.
+     * @return Whether this app is eligible for backup.
+     *
+     * @hide
+     */
+    @SystemApi
+    public boolean isAppEligibleForBackup(String packageName) {
+        checkServiceBinder();
+        if (sService != null) {
+            try {
+                return sService.isAppEligibleForBackup(packageName);
+            } catch (RemoteException e) {
+                Log.e(TAG, "isAppEligibleForBackup(pkg) couldn't connect");
+            }
+        }
+        return false;
+    }
+    
+    /**
      * Request an immediate backup, providing an observer to which results of the backup operation
      * will be published. The Android backup system will decide for each package whether it will
      * be full app data backup or key/value-pair-based backup.
diff --git a/core/java/android/app/backup/BackupTransport.java b/core/java/android/app/backup/BackupTransport.java
index 4363604..aca115f 100644
--- a/core/java/android/app/backup/BackupTransport.java
+++ b/core/java/android/app/backup/BackupTransport.java
@@ -482,6 +482,18 @@
                 "Transport cancelFullBackup() not implemented");
     }
 
+    /**
+     * Ask the transport whether this app is eligible for backup.
+     *
+     * @param targetPackage The identity of the application.
+     * @param isFullBackup If set, transport should check if app is eligible for full data backup,
+     *   otherwise to check if eligible for key-value backup.
+     * @return Whether this app is eligible for backup.
+     */
+    public boolean isAppEligibleForBackup(PackageInfo targetPackage, boolean isFullBackup) {
+        return true;
+    }
+
     // ------------------------------------------------------------------------------------
     // Full restore interfaces
 
@@ -659,6 +671,12 @@
         }
 
         @Override
+        public boolean isAppEligibleForBackup(PackageInfo targetPackage, boolean isFullBackup)
+                throws RemoteException {
+            return BackupTransport.this.isAppEligibleForBackup(targetPackage, isFullBackup);
+        }
+
+        @Override
         public int getNextFullRestoreDataChunk(ParcelFileDescriptor socket) {
             return BackupTransport.this.getNextFullRestoreDataChunk(socket);
         }
diff --git a/core/java/android/app/backup/IBackupManager.aidl b/core/java/android/app/backup/IBackupManager.aidl
index 2a1c00f..5d4cc6f 100644
--- a/core/java/android/app/backup/IBackupManager.aidl
+++ b/core/java/android/app/backup/IBackupManager.aidl
@@ -329,6 +329,16 @@
     long getAvailableRestoreToken(String packageName);
 
     /**
+     * Ask the framework whether this app is eligible for backup.
+     *
+     * <p>Callers must hold the android.permission.BACKUP permission to use this method.
+     *
+     * @param packageName The name of the package.
+     * @return Whether this app is eligible for backup.
+     */
+    boolean isAppEligibleForBackup(String packageName);
+
+    /**
      * Request an immediate backup, providing an observer to which results of the backup operation
      * will be published. The Android backup system will decide for each package whether it will
      * be full app data backup or key/value-pair-based backup.
diff --git a/core/java/android/bluetooth/BluetoothHeadsetClientCall.java b/core/java/android/bluetooth/BluetoothHeadsetClientCall.java
index 002f63f..c73bc3c 100644
--- a/core/java/android/bluetooth/BluetoothHeadsetClientCall.java
+++ b/core/java/android/bluetooth/BluetoothHeadsetClientCall.java
@@ -196,7 +196,7 @@
 
     public String toString(boolean loggable) {
         StringBuilder builder = new StringBuilder("BluetoothHeadsetClientCall{mDevice: ");
-        builder.append(loggable ? mDevice.hashCode() : mDevice);
+        builder.append(loggable ? mDevice : mDevice.hashCode());
         builder.append(", mId: ");
         builder.append(mId);
         builder.append(", mUUID: ");
@@ -214,7 +214,7 @@
             default: builder.append(mState); break;
         }
         builder.append(", mNumber: ");
-        builder.append(loggable ? mNumber.hashCode() : mNumber);
+        builder.append(loggable ? mNumber : mNumber.hashCode());
         builder.append(", mMultiParty: ");
         builder.append(mMultiParty);
         builder.append(", mOutgoing: ");
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index ba4d14c..89f6870 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -4268,6 +4268,22 @@
 
     /**
      * If the target user is a managed profile of the calling user or the caller
+     * is itself a managed profile, then this returns a drawable to use as a small
+     * icon to include in a view to distinguish it from the original icon. This version
+     * doesn't have background protection and should be used over a light background instead of
+     * a badge.
+     *
+     * @param user The target user.
+     * @param density The optional desired density for the badge as per
+     *         {@link android.util.DisplayMetrics#densityDpi}. If not provided
+     *         the density of the current display is used.
+     * @return the drawable or null if no drawable is required.
+     * @hide
+     */
+    public abstract Drawable getUserBadgeForDensityNoBackground(UserHandle user, int density);
+
+    /**
+     * If the target user is a managed profile of the calling user or the caller
      * is itself a managed profile, then this returns a copy of the label with
      * badging for accessibility services like talkback. E.g. passing in "Email"
      * and it might return "Work Email" for Email in the work profile.
diff --git a/core/java/android/provider/CallLog.java b/core/java/android/provider/CallLog.java
index 6a5d857c..e7c4a07 100644
--- a/core/java/android/provider/CallLog.java
+++ b/core/java/android/provider/CallLog.java
@@ -38,6 +38,7 @@
 import android.telecom.TelecomManager;
 import android.telephony.PhoneNumberUtils;
 import android.text.TextUtils;
+import android.util.Log;
 
 import com.android.internal.telephony.CallerInfo;
 import com.android.internal.telephony.PhoneConstants;
@@ -49,6 +50,7 @@
  */
 public class CallLog {
     private static final String LOG_TAG = "CallLog";
+    private static final boolean VERBOSE_LOG = false; // DON'T SUBMIT WITH TRUE.
 
     public static final String AUTHORITY = "call_log";
 
@@ -58,6 +60,17 @@
     public static final Uri CONTENT_URI =
         Uri.parse("content://" + AUTHORITY);
 
+
+    /**
+     * The "shadow" provider stores calllog when the real calllog provider is encrypted.  The
+     * real provider will alter copy from it when it starts, and remove the entries in the shadow.
+     *
+     * <p>See the comment in {@link Calls#addCall} for the details.
+     *
+     * @hide
+     */
+    public static final String SHADOW_AUTHORITY = "call_log_shadow";
+
     /**
      * Contains the recent calls.
      */
@@ -68,6 +81,10 @@
         public static final Uri CONTENT_URI =
                 Uri.parse("content://call_log/calls");
 
+        /** @hide */
+        public static final Uri SHADOW_CONTENT_URI =
+                Uri.parse("content://call_log_shadow/calls");
+
         /**
          * The content:// style URL for filtering this table on phone numbers
          */
@@ -458,8 +475,10 @@
         public static Uri addCall(CallerInfo ci, Context context, String number,
                 int presentation, int callType, int features, PhoneAccountHandle accountHandle,
                 long start, int duration, Long dataUsage) {
-            return addCall(ci, context, number, "", presentation, callType, features, accountHandle,
-                    start, duration, dataUsage, false, null, false);
+            return addCall(ci, context, number, /* postDialDigits =*/ "", presentation,
+                    callType, features, accountHandle,
+                    start, duration, dataUsage, /* addForAllUsers =*/ false,
+                    /* userToBeInsertedTo =*/ null, /* is_read =*/ false);
         }
 
 
@@ -495,7 +514,7 @@
                 boolean addForAllUsers, UserHandle userToBeInsertedTo) {
             return addCall(ci, context, number, postDialDigits, presentation, callType, features,
                     accountHandle, start, duration, dataUsage, addForAllUsers, userToBeInsertedTo,
-                    false);
+                    /* is_read =*/ false);
         }
 
         /**
@@ -526,13 +545,18 @@
          *                Used for call log restore of missed calls.
          *
          * @result The URI of the call log entry belonging to the user that made or received this
-         *        call.
+         *        call.  This could be of the shadow provider.  Do not return it to non-system apps,
+         *        as they don't have permissions.
          * {@hide}
          */
         public static Uri addCall(CallerInfo ci, Context context, String number,
                 String postDialDigits, int presentation, int callType, int features,
                 PhoneAccountHandle accountHandle, long start, int duration, Long dataUsage,
                 boolean addForAllUsers, UserHandle userToBeInsertedTo, boolean is_read) {
+            if (VERBOSE_LOG) {
+                Log.v(LOG_TAG, String.format("Add call: number=%s, user=%s, for all=%s",
+                        number, userToBeInsertedTo, addForAllUsers));
+            }
             final ContentResolver resolver = context.getContentResolver();
             int numberPresentation = PRESENTATION_ALLOWED;
 
@@ -647,41 +671,104 @@
                 }
             }
 
+            /*
+                Writing the calllog works in the following way:
+                - All user entries
+                    - if user-0 is encrypted, insert to user-0's shadow only.
+                      (other users should also be encrypted, so nothing to do for other users.)
+                    - if user-0 is decrypted, insert to user-0's real provider, as well as
+                      all other users that are running and decrypted and should have calllog.
+
+                - Single user entry.
+                    - If the target user is encryted, insert to its shadow.
+                    - Otherwise insert to its real provider.
+
+                When the (real) calllog provider starts, it copies entries that it missed from
+                elsewhere.
+                - When user-0's (real) provider starts, it copies from user-0's shadow, and clears
+                  the shadow.
+
+                - When other users (real) providers start, unless it shouldn't have calllog entries,
+                     - Copy from the user's shadow, and clears the shadow.
+                     - Copy from user-0's entries that are FOR_ALL_USERS = 1.  (and don't clear it.)
+             */
+
             Uri result = null;
 
+            final UserManager userManager = context.getSystemService(UserManager.class);
+            final int currentUserId = userManager.getUserHandle();
+
             if (addForAllUsers) {
-                // Insert the entry for all currently running users, in order to trigger any
-                // ContentObservers currently set on the call log.
-                final UserManager userManager = (UserManager) context.getSystemService(
-                        Context.USER_SERVICE);
-                List<UserInfo> users = userManager.getUsers(true);
-                final int currentUserId = userManager.getUserHandle();
+                // First, insert to the system user.
+                final Uri uriForSystem = addEntryAndRemoveExpiredEntries(
+                        context, userManager, UserHandle.SYSTEM, values);
+                if (uriForSystem == null
+                        || SHADOW_AUTHORITY.equals(uriForSystem.getAuthority())) {
+                    // This means the system user is still encrypted and the entry has inserted
+                    // into the shadow.  This means other users are still all encrypted.
+                    // Nothing further to do; just return null.
+                    return null;
+                }
+                if (UserHandle.USER_SYSTEM == currentUserId) {
+                    result = uriForSystem;
+                }
+
+                // Otherwise, insert to all other users that are running and unlocked.
+
+                final List<UserInfo> users = userManager.getUsers(true);
+
                 final int count = users.size();
                 for (int i = 0; i < count; i++) {
-                    final UserInfo user = users.get(i);
-                    final UserHandle userHandle = user.getUserHandle();
+                    final UserInfo userInfo = users.get(i);
+                    final UserHandle userHandle = userInfo.getUserHandle();
+                    final int userId = userHandle.getIdentifier();
+
+                    if (userHandle.isSystem()) {
+                        // Already written.
+                        continue;
+                    }
+
+                    if (!shouldHaveSharedCallLogEntries(context, userManager, userId)) {
+                        // Shouldn't have calllog entries.
+                        continue;
+                    }
+
+                    // For other users, we write only when they're running *and* decrypted.
+                    // Other providers will copy from the system user's real provider, when they
+                    // start.
                     if (userManager.isUserRunning(userHandle)
-                            && !userManager.hasUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS,
-                                    userHandle)
-                            && !user.isManagedProfile()) {
-                        Uri uri = addEntryAndRemoveExpiredEntries(context,
-                                ContentProvider.maybeAddUserId(CONTENT_URI, user.id), values);
-                        if (user.id == currentUserId) {
+                            && userManager.isUserUnlocked(userHandle)) {
+                        final Uri uri = addEntryAndRemoveExpiredEntries(context, userManager,
+                                userHandle, values);
+                        if (userId == currentUserId) {
                             result = uri;
                         }
                     }
                 }
             } else {
-                Uri uri = CONTENT_URI;
-                if (userToBeInsertedTo != null) {
-                    uri = ContentProvider
-                            .maybeAddUserId(CONTENT_URI, userToBeInsertedTo.getIdentifier());
-                }
-                result = addEntryAndRemoveExpiredEntries(context, uri, values);
+                // Single-user entry. Just write to that user, assuming it's running.  If the
+                // user is encrypted, we write to the shadow calllog.
+
+                final UserHandle targetUserHandle = userToBeInsertedTo != null
+                        ? userToBeInsertedTo
+                        : UserHandle.of(currentUserId);
+                result = addEntryAndRemoveExpiredEntries(context, userManager, targetUserHandle,
+                        values);
             }
             return result;
         }
 
+        /** @hide */
+        public static boolean shouldHaveSharedCallLogEntries(Context context,
+                UserManager userManager, int userId) {
+            if (userManager.hasUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS,
+                    UserHandle.of(userId))) {
+                return false;
+            }
+            final UserInfo userInfo = userManager.getUserInfo(userId);
+            return userInfo != null && !userInfo.isManagedProfile();
+        }
+
         /**
          * Query the call log database for the last dialed number.
          * @param context Used to get the content resolver.
@@ -707,14 +794,31 @@
             }
         }
 
-        private static Uri addEntryAndRemoveExpiredEntries(Context context, Uri uri,
-                ContentValues values) {
+        private static Uri addEntryAndRemoveExpiredEntries(Context context, UserManager userManager,
+                UserHandle user, ContentValues values) {
             final ContentResolver resolver = context.getContentResolver();
-            Uri result = resolver.insert(uri, values);
-            resolver.delete(uri, "_id IN " +
-                    "(SELECT _id FROM calls ORDER BY " + DEFAULT_SORT_ORDER
-                    + " LIMIT -1 OFFSET 500)", null);
-            return result;
+
+            final Uri uri = ContentProvider.maybeAddUserId(
+                    userManager.isUserUnlocked(user) ? CONTENT_URI : SHADOW_CONTENT_URI,
+                    user.getIdentifier());
+
+            if (VERBOSE_LOG) {
+                Log.v(LOG_TAG, String.format("Inserting to %s", uri));
+            }
+
+            try {
+                final Uri result = resolver.insert(uri, values);
+                resolver.delete(uri, "_id IN " +
+                        "(SELECT _id FROM calls ORDER BY " + DEFAULT_SORT_ORDER
+                        + " LIMIT -1 OFFSET 500)", null);
+                return result;
+            } catch (IllegalArgumentException e) {
+                Log.w(LOG_TAG, "Failed to insert calllog", e);
+                // Even though we make sure the target user is running and decrypted before calling
+                // this method, there's a chance that the user just got shut down, in which case
+                // we'll still get "IllegalArgumentException: Unknown URL content://call_log/calls".
+                return null;
+            }
         }
 
         private static void updateDataUsageStatForData(ContentResolver resolver, String dataId) {
diff --git a/core/java/android/util/LocaleList.java b/core/java/android/util/LocaleList.java
index 24883e3..8a2d015 100644
--- a/core/java/android/util/LocaleList.java
+++ b/core/java/android/util/LocaleList.java
@@ -301,10 +301,19 @@
             // is a pseudo-locale. So this is not a match.
             return 0;
         }
+        final String supportedScr = getLikelyScript(supported);
+        if (supportedScr.isEmpty()) {
+            // If we can't guess a script, we don't know enough about the locales' language to find
+            // if the locales match. So we fall back to old behavior of matching, which considered
+            // locales with different regions different.
+            final String supportedRegion = supported.getCountry();
+            return (supportedRegion.isEmpty() ||
+                    supportedRegion.equals(desired.getCountry()))
+                    ? 1 : 0;
+        }
+        final String desiredScr = getLikelyScript(desired);
         // There is no match if the two locales use different scripts. This will most imporantly
         // take care of traditional vs simplified Chinese.
-        final String supportedScr = getLikelyScript(supported);
-        final String desiredScr = getLikelyScript(desired);
         return supportedScr.equals(desiredScr) ? 1 : 0;
     }
 
diff --git a/core/java/android/view/NotificationHeaderView.java b/core/java/android/view/NotificationHeaderView.java
index 54fa764..1c0ea0f 100644
--- a/core/java/android/view/NotificationHeaderView.java
+++ b/core/java/android/view/NotificationHeaderView.java
@@ -25,6 +25,8 @@
 import android.widget.RemoteViews;
 import android.widget.TextView;
 
+import com.android.internal.R;
+
 import java.util.ArrayList;
 
 /**
@@ -44,6 +46,7 @@
     private ImageView mExpandButton;
     private View mIcon;
     private TextView mChildCount;
+    private View mProfileBadge;
     private int mIconColor;
     private int mOriginalNotificationColor;
     private boolean mGroupHeader;
@@ -76,6 +79,7 @@
         mExpandButton = (ImageView) findViewById(com.android.internal.R.id.expand_button);
         mIcon = findViewById(com.android.internal.R.id.icon);
         mChildCount = (TextView) findViewById(com.android.internal.R.id.number_of_children);
+        mProfileBadge = findViewById(com.android.internal.R.id.profile_badge);
     }
 
     @Override
@@ -120,12 +124,29 @@
             }
             totalWidth = givenWidth;
         }
+        if (mProfileBadge.getVisibility() != View.GONE) {
+            totalWidth = givenWidth;
+        }
         setMeasuredDimension(totalWidth, givenHeight);
     }
 
     @Override
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
         super.onLayout(changed, l, t, r, b);
+        if (mProfileBadge.getVisibility() != View.GONE) {
+            int paddingEnd = getPaddingEnd();
+            if (getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
+                mProfileBadge.layout(paddingEnd,
+                        mProfileBadge.getTop(),
+                        paddingEnd + mProfileBadge.getMeasuredWidth(),
+                        mProfileBadge.getBottom());
+            } else {
+                mProfileBadge.layout(getWidth() - paddingEnd - mProfileBadge.getMeasuredWidth(),
+                        mProfileBadge.getTop(),
+                        getWidth() - paddingEnd,
+                        mProfileBadge.getBottom());
+            }
+        }
         updateTouchListener();
     }
 
@@ -305,4 +326,20 @@
         }
         return this;
     }
+
+    public ImageView getExpandButton() {
+        return mExpandButton;
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+
+    public boolean isInTouchRect(float x, float y) {
+        if (mExpandClickListener == null) {
+            return false;
+        }
+        return mTouchListener.isInside(x, y);
+    }
 }
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index 868ddda..7b53697 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -1401,6 +1401,10 @@
                 root.setDragFocus(target);
 
                 final int action = event.mAction;
+                // Position should not be available for ACTION_DRAG_ENTERED and ACTION_DRAG_EXITED.
+                event.mX = 0;
+                event.mY = 0;
+
                 // If we've dragged off of a child view or this window, send it the EXITED message
                 if (mCurrentDragView != null) {
                     final View view = mCurrentDragView;
@@ -1429,6 +1433,8 @@
                     }
                 }
                 event.mAction = action;  // restore the event's original state
+                event.mX = tx;
+                event.mY = ty;
             }
 
             // Dispatch the actual drag location notice, localized into its coordinates
diff --git a/core/java/android/view/inputmethod/EditorInfo.java b/core/java/android/view/inputmethod/EditorInfo.java
index 3ff9522..85893b0 100644
--- a/core/java/android/view/inputmethod/EditorInfo.java
+++ b/core/java/android/view/inputmethod/EditorInfo.java
@@ -16,6 +16,7 @@
 
 package android.view.inputmethod;
 
+import android.annotation.Nullable;
 import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -341,20 +342,26 @@
     public Bundle extras;
 
     /**
-     * Additional context information that tells what languages are expected by the user.
+     * List of the languages that the user is supposed to switch to no matter what input method
+     * subtype is currently used.  This special "hint" can be used mainly for, but not limited to,
+     * multilingual users who want IMEs to switch language context automatically.
      *
-     * <p><strong>IME authors:</strong> Possible use cases for IME developers would be:</p>
-     * <ul>
-     *     <li>Automatically switching keyboard layout.</li>
-     *     <li>Changing language model for better typing experience.</li>
-     * </ul>
+     * <p>{@code null} means that no special language "hint" is needed.</p>
      *
-     * <p><strong>Editor authors:</strong> Providing this context information can help IMEs to
-     * improve text input experience.  For example, chat applications can remember what language is
-     * used in the last conversation for each chat session, and put the last used language at the
-     * top of {@link #locales}.</p>
+     * <p><strong>Editor authors:</strong> Specify this only when you are confident that the user
+     * will switch to certain languages in this context no matter what input method subtype is
+     * currently selected.  Otherwise, keep this {@code null}.  Explicit user actions and/or
+     * preferences would be good signals to specify this special "hint",  For example, a chat
+     * application may be able to put the last used language at the top of {@link #hintLocales}
+     * based on whom the user is going to talk, by remembering what language is used in the last
+     * conversation.  Do not specify {@link android.widget.TextView#getTextLocales()} only because
+     * it is used for text rendering.</p>
+     *
+     * @see android.widget.TextView#setImeHintLocales(LocaleList)
+     * @see android.widget.TextView#getImeHintLocales()
      */
-    public LocaleList locales = LocaleList.getEmptyLocaleList();
+    @Nullable
+    public LocaleList hintLocales = null;
 
     /**
      * Ensure that the data in this EditorInfo is compatible with an application
@@ -410,7 +417,7 @@
                 + " fieldId=" + fieldId
                 + " fieldName=" + fieldName);
         pw.println(prefix + "extras=" + extras);
-        pw.println(prefix + "locales=" + locales);
+        pw.println(prefix + "hintLocales=" + hintLocales);
     }
 
     /**
@@ -434,7 +441,11 @@
         dest.writeInt(fieldId);
         dest.writeString(fieldName);
         dest.writeBundle(extras);
-        locales.writeToParcel(dest, flags);
+        if (hintLocales != null) {
+            hintLocales.writeToParcel(dest, flags);
+        } else {
+            LocaleList.getEmptyLocaleList().writeToParcel(dest, flags);
+        }
     }
 
     /**
@@ -458,7 +469,8 @@
                     res.fieldId = source.readInt();
                     res.fieldName = source.readString();
                     res.extras = source.readBundle();
-                    res.locales = LocaleList.CREATOR.createFromParcel(source);
+                    LocaleList hintLocales = LocaleList.CREATOR.createFromParcel(source);
+                    res.hintLocales = hintLocales.isEmpty() ? null : hintLocales;
                     return res;
                 }
 
diff --git a/core/java/android/view/inputmethod/InputMethodInfo.java b/core/java/android/view/inputmethod/InputMethodInfo.java
index 4fc6665..43306d0 100644
--- a/core/java/android/view/inputmethod/InputMethodInfo.java
+++ b/core/java/android/view/inputmethod/InputMethodInfo.java
@@ -191,6 +191,8 @@
                                     .InputMethod_Subtype_label, 0))
                             .setSubtypeIconResId(a.getResourceId(com.android.internal.R.styleable
                                     .InputMethod_Subtype_icon, 0))
+                            .setLanguageTag(a.getString(com.android.internal.R.styleable
+                                    .InputMethod_Subtype_languageTag))
                             .setSubtypeLocale(a.getString(com.android.internal.R.styleable
                                     .InputMethod_Subtype_imeSubtypeLocale))
                             .setSubtypeMode(a.getString(com.android.internal.R.styleable
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index df5af25..4355eb3 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -69,6 +69,7 @@
 import android.text.style.TextAppearanceSpan;
 import android.text.style.URLSpan;
 import android.util.DisplayMetrics;
+import android.util.LocaleList;
 import android.util.Log;
 import android.util.SparseArray;
 import android.view.ActionMode;
@@ -5299,6 +5300,7 @@
         Bundle extras;
         OnEditorActionListener onEditorActionListener;
         boolean enterDown;
+        LocaleList imeHintLocales;
     }
 
     static class InputMethodState {
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 81ebdb3..c626af6 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -645,6 +645,16 @@
      */
     private Editor mEditor;
 
+    private static final int DEVICE_PROVISIONED_UNKNOWN = 0;
+    private static final int DEVICE_PROVISIONED_NO = 1;
+    private static final int DEVICE_PROVISIONED_YES = 2;
+
+    /**
+     * Some special options such as sharing selected text should only be shown if the device
+     * is provisioned. Only check the provisioned state once for a given view instance.
+     */
+    private int mDeviceProvisionedState = DEVICE_PROVISIONED_UNKNOWN;
+
     /*
      * Kick-start the font cache for the zygote process (to pay the cost of
      * initializing freetype for our default font only once).
@@ -4992,6 +5002,35 @@
     }
 
     /**
+     * Change "hint" locales associated with the text view, which will be reported to an IME with
+     * {@link EditorInfo#hintLocales} when it has focus.
+     *
+     * <p><strong>Note:</strong> If you want new "hint" to take effect immediately you need to
+     * call {@link InputMethodManager#restartInput(View)}.</p>
+     * @param hintLocales List of the languages that the user is supposed to switch to no matter
+     * what input method subtype is currently used. Set {@code null} to clear the current "hint".
+     * @see #getImeHIntLocales()
+     * @see android.view.inputmethod.EditorInfo#hintLocales
+     */
+    public void setImeHintLocales(@Nullable LocaleList hintLocales) {
+        createEditorIfNeeded();
+        mEditor.createInputContentTypeIfNeeded();
+        mEditor.mInputContentType.imeHintLocales = hintLocales;
+    }
+
+    /**
+     * @return The current languages list "hint". {@code null} when no "hint" is available.
+     * @see #setImeHintLocales(LocaleList)
+     * @see android.view.inputmethod.EditorInfo#hintLocales
+     */
+    @Nullable
+    public LocaleList getImeHintLocales() {
+        if (mEditor == null) { return null; }
+        if (mEditor.mInputContentType == null) { return null; }
+        return mEditor.mInputContentType.imeHintLocales;
+    }
+
+    /**
      * Returns the error message that was set to be displayed with
      * {@link #setError}, or <code>null</code> if no error was set
      * or if it the error was cleared by the widget after user input.
@@ -6411,8 +6450,10 @@
                 outAttrs.actionLabel = mEditor.mInputContentType.imeActionLabel;
                 outAttrs.actionId = mEditor.mInputContentType.imeActionId;
                 outAttrs.extras = mEditor.mInputContentType.extras;
+                outAttrs.hintLocales = mEditor.mInputContentType.imeHintLocales;
             } else {
                 outAttrs.imeOptions = EditorInfo.IME_NULL;
+                outAttrs.hintLocales = null;
             }
             if (focusSearch(FOCUS_DOWN) != null) {
                 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_NEXT;
@@ -6440,9 +6481,6 @@
                 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
             }
             outAttrs.hintText = mHint;
-            // LocaleList is designed to be immutable.  This is theoretically equivalent to copy
-            // the snapshot of the current text locales.
-            outAttrs.locales = getTextLocales();
             if (mText instanceof Editable) {
                 InputConnection ic = new EditableInputConnection(this);
                 outAttrs.initialSelStart = getSelectionStart();
@@ -9613,7 +9651,17 @@
     }
 
     boolean canShare() {
-        return canCopy();
+        return canCopy() && isDeviceProvisioned();
+    }
+
+    boolean isDeviceProvisioned() {
+        if (mDeviceProvisionedState == DEVICE_PROVISIONED_UNKNOWN) {
+            mDeviceProvisionedState = Settings.Global.getInt(
+                    mContext.getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 0) != 0
+                    ? DEVICE_PROVISIONED_YES
+                    : DEVICE_PROVISIONED_NO;
+        }
+        return mDeviceProvisionedState == DEVICE_PROVISIONED_YES;
     }
 
     boolean canPaste() {
diff --git a/core/java/com/android/internal/backup/IBackupTransport.aidl b/core/java/com/android/internal/backup/IBackupTransport.aidl
index b1fc20d..444dbfa 100644
--- a/core/java/com/android/internal/backup/IBackupTransport.aidl
+++ b/core/java/com/android/internal/backup/IBackupTransport.aidl
@@ -239,6 +239,16 @@
     int sendBackupData(int numBytes);
     void cancelFullBackup();
 
+    /**
+     * Ask the transport whether this app is eligible for backup.
+     *
+     * @param targetPackage The identity of the application.
+     * @param isFullBackup If set, transport should check if app is eligible for full data backup,
+     *   otherwise to check if eligible for key-value backup.
+     * @return Whether this app is eligible for backup.
+     */
+    boolean isAppEligibleForBackup(in PackageInfo targetPackage, boolean isFullBackup);
+
     // full restore stuff
 
     /**
@@ -286,5 +296,4 @@
      *    operation will immediately be finished with no further attempts to restore app data.
      */
     int abortFullRestore();
-
 }
diff --git a/core/java/com/android/internal/policy/DividerSnapAlgorithm.java b/core/java/com/android/internal/policy/DividerSnapAlgorithm.java
index 59a1e4a..5b40bc0 100644
--- a/core/java/com/android/internal/policy/DividerSnapAlgorithm.java
+++ b/core/java/com/android/internal/policy/DividerSnapAlgorithm.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Rect;
+import android.util.Log;
 
 import java.util.ArrayList;
 
@@ -30,6 +31,9 @@
  */
 public class DividerSnapAlgorithm {
 
+    private static final int MIN_FLING_VELOCITY_DP_PER_SECOND = 400;
+    private static final int MIN_DISMISS_VELOCITY_DP_PER_SECOND = 600;
+
     /**
      * 3 snap targets: left/top has 16:9 ratio (for videos), 1:1, and right/bottom has 16:9 ratio
      */
@@ -46,6 +50,7 @@
     private static final int SNAP_ONLY_1_1 = 2;
 
     private final float mMinFlingVelocityPxPerSecond;
+    private final float mMinDismissVelocityPxPerSecond;
     private final int mDisplayWidth;
     private final int mDisplayHeight;
     private final int mDividerSize;
@@ -64,10 +69,12 @@
     private final SnapTarget mDismissEndTarget;
     private final SnapTarget mMiddleTarget;
 
-    public DividerSnapAlgorithm(Resources res, float minFlingVelocityPxPerSecond,
-            int displayWidth, int displayHeight, int dividerSize, boolean isHorizontalDivision,
-            Rect insets) {
-        mMinFlingVelocityPxPerSecond = minFlingVelocityPxPerSecond;
+    public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize,
+            boolean isHorizontalDivision, Rect insets) {
+        mMinFlingVelocityPxPerSecond =
+                MIN_FLING_VELOCITY_DP_PER_SECOND * res.getDisplayMetrics().density;
+        mMinDismissVelocityPxPerSecond =
+                MIN_DISMISS_VELOCITY_DP_PER_SECOND * res.getDisplayMetrics().density;
         mDividerSize = dividerSize;
         mDisplayWidth = displayWidth;
         mDisplayHeight = displayHeight;
@@ -85,15 +92,24 @@
     }
 
     public SnapTarget calculateSnapTarget(int position, float velocity) {
-        if (Math.abs(velocity) < mMinFlingVelocityPxPerSecond) {
-            return snap(position);
-        }
-        if (position < mFirstSplitTarget.position && velocity < 0) {
+        return calculateSnapTarget(position, velocity, true /* hardDismiss */);
+    }
+
+    /**
+     * @param position the top/left position of the divider
+     * @param velocity current dragging velocity
+     * @param hardDismiss if set, make it a bit harder to get reach the dismiss targets
+     */
+    public SnapTarget calculateSnapTarget(int position, float velocity, boolean hardDismiss) {
+        if (position < mFirstSplitTarget.position && velocity < -mMinDismissVelocityPxPerSecond) {
             return mDismissStartTarget;
         }
-        if (position > mLastSplitTarget.position && velocity > 0) {
+        if (position > mLastSplitTarget.position && velocity > mMinDismissVelocityPxPerSecond) {
             return mDismissEndTarget;
         }
+        if (Math.abs(velocity) < mMinFlingVelocityPxPerSecond) {
+            return snap(position, hardDismiss);
+        }
         if (velocity < 0) {
             return mFirstSplitTarget;
         } else {
@@ -102,7 +118,7 @@
     }
 
     public SnapTarget calculateNonDismissingSnapTarget(int position) {
-        SnapTarget target = snap(position);
+        SnapTarget target = snap(position, false /* hardDismiss */);
         if (target == mDismissStartTarget) {
             return mFirstSplitTarget;
         } else if (target == mDismissEndTarget) {
@@ -146,12 +162,16 @@
         return mDismissEndTarget;
     }
 
-    private SnapTarget snap(int position) {
+    private SnapTarget snap(int position, boolean hardDismiss) {
         int minIndex = -1;
-        int minDistance = Integer.MAX_VALUE;
+        float minDistance = Float.MAX_VALUE;
         int size = mTargets.size();
         for (int i = 0; i < size; i++) {
-            int distance = Math.abs(position - mTargets.get(i).position);
+            SnapTarget target = mTargets.get(i);
+            float distance = Math.abs(position - target.position);
+            if (hardDismiss) {
+                distance /= target.distanceMultiplier;
+            }
             if (distance < minDistance) {
                 minIndex = i;
                 minDistance = distance;
@@ -165,7 +185,7 @@
         int dividerMax = isHorizontalDivision
                 ? mDisplayHeight
                 : mDisplayWidth;
-        mTargets.add(new SnapTarget(-mDividerSize, SnapTarget.FLAG_DISMISS_START));
+        mTargets.add(new SnapTarget(-mDividerSize, SnapTarget.FLAG_DISMISS_START, 0.35f));
         switch (mSnapMode) {
             case SNAP_MODE_16_9:
                 addRatio16_9Targets(isHorizontalDivision);
@@ -177,7 +197,7 @@
                 addMiddleTarget(isHorizontalDivision);
                 break;
         }
-        mTargets.add(new SnapTarget(dividerMax, SnapTarget.FLAG_DISMISS_END));
+        mTargets.add(new SnapTarget(dividerMax, SnapTarget.FLAG_DISMISS_END, 0.35f));
     }
 
     private void addFixedDivisionTargets(boolean isHorizontalDivision) {
@@ -232,9 +252,20 @@
         public final int position;
         public final int flag;
 
+        /**
+         * Multiplier used to calculate distance to snap position. The lower this value, the harder
+         * it's to snap on this target
+         */
+        private final float distanceMultiplier;
+
         public SnapTarget(int position, int flag) {
+            this(position, flag, 1f);
+        }
+
+        public SnapTarget(int position, int flag, float distanceMultiplier) {
             this.position = position;
             this.flag = flag;
+            this.distanceMultiplier = distanceMultiplier;
         }
     }
 }
diff --git a/core/jni/android/graphics/MinikinUtils.cpp b/core/jni/android/graphics/MinikinUtils.cpp
index 0597d3f..309d35b 100644
--- a/core/jni/android/graphics/MinikinUtils.cpp
+++ b/core/jni/android/graphics/MinikinUtils.cpp
@@ -62,6 +62,15 @@
     layout->doLayout(buf, start, count, bufSize, bidiFlags, minikinStyle, minikinPaint);
 }
 
+float MinikinUtils::measureText(const Paint* paint, int bidiFlags, TypefaceImpl* typeface,
+        const uint16_t* buf, size_t start, size_t count, size_t bufSize, float *advances) {
+    FontCollection *font;
+    MinikinPaint minikinPaint;
+    FontStyle minikinStyle = prepareMinikinPaint(&minikinPaint, &font, paint, typeface);
+    return Layout::measureText(buf, start, count, bufSize, bidiFlags, minikinStyle, minikinPaint,
+            font, advances);
+}
+
 bool MinikinUtils::hasVariationSelector(TypefaceImpl* typeface, uint32_t codepoint, uint32_t vs) {
     const TypefaceImpl* resolvedFace = TypefaceImpl_resolveDefault(typeface);
     return resolvedFace->fFontCollection->hasVariationSelector(codepoint, vs);
diff --git a/core/jni/android/graphics/MinikinUtils.h b/core/jni/android/graphics/MinikinUtils.h
index 5bf1eec..9152539 100644
--- a/core/jni/android/graphics/MinikinUtils.h
+++ b/core/jni/android/graphics/MinikinUtils.h
@@ -40,6 +40,9 @@
             TypefaceImpl* typeface, const uint16_t* buf, size_t start, size_t count,
             size_t bufSize);
 
+    static float measureText(const Paint* paint, int bidiFlags, TypefaceImpl* typeface,
+            const uint16_t* buf, size_t start, size_t count, size_t bufSize, float *advances);
+
     static bool hasVariationSelector(TypefaceImpl* typeface, uint32_t codepoint, uint32_t vs);
 
     static float xOffsetForTextAlign(Paint* paint, const Layout& layout);
diff --git a/core/jni/android/graphics/Paint.cpp b/core/jni/android/graphics/Paint.cpp
index 98f8ce3..a3214eb 100644
--- a/core/jni/android/graphics/Paint.cpp
+++ b/core/jni/android/graphics/Paint.cpp
@@ -493,16 +493,16 @@
                 return 0;
             }
         }
-
-        Layout layout;
-        MinikinUtils::doLayout(&layout, paint, bidiFlags, typeface, text, start, count,
-                contextCount);
-        if (advances != NULL) {
-            std::unique_ptr<jfloat> advancesArray(new jfloat[count]);
-            layout.getAdvances(advancesArray.get());
+        std::unique_ptr<jfloat[]> advancesArray;
+        if (advances) {
+            advancesArray.reset(new jfloat[count]);
+        }
+        const float advance = MinikinUtils::measureText(paint, bidiFlags, typeface, text,
+                start, count, contextCount, advancesArray.get());
+        if (advances) {
             env->SetFloatArrayRegion(advances, advancesIndex, count, advancesArray.get());
         }
-        return layout.getAdvance();
+        return advance;
     }
 
     static jfloat getTextAdvances___CIIIII_FI(JNIEnv* env, jobject clazz, jlong paintHandle,
diff --git a/core/jni/android_view_DisplayEventReceiver.cpp b/core/jni/android_view_DisplayEventReceiver.cpp
index 24eb961..f9936ae 100644
--- a/core/jni/android_view_DisplayEventReceiver.cpp
+++ b/core/jni/android_view_DisplayEventReceiver.cpp
@@ -23,6 +23,7 @@
 #include <inttypes.h>
 
 #include <android_runtime/AndroidRuntime.h>
+#include <androidfw/DisplayEventDispatcher.h>
 #include <utils/Log.h>
 #include <utils/Looper.h>
 #include <utils/threads.h>
@@ -48,14 +49,12 @@
 } gDisplayEventReceiverClassInfo;
 
 
-class NativeDisplayEventReceiver : public LooperCallback {
+class NativeDisplayEventReceiver : public DisplayEventDispatcher {
 public:
     NativeDisplayEventReceiver(JNIEnv* env,
             jobject receiverWeak, const sp<MessageQueue>& messageQueue);
 
-    status_t initialize();
     void dispose();
-    status_t scheduleVsync();
 
 protected:
     virtual ~NativeDisplayEventReceiver();
@@ -66,15 +65,14 @@
     DisplayEventReceiver mReceiver;
     bool mWaitingForVsync;
 
-    virtual int handleEvent(int receiveFd, int events, void* data);
-    bool processPendingEvents(nsecs_t* outTimestamp, int32_t* id, uint32_t* outCount);
-    void dispatchVsync(nsecs_t timestamp, int32_t id, uint32_t count);
-    void dispatchHotplug(nsecs_t timestamp, int32_t id, bool connected);
+    virtual void dispatchVsync(nsecs_t timestamp, int32_t id, uint32_t count);
+    virtual void dispatchHotplug(nsecs_t timestamp, int32_t id, bool connected);
 };
 
 
 NativeDisplayEventReceiver::NativeDisplayEventReceiver(JNIEnv* env,
         jobject receiverWeak, const sp<MessageQueue>& messageQueue) :
+        DisplayEventDispatcher(messageQueue->getLooper()),
         mReceiverWeakGlobal(env->NewGlobalRef(receiverWeak)),
         mMessageQueue(messageQueue), mWaitingForVsync(false) {
     ALOGV("receiver %p ~ Initializing display event receiver.", this);
@@ -85,21 +83,6 @@
     env->DeleteGlobalRef(mReceiverWeakGlobal);
 }
 
-status_t NativeDisplayEventReceiver::initialize() {
-    status_t result = mReceiver.initCheck();
-    if (result) {
-        ALOGW("Failed to initialize display event receiver, status=%d", result);
-        return result;
-    }
-
-    int rc = mMessageQueue->getLooper()->addFd(mReceiver.getFd(), 0, Looper::EVENT_INPUT,
-            this, NULL);
-    if (rc < 0) {
-        return UNKNOWN_ERROR;
-    }
-    return OK;
-}
-
 void NativeDisplayEventReceiver::dispose() {
     ALOGV("receiver %p ~ Disposing display event receiver.", this);
 
@@ -108,87 +91,6 @@
     }
 }
 
-status_t NativeDisplayEventReceiver::scheduleVsync() {
-    if (!mWaitingForVsync) {
-        ALOGV("receiver %p ~ Scheduling vsync.", this);
-
-        // Drain all pending events.
-        nsecs_t vsyncTimestamp;
-        int32_t vsyncDisplayId;
-        uint32_t vsyncCount;
-        processPendingEvents(&vsyncTimestamp, &vsyncDisplayId, &vsyncCount);
-
-        status_t status = mReceiver.requestNextVsync();
-        if (status) {
-            ALOGW("Failed to request next vsync, status=%d", status);
-            return status;
-        }
-
-        mWaitingForVsync = true;
-    }
-    return OK;
-}
-
-int NativeDisplayEventReceiver::handleEvent(int receiveFd, int events, void* data) {
-    if (events & (Looper::EVENT_ERROR | Looper::EVENT_HANGUP)) {
-        ALOGE("Display event receiver pipe was closed or an error occurred.  "
-                "events=0x%x", events);
-        return 0; // remove the callback
-    }
-
-    if (!(events & Looper::EVENT_INPUT)) {
-        ALOGW("Received spurious callback for unhandled poll event.  "
-                "events=0x%x", events);
-        return 1; // keep the callback
-    }
-
-    // Drain all pending events, keep the last vsync.
-    nsecs_t vsyncTimestamp;
-    int32_t vsyncDisplayId;
-    uint32_t vsyncCount;
-    if (processPendingEvents(&vsyncTimestamp, &vsyncDisplayId, &vsyncCount)) {
-        ALOGV("receiver %p ~ Vsync pulse: timestamp=%" PRId64 ", id=%d, count=%d",
-                this, vsyncTimestamp, vsyncDisplayId, vsyncCount);
-        mWaitingForVsync = false;
-        dispatchVsync(vsyncTimestamp, vsyncDisplayId, vsyncCount);
-    }
-
-    return 1; // keep the callback
-}
-
-bool NativeDisplayEventReceiver::processPendingEvents(
-        nsecs_t* outTimestamp, int32_t* outId, uint32_t* outCount) {
-    bool gotVsync = false;
-    DisplayEventReceiver::Event buf[EVENT_BUFFER_SIZE];
-    ssize_t n;
-    while ((n = mReceiver.getEvents(buf, EVENT_BUFFER_SIZE)) > 0) {
-        ALOGV("receiver %p ~ Read %d events.", this, int(n));
-        for (ssize_t i = 0; i < n; i++) {
-            const DisplayEventReceiver::Event& ev = buf[i];
-            switch (ev.header.type) {
-            case DisplayEventReceiver::DISPLAY_EVENT_VSYNC:
-                // Later vsync events will just overwrite the info from earlier
-                // ones. That's fine, we only care about the most recent.
-                gotVsync = true;
-                *outTimestamp = ev.header.timestamp;
-                *outId = ev.header.id;
-                *outCount = ev.vsync.count;
-                break;
-            case DisplayEventReceiver::DISPLAY_EVENT_HOTPLUG:
-                dispatchHotplug(ev.header.timestamp, ev.header.id, ev.hotplug.connected);
-                break;
-            default:
-                ALOGW("receiver %p ~ ignoring unknown event type %#x", this, ev.header.type);
-                break;
-            }
-        }
-    }
-    if (n < 0) {
-        ALOGW("Failed to get events from display event receiver, status=%d", status_t(n));
-    }
-    return gotVsync;
-}
-
 void NativeDisplayEventReceiver::dispatchVsync(nsecs_t timestamp, int32_t id, uint32_t count) {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
 
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index a65cdb8..a82bfbe 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -356,6 +356,20 @@
     <protected-broadcast android:name="android.intent.action.WALLPAPER_CHANGED" />
 
     <protected-broadcast android:name="android.app.action.DEVICE_POLICY_MANAGER_STATE_CHANGED" />
+    <protected-broadcast android:name="android.app.action.CHOOSE_PRIVATE_KEY_ALIAS" />
+    <protected-broadcast android:name="android.app.action.DEVICE_ADMIN_DISABLED" />
+    <protected-broadcast android:name="android.app.action.DEVICE_ADMIN_DISABLE_REQUESTED" />
+    <protected-broadcast android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+    <protected-broadcast android:name="android.app.action.LOCK_TASK_ENTERING" />
+    <protected-broadcast android:name="android.app.action.LOCK_TASK_EXITING" />
+    <protected-broadcast android:name="android.app.action.NOTIFY_PENDING_SYSTEM_UPDATE" />
+    <protected-broadcast android:name="android.app.action.ACTION_PASSWORD_CHANGED" />
+    <protected-broadcast android:name="android.app.action.ACTION_PASSWORD_EXPIRING" />
+    <protected-broadcast android:name="android.app.action.ACTION_PASSWORD_FAILED" />
+    <protected-broadcast android:name="android.app.action.ACTION_PASSWORD_SUCCEEDED" />
+    <protected-broadcast android:name="com.android.server.ACTION_EXPIRED_PASSWORD_NOTIFICATION" />
+    <protected-broadcast android:name="android.intent.action.MANAGED_PROFILE_ADDED" />
+
     <protected-broadcast android:name="android.bluetooth.adapter.action.BLE_STATE_CHANGED" />
     <protected-broadcast android:name="android.content.jobscheduler.JOB_DELAY_EXPIRED" />
     <protected-broadcast android:name="android.content.syncmanager.SYNC_ALARM" />
diff --git a/core/res/res/drawable/ic_corp_badge_no_background.xml b/core/res/res/drawable/ic_corp_badge_no_background.xml
new file mode 100644
index 0000000..b1bddfc
--- /dev/null
+++ b/core/res/res/drawable/ic_corp_badge_no_background.xml
@@ -0,0 +1,30 @@
+<!--
+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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24.0dp"
+    android:height="24.0dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:pathData="M20.801,5.981L17.13,5.98l0.001,-1.471l-2.053,-2.055L8.969,2.453L6.915,4.506L6.914,5.977L3.203,5.976c-1.216,0.0 -2.189,0.983 -2.189,2.199L1.0,12.406c0.0,1.216 0.983,2.2 2.199,2.2L10.0,14.608l0.0,-1.644l0.291,0.0l3.351,0.0l0.291,0.0l0.0,1.645l6.863,0.002c1.216,0.0 2.2,-0.983 2.2,-2.199L23.0,8.181C23.0,6.965 22.017,5.981 20.801,5.981zM15.076,5.979L8.968,5.978l0.001,-1.471l6.108,0.001L15.076,5.979z"
+        android:fillColor="#FF5722"/>
+    <path
+        android:pathData="M13.911,16.646L9.978,16.646L9.978,15.48L1.673,15.48l0.0,4.105c0.0,1.216 0.959,2.2 2.175,2.2l16.13,0.004c1.216,0.0 2.203,-0.983 2.203,-2.199l0.0,-4.11l-8.27,0.0L13.910999,16.646z"
+        android:fillColor="#FF5722"/>
+    <path
+        android:pathData="M23.657,6.55 h4.72 v1.137 h-4.72z"
+        android:fillColor="#00000000"/>
+</vector>
\ No newline at end of file
diff --git a/core/res/res/layout/notification_material_action_list.xml b/core/res/res/layout/notification_material_action_list.xml
index 2a89faa..2a4aa967 100644
--- a/core/res/res/layout/notification_material_action_list.xml
+++ b/core/res/res/layout/notification_material_action_list.xml
@@ -26,6 +26,7 @@
             android:layout_height="56dp"
             android:paddingEnd="4dp"
             android:orientation="horizontal"
+            android:gravity="center_vertical"
             android:visibility="gone"
             android:background="#ffeeeeee"
             >
diff --git a/core/res/res/layout/notification_template_header.xml b/core/res/res/layout/notification_template_header.xml
index e45f52b..163db30 100644
--- a/core/res/res/layout/notification_template_header.xml
+++ b/core/res/res/layout/notification_template_header.xml
@@ -127,5 +127,15 @@
         android:paddingTop="1dp"
         android:visibility="gone"
         />
+    <ImageView android:id="@+id/profile_badge"
+        android:layout_width="@dimen/notification_badge_size"
+        android:layout_height="@dimen/notification_badge_size"
+        android:layout_gravity="center"
+        android:layout_marginStart="4dp"
+        android:paddingTop="1dp"
+        android:scaleType="fitCenter"
+        android:visibility="gone"
+        android:contentDescription="@string/notification_work_profile_content_description"
+        />
 </NotificationHeaderView>
 
diff --git a/core/res/res/layout/notification_template_material_base.xml b/core/res/res/layout/notification_template_material_base.xml
index fdbbbd6..a37bfa9 100644
--- a/core/res/res/layout/notification_template_material_base.xml
+++ b/core/res/res/layout/notification_template_material_base.xml
@@ -34,7 +34,7 @@
         android:orientation="vertical"
         >
         <include layout="@layout/notification_template_part_line1" />
-        <include layout="@layout/notification_template_part_line3" />
+        <include layout="@layout/notification_template_text" />
     </LinearLayout>
     <FrameLayout
         android:layout_width="match_parent"
diff --git a/core/res/res/layout/notification_template_material_big_base.xml b/core/res/res/layout/notification_template_material_big_base.xml
index dfd72c0..f302087 100644
--- a/core/res/res/layout/notification_template_material_big_base.xml
+++ b/core/res/res/layout/notification_template_material_big_base.xml
@@ -42,7 +42,7 @@
             android:orientation="vertical"
             >
             <include layout="@layout/notification_template_part_line1" />
-            <include layout="@layout/notification_template_part_line3" />
+            <include layout="@layout/notification_template_text" />
         </LinearLayout>
         <FrameLayout
             android:layout_width="match_parent"
diff --git a/core/res/res/layout/notification_template_material_big_media.xml b/core/res/res/layout/notification_template_material_big_media.xml
index c3db7c5..e8ff186 100644
--- a/core/res/res/layout/notification_template_material_big_media.xml
+++ b/core/res/res/layout/notification_template_material_big_media.xml
@@ -40,7 +40,7 @@
         android:orientation="vertical"
         >
         <include layout="@layout/notification_template_part_line1" />
-        <include layout="@layout/notification_template_part_line3" />
+        <include layout="@layout/notification_template_text" />
     </LinearLayout>
     <LinearLayout
         android:id="@+id/media_actions"
diff --git a/core/res/res/layout/notification_template_material_big_picture.xml b/core/res/res/layout/notification_template_material_big_picture.xml
index 5c07b9b..50aec6b 100644
--- a/core/res/res/layout/notification_template_material_big_picture.xml
+++ b/core/res/res/layout/notification_template_material_big_picture.xml
@@ -40,7 +40,7 @@
             android:orientation="vertical">
             <include layout="@layout/notification_template_part_line1"/>
             <include layout="@layout/notification_template_progress"/>
-            <include layout="@layout/notification_template_part_line3"/>
+            <include layout="@layout/notification_template_text"/>
         </LinearLayout>
         <ImageView
                 android:id="@+id/big_picture"
diff --git a/core/res/res/layout/notification_template_material_big_text.xml b/core/res/res/layout/notification_template_material_big_text.xml
index ebaffc3..9a4b28c 100644
--- a/core/res/res/layout/notification_template_material_big_text.xml
+++ b/core/res/res/layout/notification_template_material_big_text.xml
@@ -36,33 +36,17 @@
         >
         <include layout="@layout/notification_template_part_line1" />
         <include layout="@layout/notification_template_progress" />
-        <LinearLayout
+        <com.android.internal.widget.ImageFloatingTextView android:id="@+id/big_text"
             android:layout_width="match_parent"
             android:layout_height="0dp"
-            android:paddingBottom="@dimen/notification_content_margin_bottom"
-            android:orientation="horizontal"
-            android:gravity="top"
-            android:layout_weight="1"
             android:layout_marginTop="1.5dp"
-            >
-            <com.android.internal.widget.ImageFloatingTextView android:id="@+id/big_text"
-                android:textAppearance="@style/TextAppearance.Material.Notification"
-                android:layout_width="0dp"
-                android:layout_height="wrap_content"
-                android:layout_weight="1"
-                android:singleLine="false"
-                android:visibility="gone"
-                />
-            <ImageView android:id="@+id/profile_badge_large_template"
-                android:layout_width="@dimen/notification_badge_size"
-                android:layout_height="@dimen/notification_badge_size"
-                android:layout_weight="0"
-                android:layout_marginStart="4dp"
-                android:scaleType="fitCenter"
-                android:visibility="gone"
-                android:contentDescription="@string/notification_work_profile_content_description"
-                />
-        </LinearLayout>
+            android:paddingBottom="@dimen/notification_content_margin_bottom"
+            android:textAppearance="@style/TextAppearance.Material.Notification"
+            android:singleLine="false"
+            android:layout_weight="1"
+            android:gravity="top"
+            android:visibility="gone"
+            />
         <ViewStub android:layout="@layout/notification_material_reply_text"
                 android:id="@+id/notification_material_reply_container"
                 android:layout_width="match_parent"
diff --git a/core/res/res/layout/notification_template_material_inbox.xml b/core/res/res/layout/notification_template_material_inbox.xml
index 14bf899..86902db 100644
--- a/core/res/res/layout/notification_template_material_inbox.xml
+++ b/core/res/res/layout/notification_template_material_inbox.xml
@@ -41,34 +41,15 @@
             android:layout_width="match_parent"
             android:layout_height="15dp"
             android:layout_marginTop="4dp"/>
-
-        <!-- We can't have another vertical linear layout here with weight != 0 so this forces us to
-             put the badge on the first line. -->
-        <LinearLayout
+        <TextView android:id="@+id/inbox_text0"
+            android:textAppearance="@style/TextAppearance.Material.Notification"
             android:layout_width="match_parent"
-            android:layout_weight="1"
             android:layout_height="0dp"
-            android:orientation="horizontal"
-            >
-            <TextView android:id="@+id/inbox_text0"
-                android:textAppearance="@style/TextAppearance.Material.Notification"
-                android:layout_width="0dp"
-                android:layout_height="wrap_content"
-                android:singleLine="true"
-                android:ellipsize="end"
-                android:visibility="gone"
-                android:layout_weight="1"
-                />
-            <ImageView android:id="@+id/profile_badge_large_template"
-                android:layout_width="@dimen/notification_badge_size"
-                android:layout_height="@dimen/notification_badge_size"
-                android:layout_weight="0"
-                android:layout_marginStart="4dp"
-                android:scaleType="fitCenter"
-                android:visibility="gone"
-                android:contentDescription="@string/notification_work_profile_content_description"
-                />
-        </LinearLayout>
+            android:singleLine="true"
+            android:ellipsize="end"
+            android:visibility="gone"
+            android:layout_weight="1"
+            />
         <TextView android:id="@+id/inbox_text1"
             android:textAppearance="@style/TextAppearance.Material.Notification"
             android:layout_width="match_parent"
diff --git a/core/res/res/layout/notification_template_material_media.xml b/core/res/res/layout/notification_template_material_media.xml
index f0ced5f..9fcd356 100644
--- a/core/res/res/layout/notification_template_material_media.xml
+++ b/core/res/res/layout/notification_template_material_media.xml
@@ -48,7 +48,7 @@
             >
             <include layout="@layout/notification_template_part_line1"/>
             <include layout="@layout/notification_template_progress"/>
-            <include layout="@layout/notification_template_part_line3"/>
+            <include layout="@layout/notification_template_text"/>
         </LinearLayout>
         <LinearLayout
             android:id="@+id/media_actions"
diff --git a/core/res/res/layout/notification_template_part_line3.xml b/core/res/res/layout/notification_template_part_line3.xml
deleted file mode 100644
index dc47a48..0000000
--- a/core/res/res/layout/notification_template_part_line3.xml
+++ /dev/null
@@ -1,46 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2014 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License
-  -->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/line3"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:orientation="horizontal"
-    android:gravity="top"
-    >
-    <TextView android:id="@+id/text"
-        android:textAppearance="@style/TextAppearance.Material.Notification"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:layout_weight="1"
-        android:layout_gravity="top"
-        android:singleLine="true"
-        android:ellipsize="marquee"
-        android:fadingEdge="horizontal"
-        android:layout_marginTop="1.5dp"
-        />
-    <ImageView android:id="@+id/profile_badge_line3"
-        android:layout_width="@dimen/notification_badge_size"
-        android:layout_height="@dimen/notification_badge_size"
-        android:layout_gravity="center"
-        android:layout_weight="0"
-        android:layout_marginStart="4dp"
-        android:scaleType="fitCenter"
-        android:visibility="gone"
-        android:contentDescription="@string/notification_work_profile_content_description"
-        />
-</LinearLayout>
diff --git a/core/res/res/layout/notification_template_text.xml b/core/res/res/layout/notification_template_text.xml
new file mode 100644
index 0000000..38470cd
--- /dev/null
+++ b/core/res/res/layout/notification_template_text.xml
@@ -0,0 +1,28 @@
+<?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/text"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_gravity="top"
+    android:layout_marginTop="1.5dp"
+    android:ellipsize="marquee"
+    android:fadingEdge="horizontal"
+    android:gravity="top"
+    android:singleLine="true"
+    android:textAppearance="@style/TextAppearance.Material.Notification"
+    />
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index b2482cd..f92e7f0 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -275,7 +275,7 @@
     <dimen name="notification_large_icon_circle_padding">11dp</dimen>
 
     <!-- Size of the profile badge for notifications -->
-    <dimen name="notification_badge_size">16dp</dimen>
+    <dimen name="notification_badge_size">12dp</dimen>
 
     <!-- Keyguard dimensions -->
     <!-- TEMP -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 9023dd4..a5d0020 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -86,7 +86,6 @@
   <java-symbol type="id" name="left_icon" />
   <java-symbol type="id" name="leftSpacer" />
   <java-symbol type="id" name="line1" />
-  <java-symbol type="id" name="line3" />
   <java-symbol type="id" name="list_footer" />
   <java-symbol type="id" name="list_item" />
   <java-symbol type="id" name="listContainer" />
@@ -209,8 +208,7 @@
   <java-symbol type="id" name="pin_confirm_text" />
   <java-symbol type="id" name="pin_error_message" />
   <java-symbol type="id" name="timePickerLayout" />
-  <java-symbol type="id" name="profile_badge_large_template" />
-  <java-symbol type="id" name="profile_badge_line3" />
+  <java-symbol type="id" name="profile_badge" />
   <java-symbol type="id" name="transitionPosition" />
   <java-symbol type="id" name="selection_start_handle" />
   <java-symbol type="id" name="selection_end_handle" />
@@ -1267,6 +1265,7 @@
   <java-symbol type="drawable" name="ic_corp_badge" />
   <java-symbol type="drawable" name="ic_corp_badge_off" />
   <java-symbol type="drawable" name="ic_corp_icon_badge" />
+  <java-symbol type="drawable" name="ic_corp_badge_no_background" />
   <java-symbol type="drawable" name="ic_corp_icon" />
   <java-symbol type="drawable" name="ic_corp_statusbar_icon" />
   <java-symbol type="drawable" name="emulator_circular_window_overlay" />
diff --git a/core/tests/coretests/src/android/widget/TextViewActivityTest.java b/core/tests/coretests/src/android/widget/TextViewActivityTest.java
index e54723e..edbfef9 100644
--- a/core/tests/coretests/src/android/widget/TextViewActivityTest.java
+++ b/core/tests/coretests/src/android/widget/TextViewActivityTest.java
@@ -48,6 +48,9 @@
 import android.test.suitebuilder.annotation.SmallTest;
 import android.view.KeyEvent;
 
+import static org.hamcrest.Matchers.anyOf;
+import static org.hamcrest.Matchers.is;
+
 /**
  * Tests the TextView widget from an Activity
  */
@@ -84,6 +87,43 @@
     }
 
     @SmallTest
+    public void testPositionCursorAtTextAtIndex_arabic() throws Exception {
+        // Arabic text. The expected cursorable boundary is
+        // | \u0623 \u064F | \u067A | \u0633 \u0652 |
+        final String text = "\u0623\u064F\u067A\u0633\u0652";
+        onView(withId(R.id.textview)).perform(click());
+        onView(withId(R.id.textview)).perform(replaceText(text));
+
+        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(0));
+        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(0));
+        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(1));
+        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(anyOf(is(0), is(2))));
+        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(2));
+        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(2));
+        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(3));
+        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(3));
+        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(4));
+        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(anyOf(is(3), is(5))));
+        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(5));
+        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(5));
+    }
+
+    @SmallTest
+    public void testPositionCursorAtTextAtIndex_devanagari() throws Exception {
+        // Devanagari text. The expected cursorable boundary is | \u0915 \u093E |
+        final String text = "\u0915\u093E";
+        onView(withId(R.id.textview)).perform(click());
+        onView(withId(R.id.textview)).perform(replaceText(text));
+
+        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(0));
+        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(0));
+        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(1));
+        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(anyOf(is(0), is(2))));
+        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(2));
+        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(2));
+    }
+
+    @SmallTest
     public void testLongPressToSelect() throws Exception {
         final String helloWorld = "Hello Kirk!";
         onView(withId(R.id.textview)).perform(click());
diff --git a/include/androidfw/DisplayEventDispatcher.h b/include/androidfw/DisplayEventDispatcher.h
new file mode 100644
index 0000000..3ade215
--- /dev/null
+++ b/include/androidfw/DisplayEventDispatcher.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <gui/DisplayEventReceiver.h>
+#include <utils/Log.h>
+#include <utils/Looper.h>
+
+namespace android {
+
+class DisplayEventDispatcher : public LooperCallback {
+public:
+    DisplayEventDispatcher(const sp<Looper>& looper);
+
+    status_t initialize();
+    void dispose();
+    status_t scheduleVsync();
+
+protected:
+    virtual ~DisplayEventDispatcher() = default;
+
+private:
+    sp<Looper> mLooper;
+    DisplayEventReceiver mReceiver;
+    bool mWaitingForVsync;
+
+    virtual void dispatchVsync(nsecs_t timestamp, int32_t id, uint32_t count) = 0;
+    virtual void dispatchHotplug(nsecs_t timestamp, int32_t id, bool connected) = 0;
+
+    virtual int handleEvent(int receiveFd, int events, void* data);
+    bool processPendingEvents(nsecs_t* outTimestamp, int32_t* id, uint32_t* outCount);
+};
+}
diff --git a/include/androidfw/LocaleData.h b/include/androidfw/LocaleData.h
new file mode 100644
index 0000000..add0ab5
--- /dev/null
+++ b/include/androidfw/LocaleData.h
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+
+#ifndef _LIBS_UTILS_LOCALE_DATA_H
+#define _LIBS_UTILS_LOCALE_DATA_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+namespace android {
+
+int localeDataCompareRegions(
+        const char* left_region, const char* right_region,
+        const char* requested_language, const char* requested_script,
+        const char* requested_region);
+
+void localeDataComputeScript(char out[4], const char* language, const char* region);
+
+} // namespace android
+
+#endif // _LIBS_UTILS_LOCALE_DATA_H
diff --git a/include/androidfw/ResourceTypes.h b/include/androidfw/ResourceTypes.h
index 428a2b8..88fecce 100644
--- a/include/androidfw/ResourceTypes.h
+++ b/include/androidfw/ResourceTypes.h
@@ -21,6 +21,7 @@
 #define _LIBS_UTILS_RESOURCE_TYPES_H
 
 #include <androidfw/Asset.h>
+#include <androidfw/LocaleData.h>
 #include <utils/ByteOrder.h>
 #include <utils/Errors.h>
 #include <utils/String16.h>
@@ -1127,8 +1128,9 @@
     // configuration. (eg. Hant, Latn, etc.). Interpreted in conjunction with
     // the locale field.
     char localeScript[4];
+    bool localeScriptWasProvided;
 
-    // A single BCP-47 variant subtag. Will vary in length between 5 and 8
+    // A single BCP-47 variant subtag. Will vary in length between 4 and 8
     // chars. Interpreted in conjunction with the locale field.
     char localeVariant[8];
 
@@ -1228,10 +1230,15 @@
 
     inline void clearLocale() {
         locale = 0;
+        localeScriptWasProvided = false;
         memset(localeScript, 0, sizeof(localeScript));
         memset(localeVariant, 0, sizeof(localeVariant));
     }
 
+    inline void computeScript() {
+        localeDataComputeScript(localeScript, language, country);
+    }
+
     // Get the 2 or 3 letter language code of this configuration. Trailing
     // bytes are set to '\0'.
     size_t unpackLanguage(char language[4]) const;
@@ -1255,6 +1262,12 @@
     // and 0 if they're equally specific.
     int isLocaleMoreSpecificThan(const ResTable_config &o) const;
 
+    // Return true if 'this' is a better locale match than 'o' for the
+    // 'requested' configuration. Similar to isBetterThan(), this assumes that
+    // match() has already been used to remove any configurations that don't
+    // match the requested configuration at all.
+    bool isLocaleBetterThan(const ResTable_config& o, const ResTable_config* requested) const;
+
     String8 toString() const;
 };
 
diff --git a/libs/androidfw/Android.mk b/libs/androidfw/Android.mk
index f682fb8..6bbfcd2 100644
--- a/libs/androidfw/Android.mk
+++ b/libs/androidfw/Android.mk
@@ -21,6 +21,7 @@
     Asset.cpp \
     AssetDir.cpp \
     AssetManager.cpp \
+    LocaleData.cpp \
     misc.cpp \
     ObbFile.cpp \
     ResourceTypes.cpp \
@@ -33,7 +34,8 @@
     $(commonSources) \
     BackupData.cpp \
     BackupHelpers.cpp \
-    CursorWindow.cpp
+    CursorWindow.cpp \
+    DisplayEventDispatcher.cpp
 
 hostSources := $(commonSources)
 
@@ -65,6 +67,7 @@
     libbinder \
     liblog \
     libcutils \
+    libgui \
     libutils \
     libz
 
diff --git a/libs/androidfw/DisplayEventDispatcher.cpp b/libs/androidfw/DisplayEventDispatcher.cpp
new file mode 100644
index 0000000..99cd173
--- /dev/null
+++ b/libs/androidfw/DisplayEventDispatcher.cpp
@@ -0,0 +1,147 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "DisplayEventDispatcher"
+
+#include <cinttypes>
+#include <cstdint>
+
+#include <androidfw/DisplayEventDispatcher.h>
+#include <gui/DisplayEventReceiver.h>
+#include <utils/Log.h>
+#include <utils/Looper.h>
+
+#include <utils/Timers.h>
+
+namespace android {
+
+// Number of events to read at a time from the DisplayEventDispatcher pipe.
+// The value should be large enough that we can quickly drain the pipe
+// using just a few large reads.
+static const size_t EVENT_BUFFER_SIZE = 100;
+
+DisplayEventDispatcher::DisplayEventDispatcher(const sp<Looper>& looper) :
+        mLooper(looper), mWaitingForVsync(false) {
+    ALOGV("dispatcher %p ~ Initializing display event dispatcher.", this);
+}
+
+status_t DisplayEventDispatcher::initialize() {
+    status_t result = mReceiver.initCheck();
+    if (result) {
+        ALOGW("Failed to initialize display event receiver, status=%d", result);
+        return result;
+    }
+
+    int rc = mLooper->addFd(mReceiver.getFd(), 0, Looper::EVENT_INPUT,
+            this, NULL);
+    if (rc < 0) {
+        return UNKNOWN_ERROR;
+    }
+    return OK;
+}
+
+void DisplayEventDispatcher::dispose() {
+    ALOGV("dispatcher %p ~ Disposing display event dispatcher.", this);
+
+    if (!mReceiver.initCheck()) {
+        mLooper->removeFd(mReceiver.getFd());
+    }
+}
+
+status_t DisplayEventDispatcher::scheduleVsync() {
+    if (!mWaitingForVsync) {
+        ALOGV("dispatcher %p ~ Scheduling vsync.", this);
+
+        // Drain all pending events.
+        nsecs_t vsyncTimestamp;
+        int32_t vsyncDisplayId;
+        uint32_t vsyncCount;
+        if (processPendingEvents(&vsyncTimestamp, &vsyncDisplayId, &vsyncCount)) {
+            ALOGE("dispatcher %p ~ last event processed while scheduling was for %" PRId64 "",
+                    this, ns2ms(static_cast<nsecs_t>(vsyncTimestamp)));
+        }
+
+        status_t status = mReceiver.requestNextVsync();
+        if (status) {
+            ALOGW("Failed to request next vsync, status=%d", status);
+            return status;
+        }
+
+        mWaitingForVsync = true;
+    }
+    return OK;
+}
+
+int DisplayEventDispatcher::handleEvent(int, int events, void*) {
+    if (events & (Looper::EVENT_ERROR | Looper::EVENT_HANGUP)) {
+        ALOGE("Display event receiver pipe was closed or an error occurred.  "
+                "events=0x%x", events);
+        return 0; // remove the callback
+    }
+
+    if (!(events & Looper::EVENT_INPUT)) {
+        ALOGW("Received spurious callback for unhandled poll event.  "
+                "events=0x%x", events);
+        return 1; // keep the callback
+    }
+
+    // Drain all pending events, keep the last vsync.
+    nsecs_t vsyncTimestamp;
+    int32_t vsyncDisplayId;
+    uint32_t vsyncCount;
+    if (processPendingEvents(&vsyncTimestamp, &vsyncDisplayId, &vsyncCount)) {
+        ALOGE("dispatcher %p ~ Vsync pulse: timestamp=%" PRId64 ", id=%d, count=%d",
+                this, ns2ms(vsyncTimestamp), vsyncDisplayId, vsyncCount);
+        mWaitingForVsync = false;
+        dispatchVsync(vsyncTimestamp, vsyncDisplayId, vsyncCount);
+    }
+
+    return 1; // keep the callback
+}
+
+bool DisplayEventDispatcher::processPendingEvents(
+        nsecs_t* outTimestamp, int32_t* outId, uint32_t* outCount) {
+    bool gotVsync = false;
+    DisplayEventReceiver::Event buf[EVENT_BUFFER_SIZE];
+    ssize_t n;
+    while ((n = mReceiver.getEvents(buf, EVENT_BUFFER_SIZE)) > 0) {
+        ALOGV("dispatcher %p ~ Read %d events.", this, int(n));
+        for (ssize_t i = 0; i < n; i++) {
+            const DisplayEventReceiver::Event& ev = buf[i];
+            switch (ev.header.type) {
+            case DisplayEventReceiver::DISPLAY_EVENT_VSYNC:
+                // Later vsync events will just overwrite the info from earlier
+                // ones. That's fine, we only care about the most recent.
+                gotVsync = true;
+                *outTimestamp = ev.header.timestamp;
+                *outId = ev.header.id;
+                *outCount = ev.vsync.count;
+                break;
+            case DisplayEventReceiver::DISPLAY_EVENT_HOTPLUG:
+                dispatchHotplug(ev.header.timestamp, ev.header.id, ev.hotplug.connected);
+                break;
+            default:
+                ALOGW("dispatcher %p ~ ignoring unknown event type %#x", this, ev.header.type);
+                break;
+            }
+        }
+    }
+    if (n < 0) {
+        ALOGW("Failed to get events from display event dispatcher, status=%d", status_t(n));
+    }
+    return gotVsync;
+}
+}
diff --git a/libs/androidfw/LocaleData.cpp b/libs/androidfw/LocaleData.cpp
new file mode 100644
index 0000000..c0c3ab8
--- /dev/null
+++ b/libs/androidfw/LocaleData.cpp
@@ -0,0 +1,201 @@
+/*
+ * 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.
+ */
+
+#include <cstdint>
+#include <cstdlib>
+#include <cstring>
+#include <string>
+#include <unordered_map>
+#include <unordered_set>
+
+#include <androidfw/LocaleData.h>
+
+namespace android {
+
+#include "LocaleDataTables.cpp"
+
+inline uint32_t packLocale(const char* language, const char* region) {
+    return (((uint8_t) language[0]) << 24u) | (((uint8_t) language[1]) << 16u) |
+           (((uint8_t) region[0]) << 8u) | ((uint8_t) region[1]);
+}
+
+inline uint32_t dropRegion(uint32_t packed_locale) {
+    return packed_locale & 0xFFFF0000lu;
+}
+
+inline bool hasRegion(uint32_t packed_locale) {
+    return (packed_locale & 0x0000FFFFlu) != 0;
+}
+
+const size_t SCRIPT_LENGTH = 4;
+const size_t SCRIPT_PARENTS_COUNT = sizeof(SCRIPT_PARENTS)/sizeof(SCRIPT_PARENTS[0]);
+const uint32_t PACKED_ROOT = 0; // to represent the root locale
+
+uint32_t findParent(uint32_t packed_locale, const char* script) {
+    if (hasRegion(packed_locale)) {
+        for (size_t i = 0; i < SCRIPT_PARENTS_COUNT; i++) {
+            if (memcmp(script, SCRIPT_PARENTS[i].script, SCRIPT_LENGTH) == 0) {
+                auto map = SCRIPT_PARENTS[i].map;
+                auto lookup_result = map->find(packed_locale);
+                if (lookup_result != map->end()) {
+                    return lookup_result->second;
+                }
+                break;
+            }
+        }
+        return dropRegion(packed_locale);
+    }
+    return PACKED_ROOT;
+}
+
+// Find the ancestors of a locale, and fill 'out' with it (assumes out has enough
+// space). If any of the members of stop_list was seen, write it in the
+// output but stop afterwards.
+//
+// This also outputs the index of the last written ancestor in the stop_list
+// to stop_list_index, which will be -1 if it is not found in the stop_list.
+//
+// Returns the number of ancestors written in the output, which is always
+// at least one.
+size_t findAncestors(uint32_t* out, ssize_t* stop_list_index,
+                     uint32_t packed_locale, const char* script,
+                     const uint32_t* stop_list, size_t stop_set_length) {
+    uint32_t ancestor = packed_locale;
+    size_t count = 0;
+    do {
+        out[count++] = ancestor;
+        for (size_t i = 0; i < stop_set_length; i++) {
+            if (stop_list[i] == ancestor) {
+                *stop_list_index = (ssize_t) i;
+                return count;
+            }
+        }
+        ancestor = findParent(ancestor, script);
+    } while (ancestor != PACKED_ROOT);
+    *stop_list_index = (ssize_t) -1;
+    return count;
+}
+
+size_t findDistance(uint32_t supported,
+                    const char* script,
+                    const uint32_t* request_ancestors,
+                    size_t request_ancestors_count) {
+    uint32_t supported_ancestors[MAX_PARENT_DEPTH+1];
+    ssize_t request_ancestors_index;
+    const size_t supported_ancestor_count = findAncestors(
+            supported_ancestors, &request_ancestors_index,
+            supported, script,
+            request_ancestors, request_ancestors_count);
+    // Since both locales share the same root, there will always be a shared
+    // ancestor, so the distance in the parent tree is the sum of the distance
+    // of 'supported' to the lowest common ancestor (number of ancestors
+    // written for 'supported' minus 1) plus the distance of 'request' to the
+    // lowest common ancestor (the index of the ancestor in request_ancestors).
+    return supported_ancestor_count + request_ancestors_index - 1;
+}
+
+inline bool isRepresentative(uint32_t language_and_region, const char* script) {
+    const uint64_t packed_locale = (
+            (((uint64_t) language_and_region) << 32u) |
+            (((uint64_t) script[0]) << 24u) |
+            (((uint64_t) script[1]) << 16u) |
+            (((uint64_t) script[2]) <<  8u) |
+            ((uint64_t) script[3]));
+
+    return (REPRESENTATIVE_LOCALES.count(packed_locale) != 0);
+}
+
+int localeDataCompareRegions(
+        const char* left_region, const char* right_region,
+        const char* requested_language, const char* requested_script,
+        const char* requested_region) {
+
+    if (left_region[0] == right_region[0] && left_region[1] == right_region[1]) {
+        return 0;
+    }
+    const uint32_t left = packLocale(requested_language, left_region);
+    const uint32_t right = packLocale(requested_language, right_region);
+    const uint32_t request = packLocale(requested_language, requested_region);
+
+    uint32_t request_ancestors[MAX_PARENT_DEPTH+1];
+    ssize_t left_right_index;
+    // Find the parents of the request, but stop as soon as we saw left or right
+    const uint32_t left_and_right[] = {left, right};
+    const size_t ancestor_count = findAncestors(
+            request_ancestors, &left_right_index,
+            request, requested_script,
+            left_and_right, sizeof(left_and_right)/sizeof(left_and_right[0]));
+    if (left_right_index == 0) { // We saw left earlier
+        return 1;
+    }
+    if (left_right_index == 1) { // We saw right earlier
+        return -1;
+    }
+
+    // If we are here, neither left nor right are an ancestor of the
+    // request. This means that all the ancestors have been computed and
+    // the last ancestor is just the language by itself. We will use the
+    // distance in the parent tree for determining the better match.
+    const size_t left_distance = findDistance(
+            left, requested_script, request_ancestors, ancestor_count);
+    const size_t right_distance = findDistance(
+            right, requested_script, request_ancestors, ancestor_count);
+    if (left_distance != right_distance) {
+        return (int) right_distance - (int) left_distance; // smaller distance is better
+    }
+
+    // If we are here, left and right are equidistant from the request. We will
+    // try and see if any of them is a representative locale.
+    const bool left_is_representative = isRepresentative(left, requested_script);
+    const bool right_is_representative = isRepresentative(right, requested_script);
+    if (left_is_representative != right_is_representative) {
+        return (int) left_is_representative - (int) right_is_representative;
+    }
+
+    // We have no way of figuring out which locale is a better match. For
+    // the sake of stability, we consider the locale with the lower region
+    // code (in dictionary order) better, with two-letter codes before
+    // three-digit codes (since two-letter codes are more specific).
+    return (int64_t) right - (int64_t) left;
+}
+
+void localeDataComputeScript(char out[4], const char* language, const char* region) {
+    if (language[0] == '\0') {
+        memset(out, '\0', SCRIPT_LENGTH);
+        return;
+    }
+    uint32_t lookup_key = packLocale(language, region);
+    auto lookup_result = LIKELY_SCRIPTS.find(lookup_key);
+    if (lookup_result == LIKELY_SCRIPTS.end()) {
+        // We couldn't find the locale. Let's try without the region
+        if (region[0] != '\0') {
+            lookup_key = dropRegion(lookup_key);
+            lookup_result = LIKELY_SCRIPTS.find(lookup_key);
+            if (lookup_result != LIKELY_SCRIPTS.end()) {
+                memcpy(out, SCRIPT_CODES[lookup_result->second], SCRIPT_LENGTH);
+                return;
+            }
+        }
+        // We don't know anything about the locale
+        memset(out, '\0', SCRIPT_LENGTH);
+        return;
+    } else {
+        // We found the locale.
+        memcpy(out, SCRIPT_CODES[lookup_result->second], SCRIPT_LENGTH);
+    }
+}
+
+} // namespace android
diff --git a/libs/androidfw/LocaleDataTables.cpp b/libs/androidfw/LocaleDataTables.cpp
new file mode 100644
index 0000000..1ac5085
--- /dev/null
+++ b/libs/androidfw/LocaleDataTables.cpp
@@ -0,0 +1,1701 @@
+// Auto-generated by frameworks/base/tools/localedata/extract_icu_data.py
+
+const char SCRIPT_CODES[][4] = {
+    /* 0  */ {'A', 'h', 'o', 'm'},
+    /* 1  */ {'A', 'r', 'a', 'b'},
+    /* 2  */ {'A', 'r', 'm', 'i'},
+    /* 3  */ {'A', 'r', 'm', 'n'},
+    /* 4  */ {'A', 'v', 's', 't'},
+    /* 5  */ {'B', 'a', 'm', 'u'},
+    /* 6  */ {'B', 'a', 's', 's'},
+    /* 7  */ {'B', 'e', 'n', 'g'},
+    /* 8  */ {'B', 'r', 'a', 'h'},
+    /* 9  */ {'C', 'a', 'n', 's'},
+    /* 10 */ {'C', 'a', 'r', 'i'},
+    /* 11 */ {'C', 'h', 'a', 'm'},
+    /* 12 */ {'C', 'h', 'e', 'r'},
+    /* 13 */ {'C', 'o', 'p', 't'},
+    /* 14 */ {'C', 'p', 'r', 't'},
+    /* 15 */ {'C', 'y', 'r', 'l'},
+    /* 16 */ {'D', 'e', 'v', 'a'},
+    /* 17 */ {'E', 'g', 'y', 'p'},
+    /* 18 */ {'E', 't', 'h', 'i'},
+    /* 19 */ {'G', 'e', 'o', 'r'},
+    /* 20 */ {'G', 'o', 't', 'h'},
+    /* 21 */ {'G', 'r', 'e', 'k'},
+    /* 22 */ {'G', 'u', 'j', 'r'},
+    /* 23 */ {'G', 'u', 'r', 'u'},
+    /* 24 */ {'H', 'a', 'n', 's'},
+    /* 25 */ {'H', 'a', 'n', 't'},
+    /* 26 */ {'H', 'a', 't', 'r'},
+    /* 27 */ {'H', 'e', 'b', 'r'},
+    /* 28 */ {'H', 'l', 'u', 'w'},
+    /* 29 */ {'H', 'm', 'n', 'g'},
+    /* 30 */ {'I', 't', 'a', 'l'},
+    /* 31 */ {'J', 'p', 'a', 'n'},
+    /* 32 */ {'K', 'a', 'l', 'i'},
+    /* 33 */ {'K', 'a', 'n', 'a'},
+    /* 34 */ {'K', 'h', 'a', 'r'},
+    /* 35 */ {'K', 'h', 'm', 'r'},
+    /* 36 */ {'K', 'n', 'd', 'a'},
+    /* 37 */ {'K', 'o', 'r', 'e'},
+    /* 38 */ {'K', 't', 'h', 'i'},
+    /* 39 */ {'L', 'a', 'n', 'a'},
+    /* 40 */ {'L', 'a', 'o', 'o'},
+    /* 41 */ {'L', 'a', 't', 'n'},
+    /* 42 */ {'L', 'e', 'p', 'c'},
+    /* 43 */ {'L', 'i', 'n', 'a'},
+    /* 44 */ {'L', 'i', 's', 'u'},
+    /* 45 */ {'L', 'y', 'c', 'i'},
+    /* 46 */ {'L', 'y', 'd', 'i'},
+    /* 47 */ {'M', 'a', 'n', 'd'},
+    /* 48 */ {'M', 'a', 'n', 'i'},
+    /* 49 */ {'M', 'e', 'r', 'c'},
+    /* 50 */ {'M', 'l', 'y', 'm'},
+    /* 51 */ {'M', 'o', 'n', 'g'},
+    /* 52 */ {'M', 'r', 'o', 'o'},
+    /* 53 */ {'M', 'y', 'm', 'r'},
+    /* 54 */ {'N', 'a', 'r', 'b'},
+    /* 55 */ {'N', 'k', 'o', 'o'},
+    /* 56 */ {'O', 'g', 'a', 'm'},
+    /* 57 */ {'O', 'r', 'k', 'h'},
+    /* 58 */ {'O', 'r', 'y', 'a'},
+    /* 59 */ {'P', 'a', 'u', 'c'},
+    /* 60 */ {'P', 'h', 'l', 'i'},
+    /* 61 */ {'P', 'h', 'n', 'x'},
+    /* 62 */ {'P', 'l', 'r', 'd'},
+    /* 63 */ {'P', 'r', 't', 'i'},
+    /* 64 */ {'R', 'u', 'n', 'r'},
+    /* 65 */ {'S', 'a', 'm', 'r'},
+    /* 66 */ {'S', 'a', 'r', 'b'},
+    /* 67 */ {'S', 'a', 'u', 'r'},
+    /* 68 */ {'S', 'g', 'n', 'w'},
+    /* 69 */ {'S', 'i', 'n', 'h'},
+    /* 70 */ {'S', 'o', 'r', 'a'},
+    /* 71 */ {'S', 'y', 'r', 'c'},
+    /* 72 */ {'T', 'a', 'l', 'e'},
+    /* 73 */ {'T', 'a', 'l', 'u'},
+    /* 74 */ {'T', 'a', 'm', 'l'},
+    /* 75 */ {'T', 'a', 'v', 't'},
+    /* 76 */ {'T', 'e', 'l', 'u'},
+    /* 77 */ {'T', 'f', 'n', 'g'},
+    /* 78 */ {'T', 'h', 'a', 'a'},
+    /* 79 */ {'T', 'h', 'a', 'i'},
+    /* 80 */ {'T', 'i', 'b', 't'},
+    /* 81 */ {'U', 'g', 'a', 'r'},
+    /* 82 */ {'V', 'a', 'i', 'i'},
+    /* 83 */ {'X', 'p', 'e', 'o'},
+    /* 84 */ {'X', 's', 'u', 'x'},
+    /* 85 */ {'Y', 'i', 'i', 'i'},
+    /* 86 */ {'~', '~', '~', 'A'},
+    /* 87 */ {'~', '~', '~', 'B'},
+};
+
+
+const std::unordered_map<uint32_t, uint8_t> LIKELY_SCRIPTS({
+    {0x61610000u, 41u}, // aa -> Latn
+    {0x61620000u, 15u}, // ab -> Cyrl
+    {0xC4200000u, 41u}, // abr -> Latn
+    {0x90400000u, 41u}, // ace -> Latn
+    {0x9C400000u, 41u}, // ach -> Latn
+    {0x80600000u, 41u}, // ada -> Latn
+    {0xE0600000u, 15u}, // ady -> Cyrl
+    {0x61650000u,  4u}, // ae -> Avst
+    {0x84800000u,  1u}, // aeb -> Arab
+    {0x61660000u, 41u}, // af -> Latn
+    {0xC0C00000u, 41u}, // agq -> Latn
+    {0xB8E00000u,  0u}, // aho -> Ahom
+    {0x616B0000u, 41u}, // ak -> Latn
+    {0xA9400000u, 84u}, // akk -> Xsux
+    {0xB5600000u, 41u}, // aln -> Latn
+    {0xCD600000u, 15u}, // alt -> Cyrl
+    {0x616D0000u, 18u}, // am -> Ethi
+    {0xB9800000u, 41u}, // amo -> Latn
+    {0xE5C00000u, 41u}, // aoz -> Latn
+    {0x61720000u,  1u}, // ar -> Arab
+    {0x61725842u, 87u}, // ar-XB -> ~~~B
+    {0x8A200000u,  2u}, // arc -> Armi
+    {0xB6200000u, 41u}, // arn -> Latn
+    {0xBA200000u, 41u}, // aro -> Latn
+    {0xC2200000u,  1u}, // arq -> Arab
+    {0xE2200000u,  1u}, // ary -> Arab
+    {0xE6200000u,  1u}, // arz -> Arab
+    {0x61730000u,  7u}, // as -> Beng
+    {0x82400000u, 41u}, // asa -> Latn
+    {0x92400000u, 68u}, // ase -> Sgnw
+    {0xCE400000u, 41u}, // ast -> Latn
+    {0xA6600000u, 41u}, // atj -> Latn
+    {0x61760000u, 15u}, // av -> Cyrl
+    {0x82C00000u, 16u}, // awa -> Deva
+    {0x61790000u, 41u}, // ay -> Latn
+    {0x617A0000u, 41u}, // az -> Latn
+    {0x617A4951u,  1u}, // az-IQ -> Arab
+    {0x617A4952u,  1u}, // az-IR -> Arab
+    {0x617A5255u, 15u}, // az-RU -> Cyrl
+    {0x62610000u, 15u}, // ba -> Cyrl
+    {0xAC010000u,  1u}, // bal -> Arab
+    {0xB4010000u, 41u}, // ban -> Latn
+    {0xBC010000u, 16u}, // bap -> Deva
+    {0xC4010000u, 41u}, // bar -> Latn
+    {0xC8010000u, 41u}, // bas -> Latn
+    {0xDC010000u,  5u}, // bax -> Bamu
+    {0x88210000u, 41u}, // bbc -> Latn
+    {0xA4210000u, 41u}, // bbj -> Latn
+    {0xA0410000u, 41u}, // bci -> Latn
+    {0x62650000u, 15u}, // be -> Cyrl
+    {0xA4810000u,  1u}, // bej -> Arab
+    {0xB0810000u, 41u}, // bem -> Latn
+    {0xD8810000u, 41u}, // bew -> Latn
+    {0xE4810000u, 41u}, // bez -> Latn
+    {0x8CA10000u, 41u}, // bfd -> Latn
+    {0xC0A10000u, 74u}, // bfq -> Taml
+    {0xCCA10000u,  1u}, // bft -> Arab
+    {0xE0A10000u, 16u}, // bfy -> Deva
+    {0x62670000u, 15u}, // bg -> Cyrl
+    {0x88C10000u, 16u}, // bgc -> Deva
+    {0xB4C10000u,  1u}, // bgn -> Arab
+    {0xDCC10000u, 21u}, // bgx -> Grek
+    {0x62680000u, 38u}, // bh -> Kthi
+    {0x84E10000u, 16u}, // bhb -> Deva
+    {0xA0E10000u, 16u}, // bhi -> Deva
+    {0xA8E10000u, 41u}, // bhk -> Latn
+    {0xB8E10000u, 16u}, // bho -> Deva
+    {0x62690000u, 41u}, // bi -> Latn
+    {0xA9010000u, 41u}, // bik -> Latn
+    {0xB5010000u, 41u}, // bin -> Latn
+    {0xA5210000u, 16u}, // bjj -> Deva
+    {0xB5210000u, 41u}, // bjn -> Latn
+    {0xB1410000u, 41u}, // bkm -> Latn
+    {0xD1410000u, 41u}, // bku -> Latn
+    {0xCD610000u, 75u}, // blt -> Tavt
+    {0x626D0000u, 41u}, // bm -> Latn
+    {0xC1810000u, 41u}, // bmq -> Latn
+    {0x626E0000u,  7u}, // bn -> Beng
+    {0x626F0000u, 80u}, // bo -> Tibt
+    {0xE1E10000u,  7u}, // bpy -> Beng
+    {0xA2010000u,  1u}, // bqi -> Arab
+    {0xD6010000u, 41u}, // bqv -> Latn
+    {0x62720000u, 41u}, // br -> Latn
+    {0x82210000u, 16u}, // bra -> Deva
+    {0x9E210000u,  1u}, // brh -> Arab
+    {0xDE210000u, 16u}, // brx -> Deva
+    {0x62730000u, 41u}, // bs -> Latn
+    {0xC2410000u,  6u}, // bsq -> Bass
+    {0xCA410000u, 41u}, // bss -> Latn
+    {0xBA610000u, 41u}, // bto -> Latn
+    {0xD6610000u, 16u}, // btv -> Deva
+    {0x82810000u, 15u}, // bua -> Cyrl
+    {0x8A810000u, 41u}, // buc -> Latn
+    {0x9A810000u, 41u}, // bug -> Latn
+    {0xB2810000u, 41u}, // bum -> Latn
+    {0x86A10000u, 41u}, // bvb -> Latn
+    {0xB7010000u, 18u}, // byn -> Ethi
+    {0xD7010000u, 41u}, // byv -> Latn
+    {0x93210000u, 41u}, // bze -> Latn
+    {0x63610000u, 41u}, // ca -> Latn
+    {0x9C420000u, 41u}, // cch -> Latn
+    {0xBC420000u,  7u}, // ccp -> Beng
+    {0x63650000u, 15u}, // ce -> Cyrl
+    {0x84820000u, 41u}, // ceb -> Latn
+    {0x98C20000u, 41u}, // cgg -> Latn
+    {0x63680000u, 41u}, // ch -> Latn
+    {0xA8E20000u, 41u}, // chk -> Latn
+    {0xB0E20000u, 15u}, // chm -> Cyrl
+    {0xB8E20000u, 41u}, // cho -> Latn
+    {0xBCE20000u, 41u}, // chp -> Latn
+    {0xC4E20000u, 12u}, // chr -> Cher
+    {0x81220000u,  1u}, // cja -> Arab
+    {0xB1220000u, 11u}, // cjm -> Cham
+    {0x85420000u,  1u}, // ckb -> Arab
+    {0x636F0000u, 41u}, // co -> Latn
+    {0xBDC20000u, 13u}, // cop -> Copt
+    {0xC9E20000u, 41u}, // cps -> Latn
+    {0x63720000u,  9u}, // cr -> Cans
+    {0xA6220000u,  9u}, // crj -> Cans
+    {0xAA220000u,  9u}, // crk -> Cans
+    {0xAE220000u,  9u}, // crl -> Cans
+    {0xB2220000u,  9u}, // crm -> Cans
+    {0xCA220000u, 41u}, // crs -> Latn
+    {0x63730000u, 41u}, // cs -> Latn
+    {0x86420000u, 41u}, // csb -> Latn
+    {0xDA420000u,  9u}, // csw -> Cans
+    {0x8E620000u, 59u}, // ctd -> Pauc
+    {0x63750000u, 15u}, // cu -> Cyrl
+    {0x63760000u, 15u}, // cv -> Cyrl
+    {0x63790000u, 41u}, // cy -> Latn
+    {0x64610000u, 41u}, // da -> Latn
+    {0xA8030000u, 41u}, // dak -> Latn
+    {0xC4030000u, 15u}, // dar -> Cyrl
+    {0xD4030000u, 41u}, // dav -> Latn
+    {0x88430000u,  1u}, // dcc -> Arab
+    {0x64650000u, 41u}, // de -> Latn
+    {0xB4830000u, 41u}, // den -> Latn
+    {0xC4C30000u, 41u}, // dgr -> Latn
+    {0x91230000u, 41u}, // dje -> Latn
+    {0xA5A30000u, 41u}, // dnj -> Latn
+    {0xA1C30000u,  1u}, // doi -> Arab
+    {0x86430000u, 41u}, // dsb -> Latn
+    {0xB2630000u, 41u}, // dtm -> Latn
+    {0xBE630000u, 41u}, // dtp -> Latn
+    {0x82830000u, 41u}, // dua -> Latn
+    {0x64760000u, 78u}, // dv -> Thaa
+    {0xBB030000u, 41u}, // dyo -> Latn
+    {0xD3030000u, 41u}, // dyu -> Latn
+    {0x647A0000u, 80u}, // dz -> Tibt
+    {0xD0240000u, 41u}, // ebu -> Latn
+    {0x65650000u, 41u}, // ee -> Latn
+    {0xA0A40000u, 41u}, // efi -> Latn
+    {0xACC40000u, 41u}, // egl -> Latn
+    {0xE0C40000u, 17u}, // egy -> Egyp
+    {0xE1440000u, 32u}, // eky -> Kali
+    {0x656C0000u, 21u}, // el -> Grek
+    {0x656E0000u, 41u}, // en -> Latn
+    {0x656E5841u, 86u}, // en-XA -> ~~~A
+    {0x656F0000u, 41u}, // eo -> Latn
+    {0x65730000u, 41u}, // es -> Latn
+    {0xD2440000u, 41u}, // esu -> Latn
+    {0x65740000u, 41u}, // et -> Latn
+    {0xCE640000u, 30u}, // ett -> Ital
+    {0x65750000u, 41u}, // eu -> Latn
+    {0xBAC40000u, 41u}, // ewo -> Latn
+    {0xCEE40000u, 41u}, // ext -> Latn
+    {0x66610000u,  1u}, // fa -> Arab
+    {0xB4050000u, 41u}, // fan -> Latn
+    {0x66660000u, 41u}, // ff -> Latn
+    {0xB0A50000u, 41u}, // ffm -> Latn
+    {0x66690000u, 41u}, // fi -> Latn
+    {0x81050000u,  1u}, // fia -> Arab
+    {0xAD050000u, 41u}, // fil -> Latn
+    {0xCD050000u, 41u}, // fit -> Latn
+    {0x666A0000u, 41u}, // fj -> Latn
+    {0x666F0000u, 41u}, // fo -> Latn
+    {0xB5C50000u, 41u}, // fon -> Latn
+    {0x66720000u, 41u}, // fr -> Latn
+    {0x8A250000u, 41u}, // frc -> Latn
+    {0xBE250000u, 41u}, // frp -> Latn
+    {0xC6250000u, 41u}, // frr -> Latn
+    {0xCA250000u, 41u}, // frs -> Latn
+    {0x8E850000u, 41u}, // fud -> Latn
+    {0xC2850000u, 41u}, // fuq -> Latn
+    {0xC6850000u, 41u}, // fur -> Latn
+    {0xD6850000u, 41u}, // fuv -> Latn
+    {0xC6A50000u, 41u}, // fvr -> Latn
+    {0x66790000u, 41u}, // fy -> Latn
+    {0x67610000u, 41u}, // ga -> Latn
+    {0x80060000u, 41u}, // gaa -> Latn
+    {0x98060000u, 41u}, // gag -> Latn
+    {0xB4060000u, 24u}, // gan -> Hans
+    {0xE0060000u, 41u}, // gay -> Latn
+    {0xB0260000u, 16u}, // gbm -> Deva
+    {0xE4260000u,  1u}, // gbz -> Arab
+    {0xC4460000u, 41u}, // gcr -> Latn
+    {0x67640000u, 41u}, // gd -> Latn
+    {0xE4860000u, 18u}, // gez -> Ethi
+    {0xB4C60000u, 16u}, // ggn -> Deva
+    {0xAD060000u, 41u}, // gil -> Latn
+    {0xA9260000u,  1u}, // gjk -> Arab
+    {0xD1260000u,  1u}, // gju -> Arab
+    {0x676C0000u, 41u}, // gl -> Latn
+    {0xA9660000u,  1u}, // glk -> Arab
+    {0x676E0000u, 41u}, // gn -> Latn
+    {0xB1C60000u, 16u}, // gom -> Deva
+    {0xB5C60000u, 76u}, // gon -> Telu
+    {0xC5C60000u, 41u}, // gor -> Latn
+    {0xC9C60000u, 41u}, // gos -> Latn
+    {0xCDC60000u, 20u}, // got -> Goth
+    {0x8A260000u, 14u}, // grc -> Cprt
+    {0xCE260000u,  7u}, // grt -> Beng
+    {0xDA460000u, 41u}, // gsw -> Latn
+    {0x67750000u, 22u}, // gu -> Gujr
+    {0x86860000u, 41u}, // gub -> Latn
+    {0x8A860000u, 41u}, // guc -> Latn
+    {0xC6860000u, 41u}, // gur -> Latn
+    {0xE6860000u, 41u}, // guz -> Latn
+    {0x67760000u, 41u}, // gv -> Latn
+    {0xC6A60000u, 16u}, // gvr -> Deva
+    {0xA2C60000u, 41u}, // gwi -> Latn
+    {0x68610000u, 41u}, // ha -> Latn
+    {0x6861434Du,  1u}, // ha-CM -> Arab
+    {0x68615344u,  1u}, // ha-SD -> Arab
+    {0xA8070000u, 24u}, // hak -> Hans
+    {0xD8070000u, 41u}, // haw -> Latn
+    {0xE4070000u,  1u}, // haz -> Arab
+    {0x68650000u, 27u}, // he -> Hebr
+    {0x68690000u, 16u}, // hi -> Deva
+    {0x95070000u, 41u}, // hif -> Latn
+    {0xAD070000u, 41u}, // hil -> Latn
+    {0xD1670000u, 28u}, // hlu -> Hluw
+    {0x8D870000u, 62u}, // hmd -> Plrd
+    {0x8DA70000u,  1u}, // hnd -> Arab
+    {0x91A70000u, 16u}, // hne -> Deva
+    {0xA5A70000u, 29u}, // hnj -> Hmng
+    {0xB5A70000u, 41u}, // hnn -> Latn
+    {0xB9A70000u,  1u}, // hno -> Arab
+    {0x686F0000u, 41u}, // ho -> Latn
+    {0x89C70000u, 16u}, // hoc -> Deva
+    {0xA5C70000u, 16u}, // hoj -> Deva
+    {0x68720000u, 41u}, // hr -> Latn
+    {0x86470000u, 41u}, // hsb -> Latn
+    {0xB6470000u, 24u}, // hsn -> Hans
+    {0x68740000u, 41u}, // ht -> Latn
+    {0x68750000u, 41u}, // hu -> Latn
+    {0x68790000u,  3u}, // hy -> Armn
+    {0x687A0000u, 41u}, // hz -> Latn
+    {0x69610000u, 41u}, // ia -> Latn
+    {0x80280000u, 41u}, // iba -> Latn
+    {0x84280000u, 41u}, // ibb -> Latn
+    {0x69640000u, 41u}, // id -> Latn
+    {0x69670000u, 41u}, // ig -> Latn
+    {0x69690000u, 85u}, // ii -> Yiii
+    {0x696B0000u, 41u}, // ik -> Latn
+    {0xCD480000u, 41u}, // ikt -> Latn
+    {0xB9680000u, 41u}, // ilo -> Latn
+    {0x696E0000u, 41u}, // in -> Latn
+    {0x9DA80000u, 15u}, // inh -> Cyrl
+    {0x69730000u, 41u}, // is -> Latn
+    {0x69740000u, 41u}, // it -> Latn
+    {0x69750000u,  9u}, // iu -> Cans
+    {0x69770000u, 27u}, // iw -> Hebr
+    {0x9F280000u, 41u}, // izh -> Latn
+    {0x6A610000u, 31u}, // ja -> Jpan
+    {0xB0090000u, 41u}, // jam -> Latn
+    {0xB8C90000u, 41u}, // jgo -> Latn
+    {0x6A690000u, 27u}, // ji -> Hebr
+    {0x89890000u, 41u}, // jmc -> Latn
+    {0xAD890000u, 16u}, // jml -> Deva
+    {0xCE890000u, 41u}, // jut -> Latn
+    {0x6A760000u, 41u}, // jv -> Latn
+    {0x6A770000u, 41u}, // jw -> Latn
+    {0x6B610000u, 19u}, // ka -> Geor
+    {0x800A0000u, 15u}, // kaa -> Cyrl
+    {0x840A0000u, 41u}, // kab -> Latn
+    {0x880A0000u, 41u}, // kac -> Latn
+    {0xA40A0000u, 41u}, // kaj -> Latn
+    {0xB00A0000u, 41u}, // kam -> Latn
+    {0xB80A0000u, 41u}, // kao -> Latn
+    {0x8C2A0000u, 15u}, // kbd -> Cyrl
+    {0x984A0000u, 41u}, // kcg -> Latn
+    {0xA84A0000u, 41u}, // kck -> Latn
+    {0x906A0000u, 41u}, // kde -> Latn
+    {0xCC6A0000u, 79u}, // kdt -> Thai
+    {0x808A0000u, 41u}, // kea -> Latn
+    {0xB48A0000u, 41u}, // ken -> Latn
+    {0xB8AA0000u, 41u}, // kfo -> Latn
+    {0xC4AA0000u, 16u}, // kfr -> Deva
+    {0xE0AA0000u, 16u}, // kfy -> Deva
+    {0x6B670000u, 41u}, // kg -> Latn
+    {0x90CA0000u, 41u}, // kge -> Latn
+    {0xBCCA0000u, 41u}, // kgp -> Latn
+    {0x80EA0000u, 41u}, // kha -> Latn
+    {0x84EA0000u, 73u}, // khb -> Talu
+    {0xB4EA0000u, 16u}, // khn -> Deva
+    {0xC0EA0000u, 41u}, // khq -> Latn
+    {0xCCEA0000u, 53u}, // kht -> Mymr
+    {0xD8EA0000u,  1u}, // khw -> Arab
+    {0x6B690000u, 41u}, // ki -> Latn
+    {0xD10A0000u, 41u}, // kiu -> Latn
+    {0x6B6A0000u, 41u}, // kj -> Latn
+    {0x992A0000u, 40u}, // kjg -> Laoo
+    {0x6B6B0000u, 15u}, // kk -> Cyrl
+    {0x6B6B4146u,  1u}, // kk-AF -> Arab
+    {0x6B6B434Eu,  1u}, // kk-CN -> Arab
+    {0x6B6B4952u,  1u}, // kk-IR -> Arab
+    {0x6B6B4D4Eu,  1u}, // kk-MN -> Arab
+    {0xA54A0000u, 41u}, // kkj -> Latn
+    {0x6B6C0000u, 41u}, // kl -> Latn
+    {0xB56A0000u, 41u}, // kln -> Latn
+    {0x6B6D0000u, 35u}, // km -> Khmr
+    {0x858A0000u, 41u}, // kmb -> Latn
+    {0x6B6E0000u, 36u}, // kn -> Knda
+    {0x6B6F0000u, 37u}, // ko -> Kore
+    {0xA1CA0000u, 15u}, // koi -> Cyrl
+    {0xA9CA0000u, 16u}, // kok -> Deva
+    {0xC9CA0000u, 41u}, // kos -> Latn
+    {0x91EA0000u, 41u}, // kpe -> Latn
+    {0x8A2A0000u, 15u}, // krc -> Cyrl
+    {0xA22A0000u, 41u}, // kri -> Latn
+    {0xA62A0000u, 41u}, // krj -> Latn
+    {0xAE2A0000u, 41u}, // krl -> Latn
+    {0xD22A0000u, 16u}, // kru -> Deva
+    {0x6B730000u,  1u}, // ks -> Arab
+    {0x864A0000u, 41u}, // ksb -> Latn
+    {0x964A0000u, 41u}, // ksf -> Latn
+    {0x9E4A0000u, 41u}, // ksh -> Latn
+    {0x6B750000u, 41u}, // ku -> Latn
+    {0x6B754952u,  1u}, // ku-IR -> Arab
+    {0x6B754C42u,  1u}, // ku-LB -> Arab
+    {0xB28A0000u, 15u}, // kum -> Cyrl
+    {0x6B760000u, 15u}, // kv -> Cyrl
+    {0xC6AA0000u, 41u}, // kvr -> Latn
+    {0xDEAA0000u,  1u}, // kvx -> Arab
+    {0x6B770000u, 41u}, // kw -> Latn
+    {0xB2EA0000u, 79u}, // kxm -> Thai
+    {0xBEEA0000u,  1u}, // kxp -> Arab
+    {0x6B790000u, 15u}, // ky -> Cyrl
+    {0x6B79434Eu,  1u}, // ky-CN -> Arab
+    {0x6B795452u, 41u}, // ky-TR -> Latn
+    {0x6C610000u, 41u}, // la -> Latn
+    {0x840B0000u, 43u}, // lab -> Lina
+    {0x8C0B0000u, 27u}, // lad -> Hebr
+    {0x980B0000u, 41u}, // lag -> Latn
+    {0x9C0B0000u,  1u}, // lah -> Arab
+    {0xA40B0000u, 41u}, // laj -> Latn
+    {0x6C620000u, 41u}, // lb -> Latn
+    {0x902B0000u, 15u}, // lbe -> Cyrl
+    {0xD82B0000u, 41u}, // lbw -> Latn
+    {0xBC4B0000u, 79u}, // lcp -> Thai
+    {0xBC8B0000u, 42u}, // lep -> Lepc
+    {0xE48B0000u, 15u}, // lez -> Cyrl
+    {0x6C670000u, 41u}, // lg -> Latn
+    {0x6C690000u, 41u}, // li -> Latn
+    {0x950B0000u, 16u}, // lif -> Deva
+    {0xA50B0000u, 41u}, // lij -> Latn
+    {0xC90B0000u, 44u}, // lis -> Lisu
+    {0xBD2B0000u, 41u}, // ljp -> Latn
+    {0xA14B0000u,  1u}, // lki -> Arab
+    {0xCD4B0000u, 41u}, // lkt -> Latn
+    {0xB58B0000u, 76u}, // lmn -> Telu
+    {0xB98B0000u, 41u}, // lmo -> Latn
+    {0x6C6E0000u, 41u}, // ln -> Latn
+    {0x6C6F0000u, 40u}, // lo -> Laoo
+    {0xADCB0000u, 41u}, // lol -> Latn
+    {0xE5CB0000u, 41u}, // loz -> Latn
+    {0x8A2B0000u,  1u}, // lrc -> Arab
+    {0x6C740000u, 41u}, // lt -> Latn
+    {0x9A6B0000u, 41u}, // ltg -> Latn
+    {0x6C750000u, 41u}, // lu -> Latn
+    {0x828B0000u, 41u}, // lua -> Latn
+    {0xBA8B0000u, 41u}, // luo -> Latn
+    {0xE28B0000u, 41u}, // luy -> Latn
+    {0xE68B0000u,  1u}, // luz -> Arab
+    {0x6C760000u, 41u}, // lv -> Latn
+    {0xAECB0000u, 79u}, // lwl -> Thai
+    {0x9F2B0000u, 24u}, // lzh -> Hans
+    {0xE72B0000u, 41u}, // lzz -> Latn
+    {0x8C0C0000u, 41u}, // mad -> Latn
+    {0x940C0000u, 41u}, // maf -> Latn
+    {0x980C0000u, 16u}, // mag -> Deva
+    {0xA00C0000u, 16u}, // mai -> Deva
+    {0xA80C0000u, 41u}, // mak -> Latn
+    {0xB40C0000u, 41u}, // man -> Latn
+    {0xB40C474Eu, 55u}, // man-GN -> Nkoo
+    {0xC80C0000u, 41u}, // mas -> Latn
+    {0xE40C0000u, 41u}, // maz -> Latn
+    {0x946C0000u, 15u}, // mdf -> Cyrl
+    {0x9C6C0000u, 41u}, // mdh -> Latn
+    {0xC46C0000u, 41u}, // mdr -> Latn
+    {0xB48C0000u, 41u}, // men -> Latn
+    {0xC48C0000u, 41u}, // mer -> Latn
+    {0x80AC0000u,  1u}, // mfa -> Arab
+    {0x90AC0000u, 41u}, // mfe -> Latn
+    {0x6D670000u, 41u}, // mg -> Latn
+    {0x9CCC0000u, 41u}, // mgh -> Latn
+    {0xB8CC0000u, 41u}, // mgo -> Latn
+    {0xBCCC0000u, 16u}, // mgp -> Deva
+    {0xE0CC0000u, 41u}, // mgy -> Latn
+    {0x6D680000u, 41u}, // mh -> Latn
+    {0x6D690000u, 41u}, // mi -> Latn
+    {0xB50C0000u, 41u}, // min -> Latn
+    {0xC90C0000u, 26u}, // mis -> Hatr
+    {0x6D6B0000u, 15u}, // mk -> Cyrl
+    {0x6D6C0000u, 50u}, // ml -> Mlym
+    {0xC96C0000u, 41u}, // mls -> Latn
+    {0x6D6E0000u, 15u}, // mn -> Cyrl
+    {0x6D6E434Eu, 51u}, // mn-CN -> Mong
+    {0xA1AC0000u,  7u}, // mni -> Beng
+    {0xD9AC0000u, 53u}, // mnw -> Mymr
+    {0x91CC0000u, 41u}, // moe -> Latn
+    {0x9DCC0000u, 41u}, // moh -> Latn
+    {0xC9CC0000u, 41u}, // mos -> Latn
+    {0x6D720000u, 16u}, // mr -> Deva
+    {0x8E2C0000u, 16u}, // mrd -> Deva
+    {0xA62C0000u, 15u}, // mrj -> Cyrl
+    {0xD22C0000u, 52u}, // mru -> Mroo
+    {0x6D730000u, 41u}, // ms -> Latn
+    {0x6D734343u,  1u}, // ms-CC -> Arab
+    {0x6D734944u,  1u}, // ms-ID -> Arab
+    {0x6D740000u, 41u}, // mt -> Latn
+    {0xC66C0000u, 16u}, // mtr -> Deva
+    {0x828C0000u, 41u}, // mua -> Latn
+    {0xCA8C0000u, 41u}, // mus -> Latn
+    {0xE2AC0000u,  1u}, // mvy -> Arab
+    {0xAACC0000u, 41u}, // mwk -> Latn
+    {0xC6CC0000u, 16u}, // mwr -> Deva
+    {0xD6CC0000u, 41u}, // mwv -> Latn
+    {0x8AEC0000u, 41u}, // mxc -> Latn
+    {0x6D790000u, 53u}, // my -> Mymr
+    {0xD70C0000u, 15u}, // myv -> Cyrl
+    {0xDF0C0000u, 41u}, // myx -> Latn
+    {0xE70C0000u, 47u}, // myz -> Mand
+    {0xB72C0000u,  1u}, // mzn -> Arab
+    {0x6E610000u, 41u}, // na -> Latn
+    {0xB40D0000u, 24u}, // nan -> Hans
+    {0xBC0D0000u, 41u}, // nap -> Latn
+    {0xC00D0000u, 41u}, // naq -> Latn
+    {0x6E620000u, 41u}, // nb -> Latn
+    {0x9C4D0000u, 41u}, // nch -> Latn
+    {0x6E640000u, 41u}, // nd -> Latn
+    {0x886D0000u, 41u}, // ndc -> Latn
+    {0xC86D0000u, 41u}, // nds -> Latn
+    {0x6E650000u, 16u}, // ne -> Deva
+    {0xD88D0000u, 16u}, // new -> Deva
+    {0x6E670000u, 41u}, // ng -> Latn
+    {0xACCD0000u, 41u}, // ngl -> Latn
+    {0x90ED0000u, 41u}, // nhe -> Latn
+    {0xD8ED0000u, 41u}, // nhw -> Latn
+    {0xA50D0000u, 41u}, // nij -> Latn
+    {0xD10D0000u, 41u}, // niu -> Latn
+    {0xB92D0000u, 41u}, // njo -> Latn
+    {0x6E6C0000u, 41u}, // nl -> Latn
+    {0x998D0000u, 41u}, // nmg -> Latn
+    {0x6E6E0000u, 41u}, // nn -> Latn
+    {0x9DAD0000u, 41u}, // nnh -> Latn
+    {0x6E6F0000u, 41u}, // no -> Latn
+    {0x8DCD0000u, 39u}, // nod -> Lana
+    {0x91CD0000u, 16u}, // noe -> Deva
+    {0xB5CD0000u, 64u}, // non -> Runr
+    {0xBA0D0000u, 55u}, // nqo -> Nkoo
+    {0x6E720000u, 41u}, // nr -> Latn
+    {0xAA4D0000u,  9u}, // nsk -> Cans
+    {0xBA4D0000u, 41u}, // nso -> Latn
+    {0xCA8D0000u, 41u}, // nus -> Latn
+    {0x6E760000u, 41u}, // nv -> Latn
+    {0xC2ED0000u, 41u}, // nxq -> Latn
+    {0x6E790000u, 41u}, // ny -> Latn
+    {0xB30D0000u, 41u}, // nym -> Latn
+    {0xB70D0000u, 41u}, // nyn -> Latn
+    {0xA32D0000u, 41u}, // nzi -> Latn
+    {0x6F630000u, 41u}, // oc -> Latn
+    {0x6F6D0000u, 41u}, // om -> Latn
+    {0x6F720000u, 58u}, // or -> Orya
+    {0x6F730000u, 15u}, // os -> Cyrl
+    {0xAA6E0000u, 57u}, // otk -> Orkh
+    {0x70610000u, 23u}, // pa -> Guru
+    {0x7061504Bu,  1u}, // pa-PK -> Arab
+    {0x980F0000u, 41u}, // pag -> Latn
+    {0xAC0F0000u, 60u}, // pal -> Phli
+    {0xB00F0000u, 41u}, // pam -> Latn
+    {0xBC0F0000u, 41u}, // pap -> Latn
+    {0xD00F0000u, 41u}, // pau -> Latn
+    {0x8C4F0000u, 41u}, // pcd -> Latn
+    {0xB04F0000u, 41u}, // pcm -> Latn
+    {0x886F0000u, 41u}, // pdc -> Latn
+    {0xCC6F0000u, 41u}, // pdt -> Latn
+    {0xB88F0000u, 83u}, // peo -> Xpeo
+    {0xACAF0000u, 41u}, // pfl -> Latn
+    {0xB4EF0000u, 61u}, // phn -> Phnx
+    {0x814F0000u,  8u}, // pka -> Brah
+    {0xB94F0000u, 41u}, // pko -> Latn
+    {0x706C0000u, 41u}, // pl -> Latn
+    {0xC98F0000u, 41u}, // pms -> Latn
+    {0xCDAF0000u, 21u}, // pnt -> Grek
+    {0xB5CF0000u, 41u}, // pon -> Latn
+    {0x822F0000u, 34u}, // pra -> Khar
+    {0x8E2F0000u,  1u}, // prd -> Arab
+    {0x9A2F0000u, 41u}, // prg -> Latn
+    {0x70730000u,  1u}, // ps -> Arab
+    {0x70740000u, 41u}, // pt -> Latn
+    {0xD28F0000u, 41u}, // puu -> Latn
+    {0x71750000u, 41u}, // qu -> Latn
+    {0x8A900000u, 41u}, // quc -> Latn
+    {0x9A900000u, 41u}, // qug -> Latn
+    {0xA4110000u, 16u}, // raj -> Deva
+    {0x94510000u, 41u}, // rcf -> Latn
+    {0xA4910000u, 41u}, // rej -> Latn
+    {0xB4D10000u, 41u}, // rgn -> Latn
+    {0x81110000u, 41u}, // ria -> Latn
+    {0x95110000u, 77u}, // rif -> Tfng
+    {0x95114E4Cu, 41u}, // rif-NL -> Latn
+    {0xC9310000u, 16u}, // rjs -> Deva
+    {0xCD510000u,  7u}, // rkt -> Beng
+    {0x726D0000u, 41u}, // rm -> Latn
+    {0x95910000u, 41u}, // rmf -> Latn
+    {0xB9910000u, 41u}, // rmo -> Latn
+    {0xCD910000u,  1u}, // rmt -> Arab
+    {0xD1910000u, 41u}, // rmu -> Latn
+    {0x726E0000u, 41u}, // rn -> Latn
+    {0x99B10000u, 41u}, // rng -> Latn
+    {0x726F0000u, 41u}, // ro -> Latn
+    {0x85D10000u, 41u}, // rob -> Latn
+    {0x95D10000u, 41u}, // rof -> Latn
+    {0xB2710000u, 41u}, // rtm -> Latn
+    {0x72750000u, 15u}, // ru -> Cyrl
+    {0x92910000u, 15u}, // rue -> Cyrl
+    {0x9A910000u, 41u}, // rug -> Latn
+    {0x72770000u, 41u}, // rw -> Latn
+    {0xAAD10000u, 41u}, // rwk -> Latn
+    {0xD3110000u, 33u}, // ryu -> Kana
+    {0x73610000u, 16u}, // sa -> Deva
+    {0x94120000u, 41u}, // saf -> Latn
+    {0x9C120000u, 15u}, // sah -> Cyrl
+    {0xC0120000u, 41u}, // saq -> Latn
+    {0xC8120000u, 41u}, // sas -> Latn
+    {0xCC120000u, 41u}, // sat -> Latn
+    {0xE4120000u, 67u}, // saz -> Saur
+    {0xBC320000u, 41u}, // sbp -> Latn
+    {0x73630000u, 41u}, // sc -> Latn
+    {0xA8520000u, 16u}, // sck -> Deva
+    {0xB4520000u, 41u}, // scn -> Latn
+    {0xB8520000u, 41u}, // sco -> Latn
+    {0xC8520000u, 41u}, // scs -> Latn
+    {0x73640000u,  1u}, // sd -> Arab
+    {0x88720000u, 41u}, // sdc -> Latn
+    {0x9C720000u,  1u}, // sdh -> Arab
+    {0x73650000u, 41u}, // se -> Latn
+    {0x94920000u, 41u}, // sef -> Latn
+    {0x9C920000u, 41u}, // seh -> Latn
+    {0xA0920000u, 41u}, // sei -> Latn
+    {0xC8920000u, 41u}, // ses -> Latn
+    {0x73670000u, 41u}, // sg -> Latn
+    {0x80D20000u, 56u}, // sga -> Ogam
+    {0xC8D20000u, 41u}, // sgs -> Latn
+    {0x73680000u, 41u}, // sh -> Latn
+    {0xA0F20000u, 77u}, // shi -> Tfng
+    {0xB4F20000u, 53u}, // shn -> Mymr
+    {0x73690000u, 69u}, // si -> Sinh
+    {0x8D120000u, 41u}, // sid -> Latn
+    {0x736B0000u, 41u}, // sk -> Latn
+    {0xC5520000u,  1u}, // skr -> Arab
+    {0x736C0000u, 41u}, // sl -> Latn
+    {0xA1720000u, 41u}, // sli -> Latn
+    {0xE1720000u, 41u}, // sly -> Latn
+    {0x736D0000u, 41u}, // sm -> Latn
+    {0x81920000u, 41u}, // sma -> Latn
+    {0xA5920000u, 41u}, // smj -> Latn
+    {0xB5920000u, 41u}, // smn -> Latn
+    {0xBD920000u, 65u}, // smp -> Samr
+    {0xC9920000u, 41u}, // sms -> Latn
+    {0x736E0000u, 41u}, // sn -> Latn
+    {0xA9B20000u, 41u}, // snk -> Latn
+    {0x736F0000u, 41u}, // so -> Latn
+    {0xD1D20000u, 79u}, // sou -> Thai
+    {0x73710000u, 41u}, // sq -> Latn
+    {0x73720000u, 15u}, // sr -> Cyrl
+    {0x73724D45u, 41u}, // sr-ME -> Latn
+    {0x7372524Fu, 41u}, // sr-RO -> Latn
+    {0x73725255u, 41u}, // sr-RU -> Latn
+    {0x73725452u, 41u}, // sr-TR -> Latn
+    {0x86320000u, 70u}, // srb -> Sora
+    {0xB6320000u, 41u}, // srn -> Latn
+    {0xC6320000u, 41u}, // srr -> Latn
+    {0xDE320000u, 16u}, // srx -> Deva
+    {0x73730000u, 41u}, // ss -> Latn
+    {0xE2520000u, 41u}, // ssy -> Latn
+    {0x73740000u, 41u}, // st -> Latn
+    {0xC2720000u, 41u}, // stq -> Latn
+    {0x73750000u, 41u}, // su -> Latn
+    {0xAA920000u, 41u}, // suk -> Latn
+    {0xCA920000u, 41u}, // sus -> Latn
+    {0x73760000u, 41u}, // sv -> Latn
+    {0x73770000u, 41u}, // sw -> Latn
+    {0x86D20000u,  1u}, // swb -> Arab
+    {0x8AD20000u, 41u}, // swc -> Latn
+    {0x9AD20000u, 41u}, // swg -> Latn
+    {0xD6D20000u, 16u}, // swv -> Deva
+    {0xB6F20000u, 41u}, // sxn -> Latn
+    {0xAF120000u,  7u}, // syl -> Beng
+    {0xC7120000u, 71u}, // syr -> Syrc
+    {0xAF320000u, 41u}, // szl -> Latn
+    {0x74610000u, 74u}, // ta -> Taml
+    {0xA4130000u, 16u}, // taj -> Deva
+    {0xD8330000u, 41u}, // tbw -> Latn
+    {0xE0530000u, 36u}, // tcy -> Knda
+    {0x8C730000u, 72u}, // tdd -> Tale
+    {0x98730000u, 16u}, // tdg -> Deva
+    {0x9C730000u, 16u}, // tdh -> Deva
+    {0x74650000u, 76u}, // te -> Telu
+    {0xB0930000u, 41u}, // tem -> Latn
+    {0xB8930000u, 41u}, // teo -> Latn
+    {0xCC930000u, 41u}, // tet -> Latn
+    {0x74670000u, 15u}, // tg -> Cyrl
+    {0x7467504Bu,  1u}, // tg-PK -> Arab
+    {0x74680000u, 79u}, // th -> Thai
+    {0xACF30000u, 16u}, // thl -> Deva
+    {0xC0F30000u, 16u}, // thq -> Deva
+    {0xC4F30000u, 16u}, // thr -> Deva
+    {0x74690000u, 18u}, // ti -> Ethi
+    {0x99130000u, 18u}, // tig -> Ethi
+    {0xD5130000u, 41u}, // tiv -> Latn
+    {0x746B0000u, 41u}, // tk -> Latn
+    {0xAD530000u, 41u}, // tkl -> Latn
+    {0xC5530000u, 41u}, // tkr -> Latn
+    {0xCD530000u, 16u}, // tkt -> Deva
+    {0x746C0000u, 41u}, // tl -> Latn
+    {0xE1730000u, 41u}, // tly -> Latn
+    {0x9D930000u, 41u}, // tmh -> Latn
+    {0x746E0000u, 41u}, // tn -> Latn
+    {0x746F0000u, 41u}, // to -> Latn
+    {0x99D30000u, 41u}, // tog -> Latn
+    {0xA1F30000u, 41u}, // tpi -> Latn
+    {0x74720000u, 41u}, // tr -> Latn
+    {0xD2330000u, 41u}, // tru -> Latn
+    {0xD6330000u, 41u}, // trv -> Latn
+    {0x74730000u, 41u}, // ts -> Latn
+    {0x8E530000u, 21u}, // tsd -> Grek
+    {0x96530000u, 16u}, // tsf -> Deva
+    {0x9A530000u, 41u}, // tsg -> Latn
+    {0xA6530000u, 80u}, // tsj -> Tibt
+    {0x74740000u, 15u}, // tt -> Cyrl
+    {0xA6730000u, 41u}, // ttj -> Latn
+    {0xCA730000u, 79u}, // tts -> Thai
+    {0xCE730000u, 41u}, // ttt -> Latn
+    {0xB2930000u, 41u}, // tum -> Latn
+    {0xAEB30000u, 41u}, // tvl -> Latn
+    {0xC2D30000u, 41u}, // twq -> Latn
+    {0x74790000u, 41u}, // ty -> Latn
+    {0xD7130000u, 15u}, // tyv -> Cyrl
+    {0xB3330000u, 41u}, // tzm -> Latn
+    {0xB0740000u, 15u}, // udm -> Cyrl
+    {0x75670000u,  1u}, // ug -> Arab
+    {0x75674B5Au, 15u}, // ug-KZ -> Cyrl
+    {0x75674D4Eu, 15u}, // ug-MN -> Cyrl
+    {0x80D40000u, 81u}, // uga -> Ugar
+    {0x756B0000u, 15u}, // uk -> Cyrl
+    {0xA1740000u, 41u}, // uli -> Latn
+    {0x85940000u, 41u}, // umb -> Latn
+    {0xC5B40000u,  7u}, // unr -> Beng
+    {0xC5B44E50u, 16u}, // unr-NP -> Deva
+    {0xDDB40000u,  7u}, // unx -> Beng
+    {0x75720000u,  1u}, // ur -> Arab
+    {0x757A0000u, 41u}, // uz -> Latn
+    {0x757A4146u,  1u}, // uz-AF -> Arab
+    {0x757A434Eu, 15u}, // uz-CN -> Cyrl
+    {0xA0150000u, 82u}, // vai -> Vaii
+    {0x76650000u, 41u}, // ve -> Latn
+    {0x88950000u, 41u}, // vec -> Latn
+    {0xBC950000u, 41u}, // vep -> Latn
+    {0x76690000u, 41u}, // vi -> Latn
+    {0x89150000u, 41u}, // vic -> Latn
+    {0xC9750000u, 41u}, // vls -> Latn
+    {0x95950000u, 41u}, // vmf -> Latn
+    {0xD9950000u, 41u}, // vmw -> Latn
+    {0x766F0000u, 41u}, // vo -> Latn
+    {0xCDD50000u, 41u}, // vot -> Latn
+    {0xBA350000u, 41u}, // vro -> Latn
+    {0xB6950000u, 41u}, // vun -> Latn
+    {0x77610000u, 41u}, // wa -> Latn
+    {0x90160000u, 41u}, // wae -> Latn
+    {0xAC160000u, 18u}, // wal -> Ethi
+    {0xC4160000u, 41u}, // war -> Latn
+    {0xBC360000u, 41u}, // wbp -> Latn
+    {0xC0360000u, 76u}, // wbq -> Telu
+    {0xC4360000u, 16u}, // wbr -> Deva
+    {0xC9760000u, 41u}, // wls -> Latn
+    {0xA1B60000u,  1u}, // wni -> Arab
+    {0x776F0000u, 41u}, // wo -> Latn
+    {0xB2760000u, 16u}, // wtm -> Deva
+    {0xD2960000u, 24u}, // wuu -> Hans
+    {0xD4170000u, 41u}, // xav -> Latn
+    {0xC4570000u, 10u}, // xcr -> Cari
+    {0x78680000u, 41u}, // xh -> Latn
+    {0x89770000u, 45u}, // xlc -> Lyci
+    {0x8D770000u, 46u}, // xld -> Lydi
+    {0x95970000u, 19u}, // xmf -> Geor
+    {0xB5970000u, 48u}, // xmn -> Mani
+    {0xC5970000u, 49u}, // xmr -> Merc
+    {0x81B70000u, 54u}, // xna -> Narb
+    {0xC5B70000u, 16u}, // xnr -> Deva
+    {0x99D70000u, 41u}, // xog -> Latn
+    {0xC5F70000u, 63u}, // xpr -> Prti
+    {0x82570000u, 66u}, // xsa -> Sarb
+    {0xC6570000u, 16u}, // xsr -> Deva
+    {0xB8180000u, 41u}, // yao -> Latn
+    {0xBC180000u, 41u}, // yap -> Latn
+    {0xD4180000u, 41u}, // yav -> Latn
+    {0x84380000u, 41u}, // ybb -> Latn
+    {0x79690000u, 27u}, // yi -> Hebr
+    {0x796F0000u, 41u}, // yo -> Latn
+    {0xAE380000u, 41u}, // yrl -> Latn
+    {0x82980000u, 41u}, // yua -> Latn
+    {0x7A610000u, 41u}, // za -> Latn
+    {0x98190000u, 41u}, // zag -> Latn
+    {0xA4790000u,  1u}, // zdj -> Arab
+    {0x80990000u, 41u}, // zea -> Latn
+    {0x9CD90000u, 77u}, // zgh -> Tfng
+    {0x7A680000u, 24u}, // zh -> Hans
+    {0x7A684155u, 25u}, // zh-AU -> Hant
+    {0x7A68424Eu, 25u}, // zh-BN -> Hant
+    {0x7A684742u, 25u}, // zh-GB -> Hant
+    {0x7A684746u, 25u}, // zh-GF -> Hant
+    {0x7A68484Bu, 25u}, // zh-HK -> Hant
+    {0x7A684944u, 25u}, // zh-ID -> Hant
+    {0x7A684D4Fu, 25u}, // zh-MO -> Hant
+    {0x7A684D59u, 25u}, // zh-MY -> Hant
+    {0x7A685041u, 25u}, // zh-PA -> Hant
+    {0x7A685046u, 25u}, // zh-PF -> Hant
+    {0x7A685048u, 25u}, // zh-PH -> Hant
+    {0x7A685352u, 25u}, // zh-SR -> Hant
+    {0x7A685448u, 25u}, // zh-TH -> Hant
+    {0x7A685457u, 25u}, // zh-TW -> Hant
+    {0x7A685553u, 25u}, // zh-US -> Hant
+    {0x7A68564Eu, 25u}, // zh-VN -> Hant
+    {0xA1990000u, 41u}, // zmi -> Latn
+    {0x7A750000u, 41u}, // zu -> Latn
+    {0x83390000u, 41u}, // zza -> Latn
+});
+
+std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({
+    0x616145544C61746Ellu, // aa_Latn_ET
+    0x616247454379726Cllu, // ab_Cyrl_GE
+    0xC42047484C61746Ellu, // abr_Latn_GH
+    0x904049444C61746Ellu, // ace_Latn_ID
+    0x9C4055474C61746Ellu, // ach_Latn_UG
+    0x806047484C61746Ellu, // ada_Latn_GH
+    0xE06052554379726Cllu, // ady_Cyrl_RU
+    0x6165495241767374llu, // ae_Avst_IR
+    0x8480544E41726162llu, // aeb_Arab_TN
+    0x61665A414C61746Ellu, // af_Latn_ZA
+    0xC0C0434D4C61746Ellu, // agq_Latn_CM
+    0xB8E0494E41686F6Dllu, // aho_Ahom_IN
+    0x616B47484C61746Ellu, // ak_Latn_GH
+    0xA940495158737578llu, // akk_Xsux_IQ
+    0xB560584B4C61746Ellu, // aln_Latn_XK
+    0xCD6052554379726Cllu, // alt_Cyrl_RU
+    0x616D455445746869llu, // am_Ethi_ET
+    0xB9804E474C61746Ellu, // amo_Latn_NG
+    0xE5C049444C61746Ellu, // aoz_Latn_ID
+    0x6172454741726162llu, // ar_Arab_EG
+    0x8A20495241726D69llu, // arc_Armi_IR
+    0x8A204A4F4E626174llu, // arc_Nbat_JO
+    0x8A20535950616C6Dllu, // arc_Palm_SY
+    0xB620434C4C61746Ellu, // arn_Latn_CL
+    0xBA20424F4C61746Ellu, // aro_Latn_BO
+    0xC220445A41726162llu, // arq_Arab_DZ
+    0xE2204D4141726162llu, // ary_Arab_MA
+    0xE620454741726162llu, // arz_Arab_EG
+    0x6173494E42656E67llu, // as_Beng_IN
+    0x8240545A4C61746Ellu, // asa_Latn_TZ
+    0x9240555353676E77llu, // ase_Sgnw_US
+    0xCE4045534C61746Ellu, // ast_Latn_ES
+    0xA66043414C61746Ellu, // atj_Latn_CA
+    0x617652554379726Cllu, // av_Cyrl_RU
+    0x82C0494E44657661llu, // awa_Deva_IN
+    0x6179424F4C61746Ellu, // ay_Latn_BO
+    0x617A495241726162llu, // az_Arab_IR
+    0x617A415A4C61746Ellu, // az_Latn_AZ
+    0x626152554379726Cllu, // ba_Cyrl_RU
+    0xAC01504B41726162llu, // bal_Arab_PK
+    0xB40149444C61746Ellu, // ban_Latn_ID
+    0xBC014E5044657661llu, // bap_Deva_NP
+    0xC40141544C61746Ellu, // bar_Latn_AT
+    0xC801434D4C61746Ellu, // bas_Latn_CM
+    0xDC01434D42616D75llu, // bax_Bamu_CM
+    0x882149444C61746Ellu, // bbc_Latn_ID
+    0xA421434D4C61746Ellu, // bbj_Latn_CM
+    0xA04143494C61746Ellu, // bci_Latn_CI
+    0x626542594379726Cllu, // be_Cyrl_BY
+    0xA481534441726162llu, // bej_Arab_SD
+    0xB0815A4D4C61746Ellu, // bem_Latn_ZM
+    0xD88149444C61746Ellu, // bew_Latn_ID
+    0xE481545A4C61746Ellu, // bez_Latn_TZ
+    0x8CA1434D4C61746Ellu, // bfd_Latn_CM
+    0xC0A1494E54616D6Cllu, // bfq_Taml_IN
+    0xCCA1504B41726162llu, // bft_Arab_PK
+    0xE0A1494E44657661llu, // bfy_Deva_IN
+    0x626742474379726Cllu, // bg_Cyrl_BG
+    0x88C1494E44657661llu, // bgc_Deva_IN
+    0xB4C1504B41726162llu, // bgn_Arab_PK
+    0xDCC154524772656Bllu, // bgx_Grek_TR
+    0x6268494E4B746869llu, // bh_Kthi_IN
+    0x84E1494E44657661llu, // bhb_Deva_IN
+    0xA0E1494E44657661llu, // bhi_Deva_IN
+    0xA8E150484C61746Ellu, // bhk_Latn_PH
+    0xB8E1494E44657661llu, // bho_Deva_IN
+    0x626956554C61746Ellu, // bi_Latn_VU
+    0xA90150484C61746Ellu, // bik_Latn_PH
+    0xB5014E474C61746Ellu, // bin_Latn_NG
+    0xA521494E44657661llu, // bjj_Deva_IN
+    0xB52149444C61746Ellu, // bjn_Latn_ID
+    0xB141434D4C61746Ellu, // bkm_Latn_CM
+    0xD14150484C61746Ellu, // bku_Latn_PH
+    0xCD61564E54617674llu, // blt_Tavt_VN
+    0x626D4D4C4C61746Ellu, // bm_Latn_ML
+    0xC1814D4C4C61746Ellu, // bmq_Latn_ML
+    0x626E424442656E67llu, // bn_Beng_BD
+    0x626F434E54696274llu, // bo_Tibt_CN
+    0xE1E1494E42656E67llu, // bpy_Beng_IN
+    0xA201495241726162llu, // bqi_Arab_IR
+    0xD60143494C61746Ellu, // bqv_Latn_CI
+    0x627246524C61746Ellu, // br_Latn_FR
+    0x8221494E44657661llu, // bra_Deva_IN
+    0x9E21504B41726162llu, // brh_Arab_PK
+    0xDE21494E44657661llu, // brx_Deva_IN
+    0x627342414C61746Ellu, // bs_Latn_BA
+    0xC2414C5242617373llu, // bsq_Bass_LR
+    0xCA41434D4C61746Ellu, // bss_Latn_CM
+    0xBA6150484C61746Ellu, // bto_Latn_PH
+    0xD661504B44657661llu, // btv_Deva_PK
+    0x828152554379726Cllu, // bua_Cyrl_RU
+    0x8A8159544C61746Ellu, // buc_Latn_YT
+    0x9A8149444C61746Ellu, // bug_Latn_ID
+    0xB281434D4C61746Ellu, // bum_Latn_CM
+    0x86A147514C61746Ellu, // bvb_Latn_GQ
+    0xB701455245746869llu, // byn_Ethi_ER
+    0xD701434D4C61746Ellu, // byv_Latn_CM
+    0x93214D4C4C61746Ellu, // bze_Latn_ML
+    0x636145534C61746Ellu, // ca_Latn_ES
+    0x9C424E474C61746Ellu, // cch_Latn_NG
+    0xBC42494E42656E67llu, // ccp_Beng_IN
+    0xBC42424443616B6Dllu, // ccp_Cakm_BD
+    0x636552554379726Cllu, // ce_Cyrl_RU
+    0x848250484C61746Ellu, // ceb_Latn_PH
+    0x98C255474C61746Ellu, // cgg_Latn_UG
+    0x636847554C61746Ellu, // ch_Latn_GU
+    0xA8E2464D4C61746Ellu, // chk_Latn_FM
+    0xB0E252554379726Cllu, // chm_Cyrl_RU
+    0xB8E255534C61746Ellu, // cho_Latn_US
+    0xBCE243414C61746Ellu, // chp_Latn_CA
+    0xC4E2555343686572llu, // chr_Cher_US
+    0x81224B4841726162llu, // cja_Arab_KH
+    0xB122564E4368616Dllu, // cjm_Cham_VN
+    0x8542495141726162llu, // ckb_Arab_IQ
+    0x636F46524C61746Ellu, // co_Latn_FR
+    0xBDC24547436F7074llu, // cop_Copt_EG
+    0xC9E250484C61746Ellu, // cps_Latn_PH
+    0x6372434143616E73llu, // cr_Cans_CA
+    0xA622434143616E73llu, // crj_Cans_CA
+    0xAA22434143616E73llu, // crk_Cans_CA
+    0xAE22434143616E73llu, // crl_Cans_CA
+    0xB222434143616E73llu, // crm_Cans_CA
+    0xCA2253434C61746Ellu, // crs_Latn_SC
+    0x6373435A4C61746Ellu, // cs_Latn_CZ
+    0x8642504C4C61746Ellu, // csb_Latn_PL
+    0xDA42434143616E73llu, // csw_Cans_CA
+    0x8E624D4D50617563llu, // ctd_Pauc_MM
+    0x637552554379726Cllu, // cu_Cyrl_RU
+    0x63754247476C6167llu, // cu_Glag_BG
+    0x637652554379726Cllu, // cv_Cyrl_RU
+    0x637947424C61746Ellu, // cy_Latn_GB
+    0x6461444B4C61746Ellu, // da_Latn_DK
+    0xA80355534C61746Ellu, // dak_Latn_US
+    0xC40352554379726Cllu, // dar_Cyrl_RU
+    0xD4034B454C61746Ellu, // dav_Latn_KE
+    0x8843494E41726162llu, // dcc_Arab_IN
+    0x646544454C61746Ellu, // de_Latn_DE
+    0xB48343414C61746Ellu, // den_Latn_CA
+    0xC4C343414C61746Ellu, // dgr_Latn_CA
+    0x91234E454C61746Ellu, // dje_Latn_NE
+    0xA5A343494C61746Ellu, // dnj_Latn_CI
+    0xA1C3494E41726162llu, // doi_Arab_IN
+    0x864344454C61746Ellu, // dsb_Latn_DE
+    0xB2634D4C4C61746Ellu, // dtm_Latn_ML
+    0xBE634D594C61746Ellu, // dtp_Latn_MY
+    0x8283434D4C61746Ellu, // dua_Latn_CM
+    0x64764D5654686161llu, // dv_Thaa_MV
+    0xBB03534E4C61746Ellu, // dyo_Latn_SN
+    0xD30342464C61746Ellu, // dyu_Latn_BF
+    0x647A425454696274llu, // dz_Tibt_BT
+    0xD0244B454C61746Ellu, // ebu_Latn_KE
+    0x656547484C61746Ellu, // ee_Latn_GH
+    0xA0A44E474C61746Ellu, // efi_Latn_NG
+    0xACC449544C61746Ellu, // egl_Latn_IT
+    0xE0C4454745677970llu, // egy_Egyp_EG
+    0xE1444D4D4B616C69llu, // eky_Kali_MM
+    0x656C47524772656Bllu, // el_Grek_GR
+    0x656E47424C61746Ellu, // en_Latn_GB
+    0x656E55534C61746Ellu, // en_Latn_US
+    0x656E474253686177llu, // en_Shaw_GB
+    0x657345534C61746Ellu, // es_Latn_ES
+    0x65734D584C61746Ellu, // es_Latn_MX
+    0x657355534C61746Ellu, // es_Latn_US
+    0xD24455534C61746Ellu, // esu_Latn_US
+    0x657445454C61746Ellu, // et_Latn_EE
+    0xCE6449544974616Cllu, // ett_Ital_IT
+    0x657545534C61746Ellu, // eu_Latn_ES
+    0xBAC4434D4C61746Ellu, // ewo_Latn_CM
+    0xCEE445534C61746Ellu, // ext_Latn_ES
+    0x6661495241726162llu, // fa_Arab_IR
+    0xB40547514C61746Ellu, // fan_Latn_GQ
+    0x6666534E4C61746Ellu, // ff_Latn_SN
+    0xB0A54D4C4C61746Ellu, // ffm_Latn_ML
+    0x666946494C61746Ellu, // fi_Latn_FI
+    0x8105534441726162llu, // fia_Arab_SD
+    0xAD0550484C61746Ellu, // fil_Latn_PH
+    0xCD0553454C61746Ellu, // fit_Latn_SE
+    0x666A464A4C61746Ellu, // fj_Latn_FJ
+    0x666F464F4C61746Ellu, // fo_Latn_FO
+    0xB5C5424A4C61746Ellu, // fon_Latn_BJ
+    0x667246524C61746Ellu, // fr_Latn_FR
+    0x8A2555534C61746Ellu, // frc_Latn_US
+    0xBE2546524C61746Ellu, // frp_Latn_FR
+    0xC62544454C61746Ellu, // frr_Latn_DE
+    0xCA2544454C61746Ellu, // frs_Latn_DE
+    0x8E8557464C61746Ellu, // fud_Latn_WF
+    0xC2854E454C61746Ellu, // fuq_Latn_NE
+    0xC68549544C61746Ellu, // fur_Latn_IT
+    0xD6854E474C61746Ellu, // fuv_Latn_NG
+    0xC6A553444C61746Ellu, // fvr_Latn_SD
+    0x66794E4C4C61746Ellu, // fy_Latn_NL
+    0x676149454C61746Ellu, // ga_Latn_IE
+    0x800647484C61746Ellu, // gaa_Latn_GH
+    0x98064D444C61746Ellu, // gag_Latn_MD
+    0xB406434E48616E73llu, // gan_Hans_CN
+    0xE00649444C61746Ellu, // gay_Latn_ID
+    0xB026494E44657661llu, // gbm_Deva_IN
+    0xE426495241726162llu, // gbz_Arab_IR
+    0xC44647464C61746Ellu, // gcr_Latn_GF
+    0x676447424C61746Ellu, // gd_Latn_GB
+    0xE486455445746869llu, // gez_Ethi_ET
+    0xB4C64E5044657661llu, // ggn_Deva_NP
+    0xAD064B494C61746Ellu, // gil_Latn_KI
+    0xA926504B41726162llu, // gjk_Arab_PK
+    0xD126504B41726162llu, // gju_Arab_PK
+    0x676C45534C61746Ellu, // gl_Latn_ES
+    0xA966495241726162llu, // glk_Arab_IR
+    0x676E50594C61746Ellu, // gn_Latn_PY
+    0xB1C6494E44657661llu, // gom_Deva_IN
+    0xB5C6494E54656C75llu, // gon_Telu_IN
+    0xC5C649444C61746Ellu, // gor_Latn_ID
+    0xC9C64E4C4C61746Ellu, // gos_Latn_NL
+    0xCDC65541476F7468llu, // got_Goth_UA
+    0x8A26435943707274llu, // grc_Cprt_CY
+    0x8A2647524C696E62llu, // grc_Linb_GR
+    0xCE26494E42656E67llu, // grt_Beng_IN
+    0xDA4643484C61746Ellu, // gsw_Latn_CH
+    0x6775494E47756A72llu, // gu_Gujr_IN
+    0x868642524C61746Ellu, // gub_Latn_BR
+    0x8A86434F4C61746Ellu, // guc_Latn_CO
+    0xC68647484C61746Ellu, // gur_Latn_GH
+    0xE6864B454C61746Ellu, // guz_Latn_KE
+    0x6776494D4C61746Ellu, // gv_Latn_IM
+    0xC6A64E5044657661llu, // gvr_Deva_NP
+    0xA2C643414C61746Ellu, // gwi_Latn_CA
+    0x68614E474C61746Ellu, // ha_Latn_NG
+    0xA807434E48616E73llu, // hak_Hans_CN
+    0xD80755534C61746Ellu, // haw_Latn_US
+    0xE407414641726162llu, // haz_Arab_AF
+    0x6865494C48656272llu, // he_Hebr_IL
+    0x6869494E44657661llu, // hi_Deva_IN
+    0x9507464A4C61746Ellu, // hif_Latn_FJ
+    0xAD0750484C61746Ellu, // hil_Latn_PH
+    0xD1675452486C7577llu, // hlu_Hluw_TR
+    0x8D87434E506C7264llu, // hmd_Plrd_CN
+    0x8DA7504B41726162llu, // hnd_Arab_PK
+    0x91A7494E44657661llu, // hne_Deva_IN
+    0xA5A74C41486D6E67llu, // hnj_Hmng_LA
+    0xB5A750484C61746Ellu, // hnn_Latn_PH
+    0xB9A7504B41726162llu, // hno_Arab_PK
+    0x686F50474C61746Ellu, // ho_Latn_PG
+    0x89C7494E44657661llu, // hoc_Deva_IN
+    0xA5C7494E44657661llu, // hoj_Deva_IN
+    0x687248524C61746Ellu, // hr_Latn_HR
+    0x864744454C61746Ellu, // hsb_Latn_DE
+    0xB647434E48616E73llu, // hsn_Hans_CN
+    0x687448544C61746Ellu, // ht_Latn_HT
+    0x687548554C61746Ellu, // hu_Latn_HU
+    0x6879414D41726D6Ellu, // hy_Armn_AM
+    0x687A4E414C61746Ellu, // hz_Latn_NA
+    0x696146524C61746Ellu, // ia_Latn_FR
+    0x80284D594C61746Ellu, // iba_Latn_MY
+    0x84284E474C61746Ellu, // ibb_Latn_NG
+    0x696449444C61746Ellu, // id_Latn_ID
+    0x69674E474C61746Ellu, // ig_Latn_NG
+    0x6969434E59696969llu, // ii_Yiii_CN
+    0x696B55534C61746Ellu, // ik_Latn_US
+    0xCD4843414C61746Ellu, // ikt_Latn_CA
+    0xB96850484C61746Ellu, // ilo_Latn_PH
+    0x696E49444C61746Ellu, // in_Latn_ID
+    0x9DA852554379726Cllu, // inh_Cyrl_RU
+    0x697349534C61746Ellu, // is_Latn_IS
+    0x697449544C61746Ellu, // it_Latn_IT
+    0x6975434143616E73llu, // iu_Cans_CA
+    0x6977494C48656272llu, // iw_Hebr_IL
+    0x9F2852554C61746Ellu, // izh_Latn_RU
+    0x6A614A504A70616Ellu, // ja_Jpan_JP
+    0xB0094A4D4C61746Ellu, // jam_Latn_JM
+    0xB8C9434D4C61746Ellu, // jgo_Latn_CM
+    0x6A69554148656272llu, // ji_Hebr_UA
+    0x8989545A4C61746Ellu, // jmc_Latn_TZ
+    0xAD894E5044657661llu, // jml_Deva_NP
+    0xCE89444B4C61746Ellu, // jut_Latn_DK
+    0x6A7649444C61746Ellu, // jv_Latn_ID
+    0x6A7749444C61746Ellu, // jw_Latn_ID
+    0x6B61474547656F72llu, // ka_Geor_GE
+    0x800A555A4379726Cllu, // kaa_Cyrl_UZ
+    0x840A445A4C61746Ellu, // kab_Latn_DZ
+    0x880A4D4D4C61746Ellu, // kac_Latn_MM
+    0xA40A4E474C61746Ellu, // kaj_Latn_NG
+    0xB00A4B454C61746Ellu, // kam_Latn_KE
+    0xB80A4D4C4C61746Ellu, // kao_Latn_ML
+    0x8C2A52554379726Cllu, // kbd_Cyrl_RU
+    0x984A4E474C61746Ellu, // kcg_Latn_NG
+    0xA84A5A574C61746Ellu, // kck_Latn_ZW
+    0x906A545A4C61746Ellu, // kde_Latn_TZ
+    0xCC6A544854686169llu, // kdt_Thai_TH
+    0x808A43564C61746Ellu, // kea_Latn_CV
+    0xB48A434D4C61746Ellu, // ken_Latn_CM
+    0xB8AA43494C61746Ellu, // kfo_Latn_CI
+    0xC4AA494E44657661llu, // kfr_Deva_IN
+    0xE0AA494E44657661llu, // kfy_Deva_IN
+    0x6B6743444C61746Ellu, // kg_Latn_CD
+    0x90CA49444C61746Ellu, // kge_Latn_ID
+    0xBCCA42524C61746Ellu, // kgp_Latn_BR
+    0x80EA494E4C61746Ellu, // kha_Latn_IN
+    0x84EA434E54616C75llu, // khb_Talu_CN
+    0xB4EA494E44657661llu, // khn_Deva_IN
+    0xC0EA4D4C4C61746Ellu, // khq_Latn_ML
+    0xCCEA494E4D796D72llu, // kht_Mymr_IN
+    0xD8EA504B41726162llu, // khw_Arab_PK
+    0x6B694B454C61746Ellu, // ki_Latn_KE
+    0xD10A54524C61746Ellu, // kiu_Latn_TR
+    0x6B6A4E414C61746Ellu, // kj_Latn_NA
+    0x992A4C414C616F6Fllu, // kjg_Laoo_LA
+    0x6B6B434E41726162llu, // kk_Arab_CN
+    0x6B6B4B5A4379726Cllu, // kk_Cyrl_KZ
+    0xA54A434D4C61746Ellu, // kkj_Latn_CM
+    0x6B6C474C4C61746Ellu, // kl_Latn_GL
+    0xB56A4B454C61746Ellu, // kln_Latn_KE
+    0x6B6D4B484B686D72llu, // km_Khmr_KH
+    0x858A414F4C61746Ellu, // kmb_Latn_AO
+    0x6B6E494E4B6E6461llu, // kn_Knda_IN
+    0x6B6F4B524B6F7265llu, // ko_Kore_KR
+    0xA1CA52554379726Cllu, // koi_Cyrl_RU
+    0xA9CA494E44657661llu, // kok_Deva_IN
+    0xC9CA464D4C61746Ellu, // kos_Latn_FM
+    0x91EA4C524C61746Ellu, // kpe_Latn_LR
+    0x8A2A52554379726Cllu, // krc_Cyrl_RU
+    0xA22A534C4C61746Ellu, // kri_Latn_SL
+    0xA62A50484C61746Ellu, // krj_Latn_PH
+    0xAE2A52554C61746Ellu, // krl_Latn_RU
+    0xD22A494E44657661llu, // kru_Deva_IN
+    0x6B73494E41726162llu, // ks_Arab_IN
+    0x864A545A4C61746Ellu, // ksb_Latn_TZ
+    0x964A434D4C61746Ellu, // ksf_Latn_CM
+    0x9E4A44454C61746Ellu, // ksh_Latn_DE
+    0x6B75495141726162llu, // ku_Arab_IQ
+    0x6B7554524C61746Ellu, // ku_Latn_TR
+    0xB28A52554379726Cllu, // kum_Cyrl_RU
+    0x6B7652554379726Cllu, // kv_Cyrl_RU
+    0xC6AA49444C61746Ellu, // kvr_Latn_ID
+    0xDEAA504B41726162llu, // kvx_Arab_PK
+    0x6B7747424C61746Ellu, // kw_Latn_GB
+    0xB2EA544854686169llu, // kxm_Thai_TH
+    0xBEEA504B41726162llu, // kxp_Arab_PK
+    0x6B79434E41726162llu, // ky_Arab_CN
+    0x6B794B474379726Cllu, // ky_Cyrl_KG
+    0x6B7954524C61746Ellu, // ky_Latn_TR
+    0x6C6156414C61746Ellu, // la_Latn_VA
+    0x840B47524C696E61llu, // lab_Lina_GR
+    0x8C0B494C48656272llu, // lad_Hebr_IL
+    0x980B545A4C61746Ellu, // lag_Latn_TZ
+    0x9C0B504B41726162llu, // lah_Arab_PK
+    0xA40B55474C61746Ellu, // laj_Latn_UG
+    0x6C624C554C61746Ellu, // lb_Latn_LU
+    0x902B52554379726Cllu, // lbe_Cyrl_RU
+    0xD82B49444C61746Ellu, // lbw_Latn_ID
+    0xBC4B434E54686169llu, // lcp_Thai_CN
+    0xBC8B494E4C657063llu, // lep_Lepc_IN
+    0xE48B52554379726Cllu, // lez_Cyrl_RU
+    0x6C6755474C61746Ellu, // lg_Latn_UG
+    0x6C694E4C4C61746Ellu, // li_Latn_NL
+    0x950B4E5044657661llu, // lif_Deva_NP
+    0x950B494E4C696D62llu, // lif_Limb_IN
+    0xA50B49544C61746Ellu, // lij_Latn_IT
+    0xC90B434E4C697375llu, // lis_Lisu_CN
+    0xBD2B49444C61746Ellu, // ljp_Latn_ID
+    0xA14B495241726162llu, // lki_Arab_IR
+    0xCD4B55534C61746Ellu, // lkt_Latn_US
+    0xB58B494E54656C75llu, // lmn_Telu_IN
+    0xB98B49544C61746Ellu, // lmo_Latn_IT
+    0x6C6E43444C61746Ellu, // ln_Latn_CD
+    0x6C6F4C414C616F6Fllu, // lo_Laoo_LA
+    0xADCB43444C61746Ellu, // lol_Latn_CD
+    0xE5CB5A4D4C61746Ellu, // loz_Latn_ZM
+    0x8A2B495241726162llu, // lrc_Arab_IR
+    0x6C744C544C61746Ellu, // lt_Latn_LT
+    0x9A6B4C564C61746Ellu, // ltg_Latn_LV
+    0x6C7543444C61746Ellu, // lu_Latn_CD
+    0x828B43444C61746Ellu, // lua_Latn_CD
+    0xBA8B4B454C61746Ellu, // luo_Latn_KE
+    0xE28B4B454C61746Ellu, // luy_Latn_KE
+    0xE68B495241726162llu, // luz_Arab_IR
+    0x6C764C564C61746Ellu, // lv_Latn_LV
+    0xAECB544854686169llu, // lwl_Thai_TH
+    0x9F2B434E48616E73llu, // lzh_Hans_CN
+    0xE72B54524C61746Ellu, // lzz_Latn_TR
+    0x8C0C49444C61746Ellu, // mad_Latn_ID
+    0x940C434D4C61746Ellu, // maf_Latn_CM
+    0x980C494E44657661llu, // mag_Deva_IN
+    0xA00C494E44657661llu, // mai_Deva_IN
+    0xA80C49444C61746Ellu, // mak_Latn_ID
+    0xB40C474D4C61746Ellu, // man_Latn_GM
+    0xB40C474E4E6B6F6Fllu, // man_Nkoo_GN
+    0xC80C4B454C61746Ellu, // mas_Latn_KE
+    0xE40C4D584C61746Ellu, // maz_Latn_MX
+    0x946C52554379726Cllu, // mdf_Cyrl_RU
+    0x9C6C50484C61746Ellu, // mdh_Latn_PH
+    0xC46C49444C61746Ellu, // mdr_Latn_ID
+    0xB48C534C4C61746Ellu, // men_Latn_SL
+    0xC48C4B454C61746Ellu, // mer_Latn_KE
+    0x80AC544841726162llu, // mfa_Arab_TH
+    0x90AC4D554C61746Ellu, // mfe_Latn_MU
+    0x6D674D474C61746Ellu, // mg_Latn_MG
+    0x9CCC4D5A4C61746Ellu, // mgh_Latn_MZ
+    0xB8CC434D4C61746Ellu, // mgo_Latn_CM
+    0xBCCC4E5044657661llu, // mgp_Deva_NP
+    0xE0CC545A4C61746Ellu, // mgy_Latn_TZ
+    0x6D684D484C61746Ellu, // mh_Latn_MH
+    0x6D694E5A4C61746Ellu, // mi_Latn_NZ
+    0xB50C49444C61746Ellu, // min_Latn_ID
+    0xC90C495148617472llu, // mis_Hatr_IQ
+    0x6D6B4D4B4379726Cllu, // mk_Cyrl_MK
+    0x6D6C494E4D6C796Dllu, // ml_Mlym_IN
+    0xC96C53444C61746Ellu, // mls_Latn_SD
+    0x6D6E4D4E4379726Cllu, // mn_Cyrl_MN
+    0x6D6E434E4D6F6E67llu, // mn_Mong_CN
+    0xA1AC494E42656E67llu, // mni_Beng_IN
+    0xD9AC4D4D4D796D72llu, // mnw_Mymr_MM
+    0x91CC43414C61746Ellu, // moe_Latn_CA
+    0x9DCC43414C61746Ellu, // moh_Latn_CA
+    0xC9CC42464C61746Ellu, // mos_Latn_BF
+    0x6D72494E44657661llu, // mr_Deva_IN
+    0x8E2C4E5044657661llu, // mrd_Deva_NP
+    0xA62C52554379726Cllu, // mrj_Cyrl_RU
+    0xD22C42444D726F6Fllu, // mru_Mroo_BD
+    0x6D734D594C61746Ellu, // ms_Latn_MY
+    0x6D744D544C61746Ellu, // mt_Latn_MT
+    0xC66C494E44657661llu, // mtr_Deva_IN
+    0x828C434D4C61746Ellu, // mua_Latn_CM
+    0xCA8C55534C61746Ellu, // mus_Latn_US
+    0xE2AC504B41726162llu, // mvy_Arab_PK
+    0xAACC4D4C4C61746Ellu, // mwk_Latn_ML
+    0xC6CC494E44657661llu, // mwr_Deva_IN
+    0xD6CC49444C61746Ellu, // mwv_Latn_ID
+    0x8AEC5A574C61746Ellu, // mxc_Latn_ZW
+    0x6D794D4D4D796D72llu, // my_Mymr_MM
+    0xD70C52554379726Cllu, // myv_Cyrl_RU
+    0xDF0C55474C61746Ellu, // myx_Latn_UG
+    0xE70C49524D616E64llu, // myz_Mand_IR
+    0xB72C495241726162llu, // mzn_Arab_IR
+    0x6E614E524C61746Ellu, // na_Latn_NR
+    0xB40D434E48616E73llu, // nan_Hans_CN
+    0xBC0D49544C61746Ellu, // nap_Latn_IT
+    0xC00D4E414C61746Ellu, // naq_Latn_NA
+    0x6E624E4F4C61746Ellu, // nb_Latn_NO
+    0x9C4D4D584C61746Ellu, // nch_Latn_MX
+    0x6E645A574C61746Ellu, // nd_Latn_ZW
+    0x886D4D5A4C61746Ellu, // ndc_Latn_MZ
+    0xC86D44454C61746Ellu, // nds_Latn_DE
+    0x6E654E5044657661llu, // ne_Deva_NP
+    0xD88D4E5044657661llu, // new_Deva_NP
+    0x6E674E414C61746Ellu, // ng_Latn_NA
+    0xACCD4D5A4C61746Ellu, // ngl_Latn_MZ
+    0x90ED4D584C61746Ellu, // nhe_Latn_MX
+    0xD8ED4D584C61746Ellu, // nhw_Latn_MX
+    0xA50D49444C61746Ellu, // nij_Latn_ID
+    0xD10D4E554C61746Ellu, // niu_Latn_NU
+    0xB92D494E4C61746Ellu, // njo_Latn_IN
+    0x6E6C4E4C4C61746Ellu, // nl_Latn_NL
+    0x998D434D4C61746Ellu, // nmg_Latn_CM
+    0x6E6E4E4F4C61746Ellu, // nn_Latn_NO
+    0x9DAD434D4C61746Ellu, // nnh_Latn_CM
+    0x6E6F4E4F4C61746Ellu, // no_Latn_NO
+    0x8DCD54484C616E61llu, // nod_Lana_TH
+    0x91CD494E44657661llu, // noe_Deva_IN
+    0xB5CD534552756E72llu, // non_Runr_SE
+    0xBA0D474E4E6B6F6Fllu, // nqo_Nkoo_GN
+    0x6E725A414C61746Ellu, // nr_Latn_ZA
+    0xAA4D434143616E73llu, // nsk_Cans_CA
+    0xBA4D5A414C61746Ellu, // nso_Latn_ZA
+    0xCA8D53534C61746Ellu, // nus_Latn_SS
+    0x6E7655534C61746Ellu, // nv_Latn_US
+    0xC2ED434E4C61746Ellu, // nxq_Latn_CN
+    0x6E794D574C61746Ellu, // ny_Latn_MW
+    0xB30D545A4C61746Ellu, // nym_Latn_TZ
+    0xB70D55474C61746Ellu, // nyn_Latn_UG
+    0xA32D47484C61746Ellu, // nzi_Latn_GH
+    0x6F6346524C61746Ellu, // oc_Latn_FR
+    0x6F6D45544C61746Ellu, // om_Latn_ET
+    0x6F72494E4F727961llu, // or_Orya_IN
+    0x6F7347454379726Cllu, // os_Cyrl_GE
+    0xAA6E4D4E4F726B68llu, // otk_Orkh_MN
+    0x7061504B41726162llu, // pa_Arab_PK
+    0x7061494E47757275llu, // pa_Guru_IN
+    0x980F50484C61746Ellu, // pag_Latn_PH
+    0xAC0F495250686C69llu, // pal_Phli_IR
+    0xAC0F434E50686C70llu, // pal_Phlp_CN
+    0xB00F50484C61746Ellu, // pam_Latn_PH
+    0xBC0F41574C61746Ellu, // pap_Latn_AW
+    0xD00F50574C61746Ellu, // pau_Latn_PW
+    0x8C4F46524C61746Ellu, // pcd_Latn_FR
+    0xB04F4E474C61746Ellu, // pcm_Latn_NG
+    0x886F55534C61746Ellu, // pdc_Latn_US
+    0xCC6F43414C61746Ellu, // pdt_Latn_CA
+    0xB88F49525870656Fllu, // peo_Xpeo_IR
+    0xACAF44454C61746Ellu, // pfl_Latn_DE
+    0xB4EF4C4250686E78llu, // phn_Phnx_LB
+    0x814F494E42726168llu, // pka_Brah_IN
+    0xB94F4B454C61746Ellu, // pko_Latn_KE
+    0x706C504C4C61746Ellu, // pl_Latn_PL
+    0xC98F49544C61746Ellu, // pms_Latn_IT
+    0xCDAF47524772656Bllu, // pnt_Grek_GR
+    0xB5CF464D4C61746Ellu, // pon_Latn_FM
+    0x822F504B4B686172llu, // pra_Khar_PK
+    0x8E2F495241726162llu, // prd_Arab_IR
+    0x7073414641726162llu, // ps_Arab_AF
+    0x707442524C61746Ellu, // pt_Latn_BR
+    0xD28F47414C61746Ellu, // puu_Latn_GA
+    0x717550454C61746Ellu, // qu_Latn_PE
+    0x8A9047544C61746Ellu, // quc_Latn_GT
+    0x9A9045434C61746Ellu, // qug_Latn_EC
+    0xA411494E44657661llu, // raj_Deva_IN
+    0x945152454C61746Ellu, // rcf_Latn_RE
+    0xA49149444C61746Ellu, // rej_Latn_ID
+    0xB4D149544C61746Ellu, // rgn_Latn_IT
+    0x8111494E4C61746Ellu, // ria_Latn_IN
+    0x95114D4154666E67llu, // rif_Tfng_MA
+    0xC9314E5044657661llu, // rjs_Deva_NP
+    0xCD51424442656E67llu, // rkt_Beng_BD
+    0x726D43484C61746Ellu, // rm_Latn_CH
+    0x959146494C61746Ellu, // rmf_Latn_FI
+    0xB99143484C61746Ellu, // rmo_Latn_CH
+    0xCD91495241726162llu, // rmt_Arab_IR
+    0xD19153454C61746Ellu, // rmu_Latn_SE
+    0x726E42494C61746Ellu, // rn_Latn_BI
+    0x99B14D5A4C61746Ellu, // rng_Latn_MZ
+    0x726F524F4C61746Ellu, // ro_Latn_RO
+    0x85D149444C61746Ellu, // rob_Latn_ID
+    0x95D1545A4C61746Ellu, // rof_Latn_TZ
+    0xB271464A4C61746Ellu, // rtm_Latn_FJ
+    0x727552554379726Cllu, // ru_Cyrl_RU
+    0x929155414379726Cllu, // rue_Cyrl_UA
+    0x9A9153424C61746Ellu, // rug_Latn_SB
+    0x727752574C61746Ellu, // rw_Latn_RW
+    0xAAD1545A4C61746Ellu, // rwk_Latn_TZ
+    0xD3114A504B616E61llu, // ryu_Kana_JP
+    0x7361494E44657661llu, // sa_Deva_IN
+    0x941247484C61746Ellu, // saf_Latn_GH
+    0x9C1252554379726Cllu, // sah_Cyrl_RU
+    0xC0124B454C61746Ellu, // saq_Latn_KE
+    0xC81249444C61746Ellu, // sas_Latn_ID
+    0xCC12494E4C61746Ellu, // sat_Latn_IN
+    0xE412494E53617572llu, // saz_Saur_IN
+    0xBC32545A4C61746Ellu, // sbp_Latn_TZ
+    0x736349544C61746Ellu, // sc_Latn_IT
+    0xA852494E44657661llu, // sck_Deva_IN
+    0xB45249544C61746Ellu, // scn_Latn_IT
+    0xB85247424C61746Ellu, // sco_Latn_GB
+    0xC85243414C61746Ellu, // scs_Latn_CA
+    0x7364504B41726162llu, // sd_Arab_PK
+    0x7364494E44657661llu, // sd_Deva_IN
+    0x7364494E4B686F6Allu, // sd_Khoj_IN
+    0x7364494E53696E64llu, // sd_Sind_IN
+    0x887249544C61746Ellu, // sdc_Latn_IT
+    0x9C72495241726162llu, // sdh_Arab_IR
+    0x73654E4F4C61746Ellu, // se_Latn_NO
+    0x949243494C61746Ellu, // sef_Latn_CI
+    0x9C924D5A4C61746Ellu, // seh_Latn_MZ
+    0xA0924D584C61746Ellu, // sei_Latn_MX
+    0xC8924D4C4C61746Ellu, // ses_Latn_ML
+    0x736743464C61746Ellu, // sg_Latn_CF
+    0x80D249454F67616Dllu, // sga_Ogam_IE
+    0xC8D24C544C61746Ellu, // sgs_Latn_LT
+    0xA0F24D4154666E67llu, // shi_Tfng_MA
+    0xB4F24D4D4D796D72llu, // shn_Mymr_MM
+    0x73694C4B53696E68llu, // si_Sinh_LK
+    0x8D1245544C61746Ellu, // sid_Latn_ET
+    0x736B534B4C61746Ellu, // sk_Latn_SK
+    0xC552504B41726162llu, // skr_Arab_PK
+    0x736C53494C61746Ellu, // sl_Latn_SI
+    0xA172504C4C61746Ellu, // sli_Latn_PL
+    0xE17249444C61746Ellu, // sly_Latn_ID
+    0x736D57534C61746Ellu, // sm_Latn_WS
+    0x819253454C61746Ellu, // sma_Latn_SE
+    0xA59253454C61746Ellu, // smj_Latn_SE
+    0xB59246494C61746Ellu, // smn_Latn_FI
+    0xBD92494C53616D72llu, // smp_Samr_IL
+    0xC99246494C61746Ellu, // sms_Latn_FI
+    0x736E5A574C61746Ellu, // sn_Latn_ZW
+    0xA9B24D4C4C61746Ellu, // snk_Latn_ML
+    0x736F534F4C61746Ellu, // so_Latn_SO
+    0xD1D2544854686169llu, // sou_Thai_TH
+    0x7371414C4C61746Ellu, // sq_Latn_AL
+    0x737252534379726Cllu, // sr_Cyrl_RS
+    0x737252534C61746Ellu, // sr_Latn_RS
+    0x8632494E536F7261llu, // srb_Sora_IN
+    0xB63253524C61746Ellu, // srn_Latn_SR
+    0xC632534E4C61746Ellu, // srr_Latn_SN
+    0xDE32494E44657661llu, // srx_Deva_IN
+    0x73735A414C61746Ellu, // ss_Latn_ZA
+    0xE25245524C61746Ellu, // ssy_Latn_ER
+    0x73745A414C61746Ellu, // st_Latn_ZA
+    0xC27244454C61746Ellu, // stq_Latn_DE
+    0x737549444C61746Ellu, // su_Latn_ID
+    0xAA92545A4C61746Ellu, // suk_Latn_TZ
+    0xCA92474E4C61746Ellu, // sus_Latn_GN
+    0x737653454C61746Ellu, // sv_Latn_SE
+    0x7377545A4C61746Ellu, // sw_Latn_TZ
+    0x86D2595441726162llu, // swb_Arab_YT
+    0x8AD243444C61746Ellu, // swc_Latn_CD
+    0x9AD244454C61746Ellu, // swg_Latn_DE
+    0xD6D2494E44657661llu, // swv_Deva_IN
+    0xB6F249444C61746Ellu, // sxn_Latn_ID
+    0xAF12424442656E67llu, // syl_Beng_BD
+    0xC712495153797263llu, // syr_Syrc_IQ
+    0xAF32504C4C61746Ellu, // szl_Latn_PL
+    0x7461494E54616D6Cllu, // ta_Taml_IN
+    0xA4134E5044657661llu, // taj_Deva_NP
+    0xD83350484C61746Ellu, // tbw_Latn_PH
+    0xE053494E4B6E6461llu, // tcy_Knda_IN
+    0x8C73434E54616C65llu, // tdd_Tale_CN
+    0x98734E5044657661llu, // tdg_Deva_NP
+    0x9C734E5044657661llu, // tdh_Deva_NP
+    0x7465494E54656C75llu, // te_Telu_IN
+    0xB093534C4C61746Ellu, // tem_Latn_SL
+    0xB89355474C61746Ellu, // teo_Latn_UG
+    0xCC93544C4C61746Ellu, // tet_Latn_TL
+    0x7467504B41726162llu, // tg_Arab_PK
+    0x7467544A4379726Cllu, // tg_Cyrl_TJ
+    0x7468544854686169llu, // th_Thai_TH
+    0xACF34E5044657661llu, // thl_Deva_NP
+    0xC0F34E5044657661llu, // thq_Deva_NP
+    0xC4F34E5044657661llu, // thr_Deva_NP
+    0x7469455445746869llu, // ti_Ethi_ET
+    0x9913455245746869llu, // tig_Ethi_ER
+    0xD5134E474C61746Ellu, // tiv_Latn_NG
+    0x746B544D4C61746Ellu, // tk_Latn_TM
+    0xAD53544B4C61746Ellu, // tkl_Latn_TK
+    0xC553415A4C61746Ellu, // tkr_Latn_AZ
+    0xCD534E5044657661llu, // tkt_Deva_NP
+    0x746C50484C61746Ellu, // tl_Latn_PH
+    0xE173415A4C61746Ellu, // tly_Latn_AZ
+    0x9D934E454C61746Ellu, // tmh_Latn_NE
+    0x746E5A414C61746Ellu, // tn_Latn_ZA
+    0x746F544F4C61746Ellu, // to_Latn_TO
+    0x99D34D574C61746Ellu, // tog_Latn_MW
+    0xA1F350474C61746Ellu, // tpi_Latn_PG
+    0x747254524C61746Ellu, // tr_Latn_TR
+    0xD23354524C61746Ellu, // tru_Latn_TR
+    0xD63354574C61746Ellu, // trv_Latn_TW
+    0x74735A414C61746Ellu, // ts_Latn_ZA
+    0x8E5347524772656Bllu, // tsd_Grek_GR
+    0x96534E5044657661llu, // tsf_Deva_NP
+    0x9A5350484C61746Ellu, // tsg_Latn_PH
+    0xA653425454696274llu, // tsj_Tibt_BT
+    0x747452554379726Cllu, // tt_Cyrl_RU
+    0xA67355474C61746Ellu, // ttj_Latn_UG
+    0xCA73544854686169llu, // tts_Thai_TH
+    0xCE73415A4C61746Ellu, // ttt_Latn_AZ
+    0xB2934D574C61746Ellu, // tum_Latn_MW
+    0xAEB354564C61746Ellu, // tvl_Latn_TV
+    0xC2D34E454C61746Ellu, // twq_Latn_NE
+    0x747950464C61746Ellu, // ty_Latn_PF
+    0xD71352554379726Cllu, // tyv_Cyrl_RU
+    0xB3334D414C61746Ellu, // tzm_Latn_MA
+    0xB07452554379726Cllu, // udm_Cyrl_RU
+    0x7567434E41726162llu, // ug_Arab_CN
+    0x75674B5A4379726Cllu, // ug_Cyrl_KZ
+    0x80D4535955676172llu, // uga_Ugar_SY
+    0x756B55414379726Cllu, // uk_Cyrl_UA
+    0xA174464D4C61746Ellu, // uli_Latn_FM
+    0x8594414F4C61746Ellu, // umb_Latn_AO
+    0xC5B4494E42656E67llu, // unr_Beng_IN
+    0xC5B44E5044657661llu, // unr_Deva_NP
+    0xDDB4494E42656E67llu, // unx_Beng_IN
+    0x7572504B41726162llu, // ur_Arab_PK
+    0x757A414641726162llu, // uz_Arab_AF
+    0x757A555A4C61746Ellu, // uz_Latn_UZ
+    0xA0154C5256616969llu, // vai_Vaii_LR
+    0x76655A414C61746Ellu, // ve_Latn_ZA
+    0x889549544C61746Ellu, // vec_Latn_IT
+    0xBC9552554C61746Ellu, // vep_Latn_RU
+    0x7669564E4C61746Ellu, // vi_Latn_VN
+    0x891553584C61746Ellu, // vic_Latn_SX
+    0xC97542454C61746Ellu, // vls_Latn_BE
+    0x959544454C61746Ellu, // vmf_Latn_DE
+    0xD9954D5A4C61746Ellu, // vmw_Latn_MZ
+    0xCDD552554C61746Ellu, // vot_Latn_RU
+    0xBA3545454C61746Ellu, // vro_Latn_EE
+    0xB695545A4C61746Ellu, // vun_Latn_TZ
+    0x776142454C61746Ellu, // wa_Latn_BE
+    0x901643484C61746Ellu, // wae_Latn_CH
+    0xAC16455445746869llu, // wal_Ethi_ET
+    0xC41650484C61746Ellu, // war_Latn_PH
+    0xBC3641554C61746Ellu, // wbp_Latn_AU
+    0xC036494E54656C75llu, // wbq_Telu_IN
+    0xC436494E44657661llu, // wbr_Deva_IN
+    0xC97657464C61746Ellu, // wls_Latn_WF
+    0xA1B64B4D41726162llu, // wni_Arab_KM
+    0x776F534E4C61746Ellu, // wo_Latn_SN
+    0xB276494E44657661llu, // wtm_Deva_IN
+    0xD296434E48616E73llu, // wuu_Hans_CN
+    0xD41742524C61746Ellu, // xav_Latn_BR
+    0xC457545243617269llu, // xcr_Cari_TR
+    0x78685A414C61746Ellu, // xh_Latn_ZA
+    0x897754524C796369llu, // xlc_Lyci_TR
+    0x8D7754524C796469llu, // xld_Lydi_TR
+    0x9597474547656F72llu, // xmf_Geor_GE
+    0xB597434E4D616E69llu, // xmn_Mani_CN
+    0xC59753444D657263llu, // xmr_Merc_SD
+    0x81B753414E617262llu, // xna_Narb_SA
+    0xC5B7494E44657661llu, // xnr_Deva_IN
+    0x99D755474C61746Ellu, // xog_Latn_UG
+    0xC5F7495250727469llu, // xpr_Prti_IR
+    0x8257594553617262llu, // xsa_Sarb_YE
+    0xC6574E5044657661llu, // xsr_Deva_NP
+    0xB8184D5A4C61746Ellu, // yao_Latn_MZ
+    0xBC18464D4C61746Ellu, // yap_Latn_FM
+    0xD418434D4C61746Ellu, // yav_Latn_CM
+    0x8438434D4C61746Ellu, // ybb_Latn_CM
+    0x796F4E474C61746Ellu, // yo_Latn_NG
+    0xAE3842524C61746Ellu, // yrl_Latn_BR
+    0x82984D584C61746Ellu, // yua_Latn_MX
+    0x7A61434E4C61746Ellu, // za_Latn_CN
+    0x981953444C61746Ellu, // zag_Latn_SD
+    0xA4794B4D41726162llu, // zdj_Arab_KM
+    0x80994E4C4C61746Ellu, // zea_Latn_NL
+    0x9CD94D4154666E67llu, // zgh_Tfng_MA
+    0x7A685457426F706Fllu, // zh_Bopo_TW
+    0x7A68434E48616E73llu, // zh_Hans_CN
+    0x7A68545748616E74llu, // zh_Hant_TW
+    0xA1994D594C61746Ellu, // zmi_Latn_MY
+    0x7A755A414C61746Ellu, // zu_Latn_ZA
+    0x833954524C61746Ellu, // zza_Latn_TR
+});
+
+const std::unordered_map<uint32_t, uint32_t> ARAB_PARENTS({
+    {0x6172445Au, 0x61729420u}, // ar-DZ -> ar-015
+    {0x61724548u, 0x61729420u}, // ar-EH -> ar-015
+    {0x61724C59u, 0x61729420u}, // ar-LY -> ar-015
+    {0x61724D41u, 0x61729420u}, // ar-MA -> ar-015
+    {0x6172544Eu, 0x61729420u}, // ar-TN -> ar-015
+});
+
+const std::unordered_map<uint32_t, uint32_t> HANT_PARENTS({
+    {0x7A684D4Fu, 0x7A68484Bu}, // zh-Hant-MO -> zh-Hant-HK
+});
+
+const std::unordered_map<uint32_t, uint32_t> LATN_PARENTS({
+    {0x656E80A1u, 0x656E8400u}, // en-150 -> en-001
+    {0x656E4147u, 0x656E8400u}, // en-AG -> en-001
+    {0x656E4149u, 0x656E8400u}, // en-AI -> en-001
+    {0x656E4154u, 0x656E80A1u}, // en-AT -> en-150
+    {0x656E4155u, 0x656E8400u}, // en-AU -> en-001
+    {0x656E4242u, 0x656E8400u}, // en-BB -> en-001
+    {0x656E4245u, 0x656E8400u}, // en-BE -> en-001
+    {0x656E424Du, 0x656E8400u}, // en-BM -> en-001
+    {0x656E4253u, 0x656E8400u}, // en-BS -> en-001
+    {0x656E4257u, 0x656E8400u}, // en-BW -> en-001
+    {0x656E425Au, 0x656E8400u}, // en-BZ -> en-001
+    {0x656E4341u, 0x656E8400u}, // en-CA -> en-001
+    {0x656E4343u, 0x656E8400u}, // en-CC -> en-001
+    {0x656E4348u, 0x656E80A1u}, // en-CH -> en-150
+    {0x656E434Bu, 0x656E8400u}, // en-CK -> en-001
+    {0x656E434Du, 0x656E8400u}, // en-CM -> en-001
+    {0x656E4358u, 0x656E8400u}, // en-CX -> en-001
+    {0x656E4359u, 0x656E8400u}, // en-CY -> en-001
+    {0x656E4445u, 0x656E80A1u}, // en-DE -> en-150
+    {0x656E4447u, 0x656E8400u}, // en-DG -> en-001
+    {0x656E444Bu, 0x656E80A1u}, // en-DK -> en-150
+    {0x656E444Du, 0x656E8400u}, // en-DM -> en-001
+    {0x656E4552u, 0x656E8400u}, // en-ER -> en-001
+    {0x656E4649u, 0x656E80A1u}, // en-FI -> en-150
+    {0x656E464Au, 0x656E8400u}, // en-FJ -> en-001
+    {0x656E464Bu, 0x656E8400u}, // en-FK -> en-001
+    {0x656E464Du, 0x656E8400u}, // en-FM -> en-001
+    {0x656E4742u, 0x656E8400u}, // en-GB -> en-001
+    {0x656E4744u, 0x656E8400u}, // en-GD -> en-001
+    {0x656E4747u, 0x656E8400u}, // en-GG -> en-001
+    {0x656E4748u, 0x656E8400u}, // en-GH -> en-001
+    {0x656E4749u, 0x656E8400u}, // en-GI -> en-001
+    {0x656E474Du, 0x656E8400u}, // en-GM -> en-001
+    {0x656E4759u, 0x656E8400u}, // en-GY -> en-001
+    {0x656E484Bu, 0x656E8400u}, // en-HK -> en-001
+    {0x656E4945u, 0x656E8400u}, // en-IE -> en-001
+    {0x656E494Cu, 0x656E8400u}, // en-IL -> en-001
+    {0x656E494Du, 0x656E8400u}, // en-IM -> en-001
+    {0x656E494Eu, 0x656E8400u}, // en-IN -> en-001
+    {0x656E494Fu, 0x656E8400u}, // en-IO -> en-001
+    {0x656E4A45u, 0x656E8400u}, // en-JE -> en-001
+    {0x656E4A4Du, 0x656E8400u}, // en-JM -> en-001
+    {0x656E4B45u, 0x656E8400u}, // en-KE -> en-001
+    {0x656E4B49u, 0x656E8400u}, // en-KI -> en-001
+    {0x656E4B4Eu, 0x656E8400u}, // en-KN -> en-001
+    {0x656E4B59u, 0x656E8400u}, // en-KY -> en-001
+    {0x656E4C43u, 0x656E8400u}, // en-LC -> en-001
+    {0x656E4C52u, 0x656E8400u}, // en-LR -> en-001
+    {0x656E4C53u, 0x656E8400u}, // en-LS -> en-001
+    {0x656E4D47u, 0x656E8400u}, // en-MG -> en-001
+    {0x656E4D4Fu, 0x656E8400u}, // en-MO -> en-001
+    {0x656E4D53u, 0x656E8400u}, // en-MS -> en-001
+    {0x656E4D54u, 0x656E8400u}, // en-MT -> en-001
+    {0x656E4D55u, 0x656E8400u}, // en-MU -> en-001
+    {0x656E4D57u, 0x656E8400u}, // en-MW -> en-001
+    {0x656E4D59u, 0x656E8400u}, // en-MY -> en-001
+    {0x656E4E41u, 0x656E8400u}, // en-NA -> en-001
+    {0x656E4E46u, 0x656E8400u}, // en-NF -> en-001
+    {0x656E4E47u, 0x656E8400u}, // en-NG -> en-001
+    {0x656E4E4Cu, 0x656E80A1u}, // en-NL -> en-150
+    {0x656E4E52u, 0x656E8400u}, // en-NR -> en-001
+    {0x656E4E55u, 0x656E8400u}, // en-NU -> en-001
+    {0x656E4E5Au, 0x656E8400u}, // en-NZ -> en-001
+    {0x656E5047u, 0x656E8400u}, // en-PG -> en-001
+    {0x656E5048u, 0x656E8400u}, // en-PH -> en-001
+    {0x656E504Bu, 0x656E8400u}, // en-PK -> en-001
+    {0x656E504Eu, 0x656E8400u}, // en-PN -> en-001
+    {0x656E5057u, 0x656E8400u}, // en-PW -> en-001
+    {0x656E5257u, 0x656E8400u}, // en-RW -> en-001
+    {0x656E5342u, 0x656E8400u}, // en-SB -> en-001
+    {0x656E5343u, 0x656E8400u}, // en-SC -> en-001
+    {0x656E5344u, 0x656E8400u}, // en-SD -> en-001
+    {0x656E5345u, 0x656E80A1u}, // en-SE -> en-150
+    {0x656E5347u, 0x656E8400u}, // en-SG -> en-001
+    {0x656E5348u, 0x656E8400u}, // en-SH -> en-001
+    {0x656E5349u, 0x656E80A1u}, // en-SI -> en-150
+    {0x656E534Cu, 0x656E8400u}, // en-SL -> en-001
+    {0x656E5353u, 0x656E8400u}, // en-SS -> en-001
+    {0x656E5358u, 0x656E8400u}, // en-SX -> en-001
+    {0x656E535Au, 0x656E8400u}, // en-SZ -> en-001
+    {0x656E5443u, 0x656E8400u}, // en-TC -> en-001
+    {0x656E544Bu, 0x656E8400u}, // en-TK -> en-001
+    {0x656E544Fu, 0x656E8400u}, // en-TO -> en-001
+    {0x656E5454u, 0x656E8400u}, // en-TT -> en-001
+    {0x656E5456u, 0x656E8400u}, // en-TV -> en-001
+    {0x656E545Au, 0x656E8400u}, // en-TZ -> en-001
+    {0x656E5547u, 0x656E8400u}, // en-UG -> en-001
+    {0x656E5643u, 0x656E8400u}, // en-VC -> en-001
+    {0x656E5647u, 0x656E8400u}, // en-VG -> en-001
+    {0x656E5655u, 0x656E8400u}, // en-VU -> en-001
+    {0x656E5753u, 0x656E8400u}, // en-WS -> en-001
+    {0x656E5A41u, 0x656E8400u}, // en-ZA -> en-001
+    {0x656E5A4Du, 0x656E8400u}, // en-ZM -> en-001
+    {0x656E5A57u, 0x656E8400u}, // en-ZW -> en-001
+    {0x65734152u, 0x6573A424u}, // es-AR -> es-419
+    {0x6573424Fu, 0x6573A424u}, // es-BO -> es-419
+    {0x6573434Cu, 0x6573A424u}, // es-CL -> es-419
+    {0x6573434Fu, 0x6573A424u}, // es-CO -> es-419
+    {0x65734352u, 0x6573A424u}, // es-CR -> es-419
+    {0x65734355u, 0x6573A424u}, // es-CU -> es-419
+    {0x6573444Fu, 0x6573A424u}, // es-DO -> es-419
+    {0x65734543u, 0x6573A424u}, // es-EC -> es-419
+    {0x65734754u, 0x6573A424u}, // es-GT -> es-419
+    {0x6573484Eu, 0x6573A424u}, // es-HN -> es-419
+    {0x65734D58u, 0x6573A424u}, // es-MX -> es-419
+    {0x65734E49u, 0x6573A424u}, // es-NI -> es-419
+    {0x65735041u, 0x6573A424u}, // es-PA -> es-419
+    {0x65735045u, 0x6573A424u}, // es-PE -> es-419
+    {0x65735052u, 0x6573A424u}, // es-PR -> es-419
+    {0x65735059u, 0x6573A424u}, // es-PY -> es-419
+    {0x65735356u, 0x6573A424u}, // es-SV -> es-419
+    {0x65735553u, 0x6573A424u}, // es-US -> es-419
+    {0x65735559u, 0x6573A424u}, // es-UY -> es-419
+    {0x65735645u, 0x6573A424u}, // es-VE -> es-419
+    {0x7074414Fu, 0x70745054u}, // pt-AO -> pt-PT
+    {0x70744356u, 0x70745054u}, // pt-CV -> pt-PT
+    {0x70744757u, 0x70745054u}, // pt-GW -> pt-PT
+    {0x70744D4Fu, 0x70745054u}, // pt-MO -> pt-PT
+    {0x70744D5Au, 0x70745054u}, // pt-MZ -> pt-PT
+    {0x70745354u, 0x70745054u}, // pt-ST -> pt-PT
+    {0x7074544Cu, 0x70745054u}, // pt-TL -> pt-PT
+});
+
+const struct {
+    const char script[4];
+    const std::unordered_map<uint32_t, uint32_t>* map;
+} SCRIPT_PARENTS[] = {
+    {{'A', 'r', 'a', 'b'}, &ARAB_PARENTS},
+    {{'H', 'a', 'n', 't'}, &HANT_PARENTS},
+    {{'L', 'a', 't', 'n'}, &LATN_PARENTS},
+};
+
+const size_t MAX_PARENT_DEPTH = 3;
diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp
index 44f92c7..71e9c92 100644
--- a/libs/androidfw/ResourceTypes.cpp
+++ b/libs/androidfw/ResourceTypes.cpp
@@ -1868,7 +1868,10 @@
     }
 
     // The language & region are equal, so compare the scripts and variants.
-    int script = memcmp(l.localeScript, r.localeScript, sizeof(l.localeScript));
+    const char emptyScript[sizeof(l.localeScript)] = {'\0', '\0', '\0', '\0'};
+    const char *lScript = l.localeScriptWasProvided ? l.localeScript : emptyScript;
+    const char *rScript = r.localeScriptWasProvided ? r.localeScript : emptyScript;
+    int script = memcmp(lScript, rScript, sizeof(l.localeScript));
     if (script) {
         return script;
     }
@@ -2012,10 +2015,10 @@
     // scripts since it seems more useful to do so. We will consider
     // "en-US-POSIX" to be more specific than "en-Latn-US".
 
-    const int score = ((localeScript[0] != 0) ? 1 : 0) +
+    const int score = (localeScriptWasProvided ? 1 : 0) +
         ((localeVariant[0] != 0) ? 2 : 0);
 
-    const int oScore = ((o.localeScript[0] != 0) ? 1 : 0) +
+    const int oScore = (o.localeScriptWasProvided ? 1 : 0) +
         ((o.localeVariant[0] != 0) ? 2 : 0);
 
     return score - oScore;
@@ -2165,6 +2168,63 @@
     return false;
 }
 
+bool ResTable_config::isLocaleBetterThan(const ResTable_config& o,
+        const ResTable_config* requested) const {
+    if (requested->locale == 0) {
+        // The request doesn't have a locale, so no resource is better
+        // than the other.
+        return false;
+    }
+
+    if (locale == 0 && o.locale == 0) {
+        // The locales parts of both resources are empty, so no one is better
+        // than the other.
+        return false;
+    }
+
+    // Non-matching locales have been filtered out, so both resources
+    // match the requested locale.
+    //
+    // Because of the locale-related checks in match() and the checks, we know
+    // that:
+    // 1) The resource languages are either empty or match the request;
+    // and
+    // 2) If the request's script is known, the resource scripts are either
+    //    unknown or match the request.
+
+    if (language[0] != o.language[0]) {
+        // The languages of the two resources are not the same. We can only
+        // assume that one of the two resources matched the request because one
+        // doesn't have a language and the other has a matching language.
+        return (language[0] != 0);
+    }
+
+    // If we are here, both the resources have the same non-empty language as
+    // the request.
+    //
+    // Because the languages are the same, computeScript() always
+    // returns a non-empty script for languages it knows about, and we have passed
+    // the script checks in match(), the scripts are either all unknown or are
+    // all the same. So we can't gain anything by checking the scripts. We need
+    // to check the region and variant.
+
+    // See if any of the regions is better than the other
+    const int region_comparison = localeDataCompareRegions(
+            country, o.country,
+            language, localeScript, requested->country);
+    if (region_comparison != 0) {
+        return (region_comparison > 0);
+    }
+
+    // The regions are the same. Try the variant.
+    if (requested->localeVariant[0] != '\0'
+            && strncmp(localeVariant, requested->localeVariant, sizeof(localeVariant)) == 0) {
+        return (strncmp(o.localeVariant, requested->localeVariant, sizeof(localeVariant)) != 0);
+    }
+
+    return false;
+}
+
 bool ResTable_config::isBetterThan(const ResTable_config& o,
         const ResTable_config* requested) const {
     if (requested) {
@@ -2178,26 +2238,8 @@
             }
         }
 
-        if (locale || o.locale) {
-            if ((language[0] != o.language[0]) && requested->language[0]) {
-                return (language[0]);
-            }
-
-            if ((country[0] != o.country[0]) && requested->country[0]) {
-                return (country[0]);
-            }
-        }
-
-        if (localeScript[0] || o.localeScript[0]) {
-            if (localeScript[0] != o.localeScript[0] && requested->localeScript[0]) {
-                return localeScript[0];
-            }
-        }
-
-        if (localeVariant[0] || o.localeVariant[0]) {
-            if (localeVariant[0] != o.localeVariant[0] && requested->localeVariant[0]) {
-                return localeVariant[0];
-            }
+        if (isLocaleBetterThan(o, requested)) {
+            return true;
         }
 
         if (screenLayout || o.screenLayout) {
@@ -2445,20 +2487,33 @@
         }
     }
     if (locale != 0) {
-        // Don't consider the script & variants when deciding matches.
+        // Don't consider country and variants when deciding matches.
+        // (Theoretically, the variant can also affect the script. For
+        // example, "ar-alalc97" probably implies the Latin script, but since
+        // CLDR doesn't support getting likely scripts for that, we'll assume
+        // the variant doesn't change the script.)
         //
-        // If we two configs differ only in their script or language, they
-        // can be weeded out in the isMoreSpecificThan test.
-        if (language[0] != 0
-            && (language[0] != settings.language[0]
-                || language[1] != settings.language[1])) {
+        // If two configs differ only in their country and variant,
+        // they can be weeded out in the isMoreSpecificThan test.
+        if (language[0] != settings.language[0] || language[1] != settings.language[1]) {
             return false;
         }
 
-        if (country[0] != 0
-            && (country[0] != settings.country[0]
-                || country[1] != settings.country[1])) {
-            return false;
+        // For backward compatibility and supporting private-use locales, we
+        // fall back to old behavior if we couldn't determine the script for
+        // either of the desired locale or the provided locale.
+        if (localeScript[0] == '\0' || localeScript[1] == '\0') {
+            if (country[0] != '\0'
+                && (country[0] != settings.country[0]
+                    || country[1] != settings.country[1])) {
+                return false;
+            }
+        } else {
+            // But if we could determine the scripts, they should be the same
+            // for the locales to match.
+            if (memcmp(localeScript, settings.localeScript, sizeof(localeScript)) != 0) {
+                return false;
+            }
         }
     }
 
@@ -2587,7 +2642,7 @@
         return;
     }
 
-    if (!localeScript[0] && !localeVariant[0]) {
+    if (!localeScriptWasProvided && !localeVariant[0]) {
         // Legacy format.
         if (out.size() > 0) {
             out.append("-");
@@ -2605,7 +2660,7 @@
         return;
     }
 
-    // We are writing the modified bcp47 tag.
+    // We are writing the modified BCP 47 tag.
     // It starts with 'b+' and uses '+' as a separator.
 
     if (out.size() > 0) {
@@ -2617,7 +2672,7 @@
     size_t len = unpackLanguage(buf);
     out.append(buf, len);
 
-    if (localeScript[0]) {
+    if (localeScriptWasProvided) {
         out.append("+");
         out.append(localeScript, sizeof(localeScript));
     }
@@ -2630,7 +2685,7 @@
 
     if (localeVariant[0]) {
         out.append("+");
-        out.append(localeVariant, sizeof(localeVariant));
+        out.append(localeVariant, strnlen(localeVariant, sizeof(localeVariant)));
     }
 }
 
@@ -2648,7 +2703,7 @@
         charsWritten += unpackLanguage(str);
     }
 
-    if (localeScript[0]) {
+    if (localeScriptWasProvided) {
         if (charsWritten) {
             str[charsWritten++] = '-';
         }
@@ -2682,11 +2737,16 @@
            config->language[0] ? config->packRegion(start) : config->packLanguage(start);
            break;
        case 4:
-           config->localeScript[0] = toupper(start[0]);
-           for (size_t i = 1; i < 4; ++i) {
-               config->localeScript[i] = tolower(start[i]);
+           if ('0' <= start[0] && start[0] <= '9') {
+               // this is a variant, so fall through
+           } else {
+               config->localeScript[0] = toupper(start[0]);
+               for (size_t i = 1; i < 4; ++i) {
+                   config->localeScript[i] = tolower(start[i]);
+               }
+               config->localeScriptWasProvided = true;
+               break;
            }
-           break;
        case 5:
        case 6:
        case 7:
@@ -2704,6 +2764,7 @@
 
 void ResTable_config::setBcp47Locale(const char* in) {
     locale = 0;
+    localeScriptWasProvided = false;
     memset(localeScript, 0, sizeof(localeScript));
     memset(localeVariant, 0, sizeof(localeVariant));
 
@@ -2720,6 +2781,9 @@
 
     const size_t size = in + strlen(in) - start;
     assignLocaleComponent(this, start, size);
+    if (localeScript[0] == '\0') {
+        computeScript();
+    };
 }
 
 String8 ResTable_config::toString() const {
diff --git a/libs/androidfw/tests/ConfigLocale_test.cpp b/libs/androidfw/tests/ConfigLocale_test.cpp
index 4999594..1941563 100644
--- a/libs/androidfw/tests/ConfigLocale_test.cpp
+++ b/libs/androidfw/tests/ConfigLocale_test.cpp
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 
+#include <androidfw/LocaleData.h>
 #include <androidfw/ResourceTypes.h>
 #include <utils/Log.h>
 #include <utils/String8.h>
@@ -28,7 +29,7 @@
      EXPECT_EQ('e', config.language[0]);
      EXPECT_EQ('n', config.language[1]);
 
-     char out[4] = { 1, 1, 1, 1};
+     char out[4] = {1, 1, 1, 1};
      config.unpackLanguage(out);
      EXPECT_EQ('e', out[0]);
      EXPECT_EQ('n', out[1]);
@@ -51,7 +52,7 @@
      EXPECT_EQ('U', config.country[0]);
      EXPECT_EQ('S', config.country[1]);
 
-     char out[4] = { 1, 1, 1, 1};
+     char out[4] = {1, 1, 1, 1};
      config.unpackRegion(out);
      EXPECT_EQ('U', out[0]);
      EXPECT_EQ('S', out[1]);
@@ -67,7 +68,7 @@
      EXPECT_EQ('\x99', config.language[0]);
      EXPECT_EQ('\xA4', config.language[1]);
 
-     char out[4] = { 1, 1, 1, 1};
+     char out[4] = {1, 1, 1, 1};
      config.unpackLanguage(out);
      EXPECT_EQ('e', out[0]);
      EXPECT_EQ('n', out[1]);
@@ -91,7 +92,7 @@
      EXPECT_EQ(char(0xbc), config.language[0]);
      EXPECT_EQ(char(0xd3), config.language[1]);
 
-     char out[4] = { 1, 1, 1, 1};
+     char out[4] = {1, 1, 1, 1};
      config.unpackLanguage(out);
      EXPECT_EQ('t', out[0]);
      EXPECT_EQ('g', out[1]);
@@ -103,7 +104,7 @@
      ResTable_config config;
      config.packRegion("419");
 
-     char out[4] = { 1, 1, 1, 1};
+     char out[4] = {1, 1, 1, 1};
      config.unpackRegion(out);
 
      EXPECT_EQ('4', out[0]);
@@ -124,6 +125,10 @@
 
      if (script != NULL) {
          memcpy(out->localeScript, script, 4);
+         out->localeScriptWasProvided = true;
+     } else {
+         out->computeScript();
+         out->localeScriptWasProvided = false;
      }
 
      if (variant != NULL) {
@@ -177,11 +182,12 @@
     EXPECT_EQ('n', test.language[1]);
     EXPECT_EQ('U', test.country[0]);
     EXPECT_EQ('S', test.country[1]);
-    EXPECT_EQ(0, test.localeScript[0]);
+    EXPECT_FALSE(test.localeScriptWasProvided);
+    EXPECT_EQ(0, memcmp("Latn", test.localeScript, 4));
     EXPECT_EQ(0, test.localeVariant[0]);
 
     test.setBcp47Locale("eng-419");
-    char out[4] = { 1, 1, 1, 1};
+    char out[4] = {1, 1, 1, 1};
     test.unpackLanguage(out);
     EXPECT_EQ('e', out[0]);
     EXPECT_EQ('n', out[1]);
@@ -193,17 +199,397 @@
     EXPECT_EQ('1', out[1]);
     EXPECT_EQ('9', out[2]);
 
-
     test.setBcp47Locale("en-Latn-419");
-    memset(out, 1, 4);
     EXPECT_EQ('e', test.language[0]);
     EXPECT_EQ('n', test.language[1]);
-
     EXPECT_EQ(0, memcmp("Latn", test.localeScript, 4));
+    EXPECT_TRUE(test.localeScriptWasProvided);
+    memset(out, 1, 4);
     test.unpackRegion(out);
     EXPECT_EQ('4', out[0]);
     EXPECT_EQ('1', out[1]);
     EXPECT_EQ('9', out[2]);
+
+    test.setBcp47Locale("de-1901");
+    memset(out, 1, 4);
+    test.unpackLanguage(out);
+    EXPECT_EQ('d', out[0]);
+    EXPECT_EQ('e', out[1]);
+    EXPECT_EQ('\0', out[2]);
+    EXPECT_FALSE(test.localeScriptWasProvided);
+    EXPECT_EQ(0, memcmp("Latn", test.localeScript, 4));
+    memset(out, 1, 4);
+    test.unpackRegion(out);
+    EXPECT_EQ('\0', out[0]);
+    EXPECT_EQ(0, strcmp("1901", test.localeVariant));
+
+    test.setBcp47Locale("de-Latn-1901");
+    memset(out, 1, 4);
+    test.unpackLanguage(out);
+    EXPECT_EQ('d', out[0]);
+    EXPECT_EQ('e', out[1]);
+    EXPECT_EQ('\0', out[2]);
+    EXPECT_TRUE(test.localeScriptWasProvided);
+    EXPECT_EQ(0, memcmp("Latn", test.localeScript, 4));
+    memset(out, 1, 4);
+    test.unpackRegion(out);
+    EXPECT_EQ('\0', out[0]);
+    EXPECT_EQ(0, strcmp("1901", test.localeVariant));
 }
 
-}  // namespace android.
+TEST(ConfigLocaleTest, computeScript) {
+    ResTable_config config;
+
+    fillIn(NULL, NULL, NULL, NULL, &config);
+    EXPECT_EQ(0, memcmp("\0\0\0\0", config.localeScript, 4));
+
+    fillIn("zh", "TW", NULL, NULL, &config);
+    EXPECT_EQ(0, memcmp("Hant", config.localeScript, 4));
+
+    fillIn("zh", "CN", NULL, NULL, &config);
+    EXPECT_EQ(0, memcmp("Hans", config.localeScript, 4));
+
+    fillIn("az", NULL, NULL, NULL, &config);
+    EXPECT_EQ(0, memcmp("Latn", config.localeScript, 4));
+
+    fillIn("az", "AZ", NULL, NULL, &config);
+    EXPECT_EQ(0, memcmp("Latn", config.localeScript, 4));
+
+    fillIn("az", "IR", NULL, NULL, &config);
+    EXPECT_EQ(0, memcmp("Arab", config.localeScript, 4));
+
+    fillIn("peo", NULL, NULL, NULL, &config);
+    EXPECT_EQ(0, memcmp("Xpeo", config.localeScript, 4));
+
+    fillIn("qaa", NULL, NULL, NULL, &config);
+    EXPECT_EQ(0, memcmp("\0\0\0\0", config.localeScript, 4));
+}
+
+TEST(ConfigLocaleTest, getBcp47Locale_script) {
+    ResTable_config config;
+    fillIn("en", NULL, "Latn", NULL, &config);
+
+    char out[RESTABLE_MAX_LOCALE_LEN];
+    config.localeScriptWasProvided = true;
+    config.getBcp47Locale(out);
+    EXPECT_EQ(0, strcmp("en-Latn", out));
+
+    config.localeScriptWasProvided = false;
+    config.getBcp47Locale(out);
+    EXPECT_EQ(0, strcmp("en", out));
+}
+
+TEST(ConfigLocaleTest, match) {
+    ResTable_config supported, requested;
+
+    fillIn(NULL, NULL, NULL, NULL, &supported);
+    fillIn("fr", "CA", NULL, NULL, &requested);
+    // Empty locale matches everything (as a default).
+    EXPECT_TRUE(supported.match(requested));
+
+    fillIn("en", "CA", NULL, NULL, &supported);
+    fillIn("fr", "CA", NULL, NULL, &requested);
+    // Different languages don't match.
+    EXPECT_FALSE(supported.match(requested));
+
+    fillIn("qaa", "FR", NULL, NULL, &supported);
+    fillIn("qaa", "CA", NULL, NULL, &requested);
+    // If we can't infer the scripts, different regions don't match.
+    EXPECT_FALSE(supported.match(requested));
+
+    fillIn("qaa", "FR", "Latn", NULL, &supported);
+    fillIn("qaa", "CA", NULL, NULL, &requested);
+    // If we can't infer any of the scripts, different regions don't match.
+    EXPECT_FALSE(supported.match(requested));
+
+    fillIn("qaa", "FR", NULL, NULL, &supported);
+    fillIn("qaa", "CA", "Latn", NULL, &requested);
+    // If we can't infer any of the scripts, different regions don't match.
+    EXPECT_FALSE(supported.match(requested));
+
+    fillIn("qaa", NULL, NULL, NULL, &supported);
+    fillIn("qaa", "CA", NULL, NULL, &requested);
+    // language-only resources still support language+region requests, even if we can't infer the
+    // script.
+    EXPECT_TRUE(supported.match(requested));
+
+    fillIn("qaa", "CA", NULL, NULL, &supported);
+    fillIn("qaa", "CA", NULL, NULL, &requested);
+    // Even if we can't infer the scripts, exactly equal locales match.
+    EXPECT_TRUE(supported.match(requested));
+
+    fillIn("az", NULL, NULL, NULL, &supported);
+    fillIn("az", NULL, "Latn", NULL, &requested);
+    // If the resolved scripts are the same, it doesn't matter if they were explicitly provided
+    // or not, and they match.
+    EXPECT_TRUE(supported.match(requested));
+
+    fillIn("az", NULL, NULL, NULL, &supported);
+    fillIn("az", NULL, "Cyrl", NULL, &requested);
+    // If the resolved scripts are different, they don't match.
+    EXPECT_FALSE(supported.match(requested));
+
+    fillIn("az", NULL, NULL, NULL, &supported);
+    fillIn("az", "IR", NULL, NULL, &requested);
+    // If the resolved scripts are different, they don't match.
+    EXPECT_FALSE(supported.match(requested));
+
+    fillIn("az", "IR", NULL, NULL, &supported);
+    fillIn("az", NULL, "Arab", NULL, &requested);
+    // If the resolved scripts are the same, it doesn't matter if they were explicitly provided
+    // or not, and they match.
+    EXPECT_TRUE(supported.match(requested));
+
+    fillIn("en", NULL, NULL, NULL, &supported);
+    fillIn("en", "XA", NULL, NULL, &requested);
+    // en-XA is a pseudo-locale, and English resources are not a match for it.
+    EXPECT_FALSE(supported.match(requested));
+
+    fillIn("en", "XA", NULL, NULL, &supported);
+    fillIn("en", NULL, NULL, NULL, &requested);
+    // en-XA is a pseudo-locale, and its resources don't support English locales.
+    EXPECT_FALSE(supported.match(requested));
+
+    fillIn("en", "XA", NULL, NULL, &supported);
+    fillIn("en", "XA", NULL, NULL, &requested);
+    // Even if they are pseudo-locales, exactly equal locales match.
+    EXPECT_TRUE(supported.match(requested));
+
+    fillIn("ar", NULL, NULL, NULL, &supported);
+    fillIn("ar", "XB", NULL, NULL, &requested);
+    // ar-XB is a pseudo-locale, and Arabic resources are not a match for it.
+    EXPECT_FALSE(supported.match(requested));
+
+    fillIn("ar", "XB", NULL, NULL, &supported);
+    fillIn("ar", NULL, NULL, NULL, &requested);
+    // ar-XB is a pseudo-locale, and its resources don't support Arabic locales.
+    EXPECT_FALSE(supported.match(requested));
+
+    fillIn("ar", "XB", NULL, NULL, &supported);
+    fillIn("ar", "XB", NULL, NULL, &requested);
+    // Even if they are pseudo-locales, exactly equal locales match.
+    EXPECT_TRUE(supported.match(requested));
+}
+
+TEST(ConfigLocaleTest, isLocaleBetterThan_basics) {
+    ResTable_config config1, config2, request;
+
+    fillIn(NULL, NULL, NULL, NULL, &request);
+    fillIn("fr", "FR", NULL, NULL, &config1);
+    fillIn("fr", "CA", NULL, NULL, &config2);
+    EXPECT_FALSE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("fr", "CA", NULL, NULL, &request);
+    fillIn(NULL, NULL, NULL, NULL, &config1);
+    fillIn(NULL, NULL, NULL, NULL, &config2);
+    EXPECT_FALSE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("fr", "CA", NULL, NULL, &request);
+    fillIn("fr", "FR", NULL, NULL, &config1);
+    fillIn(NULL, NULL, NULL, NULL, &config2);
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("de", "DE", NULL, NULL, &request);
+    fillIn("de", "DE", NULL, "1901", &config1);
+    fillIn("de", "DE", NULL, "1996", &config2);
+    EXPECT_FALSE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("de", "DE", NULL, "1901", &request);
+    fillIn("de", "DE", NULL, "1901", &config1);
+    fillIn("de", "DE", NULL, NULL, &config2);
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("de", "DE", NULL, "1901", &request);
+    fillIn("de", "DE", NULL, "1996", &config1);
+    fillIn("de", "DE", NULL, NULL, &config2);
+    EXPECT_FALSE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+}
+
+TEST(ConfigLocaleTest, isLocaleBetterThan_regionComparison) {
+    ResTable_config config1, config2, request;
+
+    fillIn("es", "AR", NULL, NULL, &request);
+    fillIn("es", "419", NULL, NULL, &config1);
+    fillIn("es", "419", NULL, NULL, &config2);
+    // Both supported locales are the same, so none is better than the other.
+    EXPECT_FALSE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("es", "AR", NULL, NULL, &request);
+    fillIn("es", "AR", NULL, NULL, &config1);
+    fillIn("es", "419", NULL, NULL, &config2);
+    // An exact locale match is better than a parent.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("es", "AR", NULL, NULL, &request);
+    fillIn("es", "419", NULL, NULL, &config1);
+    fillIn("es", NULL, NULL, NULL, &config2);
+    // A closer parent is better.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("es", "AR", NULL, NULL, &request);
+    fillIn("es", "419", NULL, NULL, &config1);
+    fillIn("es", "ES", NULL, NULL, &config2);
+    // A parent is better than a non-parent representative locale.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("es", "AR", NULL, NULL, &request);
+    fillIn("es", NULL, NULL, NULL, &config1);
+    fillIn("es", "ES", NULL, NULL, &config2);
+    // A parent is better than a non-parent representative locale.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("es", "AR", NULL, NULL, &request);
+    fillIn("es", "PE", NULL, NULL, &config1);
+    fillIn("es", "ES", NULL, NULL, &config2);
+    // A closer locale is better.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("es", "AR", NULL, NULL, &request);
+    fillIn("es", "MX", NULL, NULL, &config1);
+    fillIn("es", "BO", NULL, NULL, &config2);
+    // A representative locale is better if they are equidistant.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("es", "AR", NULL, NULL, &request);
+    fillIn("es", "US", NULL, NULL, &config1);
+    fillIn("es", "BO", NULL, NULL, &config2);
+    // A representative locale is better if they are equidistant.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("es", "AR", NULL, NULL, &request);
+    fillIn("es", "MX", NULL, NULL, &config1);
+    fillIn("es", "US", NULL, NULL, &config2);
+    // If all is equal, the locale earlier in the dictionary is better.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("es", "GQ", NULL, NULL, &request);
+    fillIn("es", "IC", NULL, NULL, &config1);
+    fillIn("es", "419", NULL, NULL, &config2);
+    // If all is equal, the locale earlier in the dictionary is better and
+    // letters are better than numbers.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("en", "GB", NULL, NULL, &request);
+    fillIn("en", "001", NULL, NULL, &config1);
+    fillIn("en", NULL, NULL, NULL, &config2);
+    // A closer parent is better.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("en", "PR", NULL, NULL, &request);
+    fillIn("en", NULL, NULL, NULL, &config1);
+    fillIn("en", "001", NULL, NULL, &config2);
+    // A parent is better than a non-parent.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("en", "DE", NULL, NULL, &request);
+    fillIn("en", "150", NULL, NULL, &config1);
+    fillIn("en", "001", NULL, NULL, &config2);
+    // A closer parent is better.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("en", "IN", NULL, NULL, &request);
+    fillIn("en", "AU", NULL, NULL, &config1);
+    fillIn("en", "US", NULL, NULL, &config2);
+    // A closer locale is better.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("en", "PR", NULL, NULL, &request);
+    fillIn("en", "001", NULL, NULL, &config1);
+    fillIn("en", "GB", NULL, NULL, &config2);
+    // A closer locale is better.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("en", "IN", NULL, NULL, &request);
+    fillIn("en", "GB", NULL, NULL, &config1);
+    fillIn("en", "AU", NULL, NULL, &config2);
+    // A representative locale is better if they are equidistant.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("en", "IN", NULL, NULL, &request);
+    fillIn("en", "AU", NULL, NULL, &config1);
+    fillIn("en", "CA", NULL, NULL, &config2);
+    // If all is equal, the locale earlier in the dictionary is better.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("pt", "MZ", NULL, NULL, &request);
+    fillIn("pt", "PT", NULL, NULL, &config1);
+    fillIn("pt", NULL, NULL, NULL, &config2);
+    // A closer parent is better.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("pt", "MZ", NULL, NULL, &request);
+    fillIn("pt", "PT", NULL, NULL, &config1);
+    fillIn("pt", "BR", NULL, NULL, &config2);
+    // A parent is better than a non-parent.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("zh", "MO", "Hant", NULL, &request);
+    fillIn("zh", "HK", "Hant", NULL, &config1);
+    fillIn("zh", "TW", "Hant", NULL, &config2);
+    // A parent is better than a non-parent.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("zh", "US", "Hant", NULL, &request);
+    fillIn("zh", "TW", "Hant", NULL, &config1);
+    fillIn("zh", "HK", "Hant", NULL, &config2);
+    // A representative locale is better if they are equidistant.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("ar", "DZ", NULL, NULL, &request);
+    fillIn("ar", "015", NULL, NULL, &config1);
+    fillIn("ar", NULL, NULL, NULL, &config2);
+    // A closer parent is better.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("ar", "EG", NULL, NULL, &request);
+    fillIn("ar", NULL, NULL, NULL, &config1);
+    fillIn("ar", "015", NULL, NULL, &config2);
+    // A parent is better than a non-parent.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("ar", "QA", NULL, NULL, &request);
+    fillIn("ar", "EG", NULL, NULL, &config1);
+    fillIn("ar", "BH", NULL, NULL, &config2);
+    // A representative locale is better if they are equidistant.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+
+    fillIn("ar", "QA", NULL, NULL, &request);
+    fillIn("ar", "SA", NULL, NULL, &config1);
+    fillIn("ar", "015", NULL, NULL, &config2);
+    // If all is equal, the locale earlier in the dictionary is better and
+    // letters are better than numbers.
+    EXPECT_TRUE(config1.isLocaleBetterThan(config2, &request));
+    EXPECT_FALSE(config2.isLocaleBetterThan(config1, &request));
+}
+
+}  // namespace android
diff --git a/libs/hwui/OpenGLRenderer.cpp b/libs/hwui/OpenGLRenderer.cpp
index 1bfa308..db017fe 100644
--- a/libs/hwui/OpenGLRenderer.cpp
+++ b/libs/hwui/OpenGLRenderer.cpp
@@ -1630,6 +1630,7 @@
 
     Texture* texture = entry ? entry->texture : mCaches.textureCache.get(bitmap);
     if (!texture) return;
+    const AutoTexture autoCleanup(texture);
 
     // 9 patches are built for stretching - always filter
     int textureFillFlags = TextureFillFlags::ForceFilter;
diff --git a/libs/hwui/VectorDrawable.cpp b/libs/hwui/VectorDrawable.cpp
index 793df92..3e20608 100644
--- a/libs/hwui/VectorDrawable.cpp
+++ b/libs/hwui/VectorDrawable.cpp
@@ -31,7 +31,7 @@
 
 const int Tree::MAX_CACHED_BITMAP_SIZE = 2048;
 
-void Path::draw(Canvas* outCanvas, const SkMatrix& groupStackedMatrix, float scaleX, float scaleY) {
+void Path::draw(SkCanvas* outCanvas, const SkMatrix& groupStackedMatrix, float scaleX, float scaleY) {
     float matrixScale = getMatrixScale(groupStackedMatrix);
     if (matrixScale == 0) {
         // When either x or y is scaled to 0, we don't need to draw anything.
@@ -186,7 +186,7 @@
     return SkColorSetA(color, alphaBytes * alpha);
 }
 
-void FullPath::drawPath(Canvas* outCanvas, const SkPath& renderPath, float strokeScale){
+void FullPath::drawPath(SkCanvas* outCanvas, const SkPath& renderPath, float strokeScale){
     // Draw path's fill, if fill color isn't transparent.
     if (mFillColor != SK_ColorTRANSPARENT) {
         mPaint.setStyle(SkPaint::Style::kFill_Style);
@@ -287,9 +287,9 @@
     return true;
 }
 
-void ClipPath::drawPath(Canvas* outCanvas, const SkPath& renderPath,
+void ClipPath::drawPath(SkCanvas* outCanvas, const SkPath& renderPath,
         float strokeScale){
-    outCanvas->clipPath(&renderPath, SkRegion::kIntersect_Op);
+    outCanvas->clipPath(renderPath, SkRegion::kIntersect_Op);
 }
 
 Group::Group(const Group& group) : Node(group) {
@@ -302,7 +302,7 @@
     mTranslateY = group.mTranslateY;
 }
 
-void Group::draw(Canvas* outCanvas, const SkMatrix& currentMatrix, float scaleX,
+void Group::draw(SkCanvas* outCanvas, const SkMatrix& currentMatrix, float scaleX,
         float scaleY) {
     // TODO: Try apply the matrix to the canvas instead of passing it down the tree
 
@@ -315,7 +315,7 @@
     stackedMatrix.postConcat(currentMatrix);
 
     // Save the current clip information, which is local to this group.
-    outCanvas->save(SkCanvas::kMatrixClip_SaveFlag);
+    outCanvas->save();
     // Draw the group tree in the same order as the XML file.
     for (Node* child : mChildren) {
         child->draw(outCanvas, stackedMatrix, scaleX, scaleY);
@@ -465,10 +465,10 @@
 
 void Tree::updateCachedBitmap(int width, int height) {
     mCachedBitmap.eraseColor(SK_ColorTRANSPARENT);
-    Canvas* outCanvas = Canvas::create_canvas(mCachedBitmap);
+    SkCanvas outCanvas(mCachedBitmap);
     float scaleX = width / mViewportWidth;
     float scaleY = height / mViewportHeight;
-    mRootNode->draw(outCanvas, SkMatrix::I(), scaleX, scaleY);
+    mRootNode->draw(&outCanvas, SkMatrix::I(), scaleX, scaleY);
     mCacheDirty = false;
 }
 
diff --git a/libs/hwui/VectorDrawable.h b/libs/hwui/VectorDrawable.h
index 6c84b05..5ae5f6a 100644
--- a/libs/hwui/VectorDrawable.h
+++ b/libs/hwui/VectorDrawable.h
@@ -20,6 +20,7 @@
 #include "Canvas.h"
 #include <SkBitmap.h>
 #include <SkColor.h>
+#include <SkCanvas.h>
 #include <SkMatrix.h>
 #include <SkPaint.h>
 #include <SkPath.h>
@@ -56,7 +57,7 @@
         mName = node.mName;
     }
     Node() {}
-    virtual void draw(Canvas* outCanvas, const SkMatrix& currentMatrix,
+    virtual void draw(SkCanvas* outCanvas, const SkMatrix& currentMatrix,
             float scaleX, float scaleY) = 0;
     virtual void dump() = 0;
     void setName(const char* name) {
@@ -85,7 +86,7 @@
     void dump() override;
     bool canMorph(const Data& path);
     bool canMorph(const Path& path);
-    void draw(Canvas* outCanvas, const SkMatrix& groupStackedMatrix,
+    void draw(SkCanvas* outCanvas, const SkMatrix& groupStackedMatrix,
             float scaleX, float scaleY) override;
     void setPath(const char* path, size_t strLength);
     void setPathData(const Data& data);
@@ -93,7 +94,7 @@
 
 protected:
     virtual const SkPath& getUpdatedPath();
-    virtual void drawPath(Canvas *outCanvas, const SkPath& renderPath,
+    virtual void drawPath(SkCanvas *outCanvas, const SkPath& renderPath,
             float strokeScale) = 0;
     Data mData;
     SkPath mSkPath;
@@ -163,7 +164,7 @@
 
 protected:
     const SkPath& getUpdatedPath() override;
-    void drawPath(Canvas* outCanvas, const SkPath& renderPath,
+    void drawPath(SkCanvas* outCanvas, const SkPath& renderPath,
             float strokeScale) override;
 
 private:
@@ -193,7 +194,7 @@
     ClipPath(const Data& nodes) : Path(nodes) {}
 
 protected:
-    void drawPath(Canvas* outCanvas, const SkPath& renderPath,
+    void drawPath(SkCanvas* outCanvas, const SkPath& renderPath,
             float strokeScale) override;
 };
 
@@ -243,7 +244,7 @@
     void setTranslateY(float translateY) {
         mTranslateY = translateY;
     }
-    virtual void draw(Canvas* outCanvas, const SkMatrix& currentMatrix,
+    virtual void draw(SkCanvas* outCanvas, const SkMatrix& currentMatrix,
             float scaleX, float scaleY) override;
     void updateLocalMatrix(float rotate, float pivotX, float pivotY,
             float scaleX, float scaleY, float translateX, float translateY);
diff --git a/libs/hwui/renderstate/RenderState.cpp b/libs/hwui/renderstate/RenderState.cpp
index e71d6ee..75dcf16 100644
--- a/libs/hwui/renderstate/RenderState.cpp
+++ b/libs/hwui/renderstate/RenderState.cpp
@@ -340,6 +340,9 @@
     SkiaShader::apply(*mCaches, fill.skiaShaderData);
 
     GL_CHECKPOINT();
+    Texture* texture = (fill.skiaShaderData.skiaShaderType & kBitmap_SkiaShaderType) ?
+            fill.skiaShaderData.bitmapData.bitmapTexture : nullptr;
+    const AutoTexture autoCleanup(texture);
 
     // ------------------------------------
     // ---------- GL state setup ----------
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 5ad6b08..dc534be 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -2163,7 +2163,7 @@
      * audio service.
      */
     private final ServiceEventHandlerDelegate mServiceEventHandlerDelegate =
-            new ServiceEventHandlerDelegate();
+            new ServiceEventHandlerDelegate(null);
 
     /**
      * Event types
@@ -2177,10 +2177,14 @@
     private class ServiceEventHandlerDelegate {
         private final Handler mHandler;
 
-        ServiceEventHandlerDelegate() {
+        ServiceEventHandlerDelegate(Handler handler) {
             Looper looper;
-            if ((looper = Looper.myLooper()) == null) {
-                looper = Looper.getMainLooper();
+            if (handler == null) {
+                if ((looper = Looper.myLooper()) == null) {
+                    looper = Looper.getMainLooper();
+                }
+            } else {
+                looper = handler.getLooper();
             }
 
             if (looper != null) {
@@ -2201,27 +2205,9 @@
                                 }
                                 break;
                             case MSSG_RECORDING_CONFIG_CHANGE:
-                                // optimizing for the case of a single callback
-                                AudioRecordingCallback singleCallback = null;
-                                ArrayList<AudioRecordingCallback> multipleCallbacks = null;
-                                synchronized(mRecordCallbackLock) {
-                                    if ((mRecordCallbackList != null)
-                                            && (mRecordCallbackList.size() != 0)) {
-                                        if (mRecordCallbackList.size() == 1) {
-                                            singleCallback = mRecordCallbackList.get(0);
-                                        } else {
-                                            multipleCallbacks =
-                                                    new ArrayList<AudioRecordingCallback>(
-                                                            mRecordCallbackList);
-                                        }
-                                    }
-                                }
-                                if (singleCallback != null) {
-                                    singleCallback.onRecordConfigChanged();
-                                } else if (multipleCallbacks != null) {
-                                    for (int i=0 ; i < multipleCallbacks.size() ; i++) {
-                                        multipleCallbacks.get(i).onRecordConfigChanged();
-                                    }
+                                final AudioRecordingCallback cb = (AudioRecordingCallback) msg.obj;
+                                if (cb != null) {
+                                    cb.onRecordConfigChanged();
                                 }
                                 break;
                             default:
@@ -2798,34 +2784,51 @@
     //====================================================================
     // Recording configuration
     /**
-     * @hide
-     * candidate for public API
+     * Interface for receiving update notifications about the recording configuration. Extend
+     * this abstract class and register it with
+     * {@link AudioManager#registerAudioRecordingCallback(AudioRecordingCallback, Handler)}
+     * to be notified.
+     * Use {@link AudioManager#getActiveRecordConfigurations()} to query the current configuration.
      */
     public static abstract class AudioRecordingCallback {
         /**
-         * @hide
-         * candidate for public API
+         * Called whenever the device recording configuration has changed.
          */
         public void onRecordConfigChanged() {}
     }
 
+    private static class AudioRecordingCallbackInfo {
+        final AudioRecordingCallback mCb;
+        final Handler mHandler;
+        AudioRecordingCallbackInfo(AudioRecordingCallback cb, Handler handler) {
+            mCb = cb;
+            mHandler = handler;
+        }
+    }
+
     /**
-     * @hide
-     * candidate for public API
-     * @param non-null callback
+     * Register a callback to be notified of audio recording changes through
+     * {@link AudioRecordingCallback}
+     * @param cb non-null callback to register
+     * @param handler the {@link Handler} object for the thread on which to execute
+     * the callback. If <code>null</code>, the {@link Handler} associated with the main
+     * {@link Looper} will be used.
      */
-    public void registerAudioRecordingCallback(@NonNull AudioRecordingCallback cb) {
+    public void registerAudioRecordingCallback(@NonNull AudioRecordingCallback cb, Handler handler)
+    {
         if (cb == null) {
             throw new IllegalArgumentException("Illegal null AudioRecordingCallback argument");
         }
+
         synchronized(mRecordCallbackLock) {
             // lazy initialization of the list of recording callbacks
             if (mRecordCallbackList == null) {
-                mRecordCallbackList = new ArrayList<AudioRecordingCallback>();
+                mRecordCallbackList = new ArrayList<AudioRecordingCallbackInfo>();
             }
             final int oldCbCount = mRecordCallbackList.size();
-            if (!mRecordCallbackList.contains(cb)) {
-                mRecordCallbackList.add(cb);
+            if (!hasRecordCallback_sync(cb)) {
+                mRecordCallbackList.add(new AudioRecordingCallbackInfo(cb,
+                        new ServiceEventHandlerDelegate(handler).getHandler()));
                 final int newCbCount = mRecordCallbackList.size();
                 if ((oldCbCount == 0) && (newCbCount > 0)) {
                     // register binder for callbacks
@@ -2844,9 +2847,9 @@
     }
 
     /**
-     * @hide
-     * candidate for public API
-     * @param non-null callback
+     * Unregister an audio recording callback previously registered with
+     * {@link #registerAudioRecordingCallback(AudioRecordingCallback, Handler)}.
+     * @param cb non-null callback to unregister
      */
     public void unregisterAudioRecordingCallback(@NonNull AudioRecordingCallback cb) {
         if (cb == null) {
@@ -2857,7 +2860,7 @@
                 return;
             }
             final int oldCbCount = mRecordCallbackList.size();
-            if (mRecordCallbackList.remove(cb)) {
+            if (removeRecordCallback_sync(cb)) {
                 final int newCbCount = mRecordCallbackList.size();
                 if ((oldCbCount > 0) && (newCbCount == 0)) {
                     // unregister binder for callbacks
@@ -2876,8 +2879,7 @@
     }
 
     /**
-     * @hide
-     * candidate for public API
+     * Returns the current active audio recording configurations of the device.
      * @return a non-null array of recording configurations. An array of length 0 indicates there is
      *     no recording active when queried.
      */
@@ -2902,18 +2904,57 @@
 
     /**
      * All operations on this list are sync'd on mRecordCallbackLock.
-     * List is lazy-initialized in {@link #registerAudioRecordingCallback(AudioRecordingCallback)}.
+     * List is lazy-initialized in
+     * {@link #registerAudioRecordingCallback(AudioRecordingCallback, Handler)}.
      * List can be null.
      */
-    private List<AudioRecordingCallback> mRecordCallbackList;
+    private List<AudioRecordingCallbackInfo> mRecordCallbackList;
     private final Object mRecordCallbackLock = new Object();
 
+    /**
+     * Must be called synchronized on mRecordCallbackLock
+     */
+    private boolean hasRecordCallback_sync(@NonNull AudioRecordingCallback cb) {
+        if (mRecordCallbackList != null) {
+            for (int i=0 ; i < mRecordCallbackList.size() ; i++) {
+                if (cb.equals(mRecordCallbackList.get(i).mCb)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Must be called synchronized on mRecordCallbackLock
+     */
+    private boolean removeRecordCallback_sync(@NonNull AudioRecordingCallback cb) {
+        if (mRecordCallbackList != null) {
+            for (int i=0 ; i < mRecordCallbackList.size() ; i++) {
+                if (cb.equals(mRecordCallbackList.get(i).mCb)) {
+                    mRecordCallbackList.remove(i);
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
     private final IRecordingConfigDispatcher mRecCb = new IRecordingConfigDispatcher.Stub() {
 
         public void dispatchRecordingConfigChange() {
-            final Message m = mServiceEventHandlerDelegate.getHandler().obtainMessage(
-                    MSSG_RECORDING_CONFIG_CHANGE/*what*/);
-            mServiceEventHandlerDelegate.getHandler().sendMessage(m);
+            synchronized(mRecordCallbackLock) {
+                if (mRecordCallbackList != null) {
+                    for (int i=0 ; i < mRecordCallbackList.size() ; i++) {
+                        final AudioRecordingCallbackInfo arci = mRecordCallbackList.get(i);
+                        if (arci.mHandler != null) {
+                            final Message m = arci.mHandler.obtainMessage(
+                                    MSSG_RECORDING_CONFIG_CHANGE/*what*/, arci.mCb/*obj*/);
+                            arci.mHandler.sendMessage(m);
+                        }
+                    }
+                }
+            }
         }
 
     };
diff --git a/media/java/android/media/AudioRecordConfiguration.java b/media/java/android/media/AudioRecordConfiguration.java
index aefe692..c7d219d 100644
--- a/media/java/android/media/AudioRecordConfiguration.java
+++ b/media/java/android/media/AudioRecordConfiguration.java
@@ -22,8 +22,9 @@
 import java.util.Objects;
 
 /**
- * @hide
- * Candidate for public API, see AudioManager.getActiveRecordConfiguration()
+ * The AudioRecordConfiguration class collects the information describing an audio recording
+ * session. This information is returned through the 
+ * {@link AudioManager#getActiveRecordConfigurations()} method.
  *
  */
 public class AudioRecordConfiguration implements Parcelable {
@@ -41,19 +42,23 @@
     }
 
     /**
-     * @return one of AudioSource.MIC, AudioSource.VOICE_UPLINK,
-     *       AudioSource.VOICE_DOWNLINK, AudioSource.VOICE_CALL,
-     *       AudioSource.CAMCORDER, AudioSource.VOICE_RECOGNITION,
-     *       AudioSource.VOICE_COMMUNICATION.
+     * Returns the audio source being used for the recording.
+     * @return one of {@link MediaRecorder.AudioSource#MIC},
+     *       {@link MediaRecorder.AudioSource#VOICE_UPLINK},
+     *       {@link MediaRecorder.AudioSource#VOICE_DOWNLINK},
+     *       {@link MediaRecorder.AudioSource#VOICE_CALL},
+     *       {@link MediaRecorder.AudioSource#CAMCORDER},
+     *       {@link MediaRecorder.AudioSource#VOICE_RECOGNITION},
+     *       {@link MediaRecorder.AudioSource#VOICE_COMMUNICATION}.
      */
     public int getClientAudioSource() { return mClientSource; }
 
     /**
-     * @return the session number of the recorder.
+     * Returns the session number of the recording, see {@link AudioRecord#getAudioSessionId()}.
+     * @return the session number.
      */
     public int getAudioSessionId() { return mSessionId; }
 
-
     public static final Parcelable.Creator<AudioRecordConfiguration> CREATOR
             = new Parcelable.Creator<AudioRecordConfiguration>() {
         /**
diff --git a/native/android/Android.mk b/native/android/Android.mk
index 1742bee..5386e6f 100644
--- a/native/android/Android.mk
+++ b/native/android/Android.mk
@@ -7,6 +7,7 @@
 #
 LOCAL_SRC_FILES:= \
     asset_manager.cpp \
+    choreographer.cpp \
     configuration.cpp \
     input.cpp \
     looper.cpp \
@@ -43,4 +44,7 @@
 
 LOCAL_CFLAGS += -Wall -Werror -Wunused -Wunreachable-code
 
+# Required because of b/25642296
+LOCAL_CLANG_arm64 := false
+
 include $(BUILD_SHARED_LIBRARY)
diff --git a/native/android/choreographer.cpp b/native/android/choreographer.cpp
new file mode 100644
index 0000000..cc2fd77
--- /dev/null
+++ b/native/android/choreographer.cpp
@@ -0,0 +1,205 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "Choreographer"
+//#define LOG_NDEBUG 0
+
+#include <cinttypes>
+#include <queue>
+#include <thread>
+
+#include <android/choreographer.h>
+#include <androidfw/DisplayEventDispatcher.h>
+#include <gui/ISurfaceComposer.h>
+#include <utils/Looper.h>
+#include <utils/Mutex.h>
+#include <utils/Timers.h>
+
+namespace android {
+
+static inline const char* toString(bool value) {
+    return value ? "true" : "false";
+}
+
+struct FrameCallback {
+    AChoreographer_frameCallback callback;
+    void* data;
+    nsecs_t dueTime;
+
+    inline bool operator<(const FrameCallback& rhs) const {
+        // Note that this is intentionally flipped because we want callbacks due sooner to be at
+        // the head of the queue
+        return dueTime > rhs.dueTime;
+    }
+};
+
+
+class Choreographer : public DisplayEventDispatcher, public MessageHandler {
+public:
+    void postFrameCallback(AChoreographer_frameCallback cb, void* data);
+    void postFrameCallbackDelayed(AChoreographer_frameCallback cb, void* data, nsecs_t delay);
+
+    enum {
+        MSG_SCHEDULE_CALLBACKS = 0,
+        MSG_SCHEDULE_VSYNC = 1
+    };
+    virtual void handleMessage(const Message& message) override;
+
+    static Choreographer* getForThread();
+
+protected:
+    virtual ~Choreographer() = default;
+
+private:
+    Choreographer(const sp<Looper>& looper);
+    Choreographer(const Choreographer&) = delete;
+
+    virtual void dispatchVsync(nsecs_t timestamp, int32_t id, uint32_t count);
+    virtual void dispatchHotplug(nsecs_t timestamp, int32_t id, bool connected);
+
+    void scheduleCallbacks();
+
+    // Protected by mLock
+    std::priority_queue<FrameCallback> mCallbacks;
+
+    mutable Mutex mLock;
+
+    const sp<Looper> mLooper;
+    const std::thread::id mThreadId;
+};
+
+
+thread_local Choreographer* gChoreographer;
+Choreographer* Choreographer::getForThread() {
+    if (gChoreographer == nullptr) {
+        sp<Looper> looper = Looper::getForThread();
+        if (!looper.get()) {
+            ALOGW("No looper prepared for thread");
+            return nullptr;
+        }
+        gChoreographer = new Choreographer(looper);
+        status_t result = gChoreographer->initialize();
+        if (result != OK) {
+            ALOGW("Failed to initialize");
+            return nullptr;
+        }
+    }
+    return gChoreographer;
+}
+
+Choreographer::Choreographer(const sp<Looper>& looper) :
+    DisplayEventDispatcher(looper), mLooper(looper), mThreadId(std::this_thread::get_id()) {
+}
+
+void Choreographer::postFrameCallback(AChoreographer_frameCallback cb, void* data) {
+    postFrameCallbackDelayed(cb, data, 0);
+}
+
+void Choreographer::postFrameCallbackDelayed(
+        AChoreographer_frameCallback cb, void* data, nsecs_t delay) {
+    nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
+    FrameCallback callback{cb, data, now + delay};
+    {
+        AutoMutex _l{mLock};
+        mCallbacks.push(callback);
+    }
+    if (callback.dueTime <= now) {
+        if (std::this_thread::get_id() != mThreadId) {
+            Message m{MSG_SCHEDULE_VSYNC};
+            mLooper->sendMessage(this, m);
+        } else {
+            scheduleVsync();
+        }
+    } else {
+        Message m{MSG_SCHEDULE_CALLBACKS};
+        mLooper->sendMessageDelayed(delay, this, m);
+    }
+}
+
+void Choreographer::scheduleCallbacks() {
+    AutoMutex _{mLock};
+    nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
+    if (mCallbacks.top().dueTime <= now) {
+        ALOGV("choreographer %p ~ scheduling vsync", this);
+        scheduleVsync();
+        return;
+    }
+}
+
+
+void Choreographer::dispatchVsync(nsecs_t timestamp, int32_t id, uint32_t) {
+    if (id != ISurfaceComposer::eDisplayIdMain) {
+        ALOGV("choreographer %p ~ ignoring vsync signal for non-main display (id=%d)", this, id);
+        scheduleVsync();
+        return;
+    }
+    std::vector<FrameCallback> callbacks{};
+    {
+        AutoMutex _l{mLock};
+        nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
+        while (!mCallbacks.empty() && mCallbacks.top().dueTime < now) {
+            callbacks.push_back(mCallbacks.top());
+            mCallbacks.pop();
+        }
+    }
+    for (const auto& cb : callbacks) {
+        cb.callback(timestamp, cb.data);
+    }
+}
+
+void Choreographer::dispatchHotplug(nsecs_t, int32_t id, bool connected) {
+    ALOGV("choreographer %p ~ received hotplug event (id=%" PRId32 ", connected=%s), ignoring.",
+            this, id, toString(connected));
+}
+
+void Choreographer::handleMessage(const Message& message) {
+    switch (message.what) {
+    case MSG_SCHEDULE_CALLBACKS:
+        scheduleCallbacks();
+        break;
+    case MSG_SCHEDULE_VSYNC:
+        scheduleVsync();
+        break;
+    }
+}
+
+}
+
+/* Glue for the NDK interface */
+
+using android::Choreographer;
+
+static inline Choreographer* AChoreographer_to_Choreographer(AChoreographer* choreographer) {
+    return reinterpret_cast<Choreographer*>(choreographer);
+}
+
+static inline AChoreographer* Choreographer_to_AChoreographer(Choreographer* choreographer) {
+    return reinterpret_cast<AChoreographer*>(choreographer);
+}
+
+AChoreographer* AChoreographer_getInstance() {
+    return Choreographer_to_AChoreographer(Choreographer::getForThread());
+}
+
+void AChoreographer_postFrameCallback(AChoreographer* choreographer,
+        AChoreographer_frameCallback callback, void* data) {
+    AChoreographer_to_Choreographer(choreographer)->postFrameCallback(callback, data);
+}
+void AChoreographer_postFrameCallbackDelayed(AChoreographer* choreographer,
+        AChoreographer_frameCallback callback, void* data, long delayMillis) {
+    AChoreographer_to_Choreographer(choreographer)->postFrameCallbackDelayed(
+            callback, data, ms2ns(delayMillis));
+}
diff --git a/packages/DocumentsUI/res/values/strings.xml b/packages/DocumentsUI/res/values/strings.xml
index 016657e..05c43b2 100644
--- a/packages/DocumentsUI/res/values/strings.xml
+++ b/packages/DocumentsUI/res/values/strings.xml
@@ -87,7 +87,7 @@
     <!-- Button label that hides the error bar [CHAR LIMIT=24] -->
     <string name="button_dismiss">Dismiss</string>
     <string name="button_retry">Try Again</string>
-    
+
     <!-- Mode that sorts documents by their display name alphabetically [CHAR LIMIT=24] -->
     <string name="sort_name">By name</string>
     <!-- Mode that sorts documents by their last modified time in descending order; most recent first [CHAR LIMIT=24] -->
@@ -158,6 +158,8 @@
     <string name="copy_preparing">Preparing for copy\u2026</string>
     <!-- Text shown on the notification while DocumentsUI performs setup in preparation for moving files [CHAR LIMIT=32] -->
     <string name="move_preparing">Preparing for move\u2026</string>
+    <!-- Text shown on the notification while DocumentsUI performs setup in preparation for deleting files [CHAR LIMIT=32] -->
+    <string name="delete_preparing">Preparing for delete\u2026</string>
     <!-- Title of the copy error notification [CHAR LIMIT=48] -->
     <plurals name="copy_error_notification_title">
         <item quantity="one">Couldn\'t copy <xliff:g id="count" example="1">%1$d</xliff:g> file</item>
@@ -168,6 +170,11 @@
         <item quantity="one">Couldn\'t move <xliff:g id="count" example="1">%1$d</xliff:g> file</item>
         <item quantity="other">Couldn\'t move <xliff:g id="count" example="2">%1$d</xliff:g> files</item>
     </plurals>
+    <!-- Title of the delete error notification [CHAR LIMIT=48] -->
+    <plurals name="delete_error_notification_title">
+        <item quantity="one">Couldn\'t delete <xliff:g id="count" example="1">%1$d</xliff:g> file</item>
+        <item quantity="other">Couldn\'t delete <xliff:g id="count" example="2">%1$d</xliff:g> files</item>
+    </plurals>
     <!-- Second line for notifications saying that more information will be shown after touching [CHAR LIMIT=48] -->
     <string name="notification_touch_for_details">Touch to view details</string>
     <!-- Label of a dialog button for retrying a failed operation [CHAR LIMIT=24] -->
diff --git a/packages/DocumentsUI/src/com/android/documentsui/Events.java b/packages/DocumentsUI/src/com/android/documentsui/Events.java
index 1b5b60de..10a78b9 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/Events.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/Events.java
@@ -111,17 +111,15 @@
 
     public static final class MotionInputEvent implements InputEvent {
         private final MotionEvent mEvent;
-        private final RecyclerView mView;
         private final int mPosition;
 
         public MotionInputEvent(MotionEvent event, RecyclerView view) {
             mEvent = event;
-            mView = view;
 
             // Consider determining position lazily as an optimization.
-            View child = mView.findChildViewUnder(mEvent.getX(), mEvent.getY());
-            mPosition = (child != null)
-                    ? mView.getChildAdapterPosition(child)
+            View child = view.findChildViewUnder(mEvent.getX(), mEvent.getY());
+            mPosition = (child!= null)
+                    ? view.getChildAdapterPosition(child)
                     : RecyclerView.NO_POSITION;
         }
 
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 580e2d8..ba96b0e 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -84,6 +84,7 @@
 import com.android.documentsui.DocumentsActivity;
 import com.android.documentsui.DocumentsApplication;
 import com.android.documentsui.Events;
+import com.android.documentsui.Events.MotionInputEvent;
 import com.android.documentsui.Menus;
 import com.android.documentsui.MessageBar;
 import com.android.documentsui.MimePredicate;
@@ -124,11 +125,13 @@
 
     public static final int REQUEST_COPY_DESTINATION = 1;
 
-    private static final String TAG = "DirectoryFragment";
-
-    private static final int LOADER_ID = 42;
     static final boolean DEBUG_ENABLE_DND = true;
 
+    private static final String TAG = "DirectoryFragment";
+    private static final int LOADER_ID = 42;
+    private static final int DELETE_UNDO_TIMEOUT = 5000;
+    private static final int DELETE_JOB_DELAY = 5500;
+
     private static final String EXTRA_TYPE = "type";
     private static final String EXTRA_ROOT = "root";
     private static final String EXTRA_DOC = "doc";
@@ -138,12 +141,13 @@
     private Model mModel;
     private MultiSelectManager mSelectionManager;
     private Model.UpdateListener mModelUpdateListener = new ModelUpdateListener();
-    private ItemClickListener mItemClickListener = new ItemClickListener();
+    private ItemEventListener mItemEventListener = new ItemEventListener();
 
     private IconHelper mIconHelper;
 
     private View mEmptyView;
     private RecyclerView mRecView;
+    private ListeningGestureDetector mGestureDetector;
 
     private int mType = TYPE_NORMAL;
     private String mStateKey;
@@ -297,36 +301,9 @@
 
         mRecView.setAdapter(mAdapter);
 
-        GestureDetector.SimpleOnGestureListener listener =
-                new GestureDetector.SimpleOnGestureListener() {
-                    @Override
-                    public boolean onSingleTapUp(MotionEvent e) {
-                        return DirectoryFragment.this.onSingleTapUp(e);
-                    }
-                    @Override
-                    public boolean onDoubleTap(MotionEvent e) {
-                        Log.d(TAG, "Handling double tap.");
-                        return DirectoryFragment.this.onDoubleTap(e);
-                    }
-                };
+        mGestureDetector = new ListeningGestureDetector(this.getContext(), new GestureListener());
 
-        final GestureDetector detector = new GestureDetector(this.getContext(), listener);
-        detector.setOnDoubleTapListener(listener);
-
-        mRecView.addOnItemTouchListener(
-                new OnItemTouchListener() {
-                    @Override
-                    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
-                        detector.onTouchEvent(e);
-                        return false;
-                    }
-
-                    @Override
-                    public void onTouchEvent(RecyclerView rv, MotionEvent e) {}
-
-                    @Override
-                    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
-                });
+        mRecView.addOnItemTouchListener(mGestureDetector);
 
         // TODO: instead of inserting the view into the constructor, extract listener-creation code
         // and set the listener on the view after the fact.  Then the view doesn't need to be passed
@@ -339,7 +316,7 @@
                     : MultiSelectManager.MODE_SINGLE);
         mSelectionManager.addCallback(new SelectionModeListener());
 
-        mModel = new Model(context);
+        mModel = new Model();
         mModel.addUpdateListener(mAdapter);
         mModel.addUpdateListener(mModelUpdateListener);
 
@@ -466,22 +443,8 @@
                 operationType);
     }
 
-    private boolean onSingleTapUp(MotionEvent e) {
-        // Only respond to touch events.  Single-click mouse events are selection events and are
-        // handled by the selection manager.  Tap events that occur while the selection manager is
-        // active are also selection events.
-        if (Events.isTouchEvent(e) && !mSelectionManager.hasSelection()) {
-            String id = getModelId(e);
-            if (id != null) {
-                return handleViewItem(id);
-            }
-        }
-        return false;
-    }
-
     protected boolean onDoubleTap(MotionEvent e) {
         if (Events.isMouseEvent(e)) {
-            Log.d(TAG, "Handling double tap from mouse.");
             String id = getModelId(e);
             if (id != null) {
                 return handleViewItem(id);
@@ -852,16 +815,32 @@
     }
 
     private void deleteDocuments(final Selection selected) {
-        Context context = getActivity();
-        String message = Shared.getQuantityString(context, R.plurals.deleting, selected.size());
 
-        // Hide the files in the UI.
-        final SparseArray<String> toDelete = mAdapter.hide(selected.getAll());
+            checkArgument(!selected.isEmpty());
+            new GetDocumentsTask() {
+                @Override
+                void onDocumentsReady(List<DocumentInfo> docs) {
+                    // Hide the files in the UI.
+                    final SparseArray<String> hidden = mAdapter.hide(selected.getAll());
+
+                    checkState(DELETE_JOB_DELAY > DELETE_UNDO_TIMEOUT);
+                    String operationId = FileOperations.delete(
+                            getActivity(), docs, getDisplayState().stack,
+                            DELETE_JOB_DELAY);
+                    showDeleteSnackbar(hidden, operationId);
+                }
+            }.execute(selected);
+    }
+
+    private void showDeleteSnackbar(final SparseArray<String> hidden, final String jobId) {
+
+        Context context = getActivity();
+        String message = Shared.getQuantityString(context, R.plurals.deleting, hidden.size());
 
         // Show a snackbar informing the user that files will be deleted, and give them an option to
         // cancel.
         final Activity activity = getActivity();
-        Snackbars.makeSnackbar(activity, message, Snackbar.LENGTH_LONG)
+        Snackbars.makeSnackbar(activity, message, DELETE_UNDO_TIMEOUT)
                 .setAction(
                         R.string.undo,
                         new View.OnClickListener() {
@@ -874,22 +853,8 @@
                             public void onDismissed(Snackbar snackbar, int event) {
                                 if (event == Snackbar.Callback.DISMISS_EVENT_ACTION) {
                                     // If the delete was cancelled, just unhide the files.
-                                    mAdapter.unhide(toDelete);
-                                } else {
-                                    // Actually kick off the delete.
-                                    mModel.delete(
-                                            selected,
-                                            new Model.DeletionListener() {
-                                                @Override
-                                                  public void onError() {
-                                                      Snackbars.makeSnackbar(
-                                                              activity,
-                                                              R.string.toast_failed_delete,
-                                                              Snackbar.LENGTH_LONG)
-                                                              .show();
-
-                                                  }
-                                            });
+                                    FileOperations.cancel(activity, jobId);
+                                    mAdapter.unhide(hidden);
                                 }
                             }
                         })
@@ -926,7 +891,7 @@
 
     @Override
     public void initDocumentHolder(DocumentHolder holder) {
-        holder.addClickListener(mItemClickListener);
+        holder.addEventListener(mItemEventListener);
         holder.addOnKeyListener(mSelectionManager);
     }
 
@@ -1150,11 +1115,7 @@
             view.setOnDragListener(mOnDragListener);
         }
 
-        // Temporary: attaching the listener to the title only.
-        // Attaching to the entire item conflicts with the item long click handler responsible
-        // for item selection.
-        final View title = view.findViewById(android.R.id.title);
-        title.setOnLongClickListener(mLongClickListener);
+        view.setOnLongClickListener(mLongClickListener);
     }
 
     private View.OnDragListener mOnDragListener = new View.OnDragListener() {
@@ -1229,24 +1190,6 @@
         }
     }
 
-    private View.OnLongClickListener mLongClickListener = new View.OnLongClickListener() {
-        @Override
-        public boolean onLongClick(View v) {
-            final List<DocumentInfo> docs = getDraggableDocuments(v);
-            if (docs.isEmpty()) {
-                return false;
-            }
-            v.startDrag(
-                    getClipDataFromDocuments(docs),
-                    new DrawableShadowBuilder(getDragShadowIcon(docs)),
-                    null,
-                    View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ |
-                            View.DRAG_FLAG_GLOBAL_URI_WRITE
-            );
-            return true;
-        }
-    };
-
     private List<DocumentInfo> getDraggableDocuments(View currentItemView) {
         String modelId = getModelId(currentItemView);
         if (modelId == null) {
@@ -1330,15 +1273,18 @@
         return mSelectionManager.getSelection().contains(modelId);
     }
 
-    private class ItemClickListener implements DocumentHolder.ClickListener {
+    private class ItemEventListener implements DocumentHolder.EventListener {
         @Override
-        public void onClick(DocumentHolder doc) {
-            if (mSelectionManager.hasSelection()) {
-                mSelectionManager.toggleSelection(doc.modelId);
-                mSelectionManager.setSelectionRangeBegin(doc.getAdapterPosition());
-            } else {
-                handleViewItem(doc.modelId);
-            }
+        public boolean onActivate(DocumentHolder doc) {
+            handleViewItem(doc.modelId);
+            return true;
+        }
+
+        @Override
+        public boolean onSelect(DocumentHolder doc) {
+            mSelectionManager.toggleSelection(doc.modelId);
+            mSelectionManager.setSelectionRangeBegin(doc.getAdapterPosition());
+            return true;
         }
     }
 
@@ -1366,4 +1312,111 @@
             showErrorView();
         }
     }
+
+    private View.OnLongClickListener mLongClickListener = new View.OnLongClickListener() {
+        @Override
+        public boolean onLongClick(View v) {
+            if (mGestureDetector.mouseSpawnedLastEvent()) {
+                List<DocumentInfo> docs = getDraggableDocuments(v);
+                if (docs.isEmpty()) {
+                    return false;
+                }
+                v.startDrag(
+                        getClipDataFromDocuments(docs),
+                        new DrawableShadowBuilder(getDragShadowIcon(docs)),
+                        null,
+                        View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ |
+                                View.DRAG_FLAG_GLOBAL_URI_WRITE
+                );
+                return true;
+            }
+
+            return false;
+        }
+    };
+
+    // Previously we listened to events with one class, only to bounce them forward
+    // to GestureDetector. We're still doing that here, but with a single class
+    // that reduces overall complexity in our glue code.
+    private static final class ListeningGestureDetector extends GestureDetector
+            implements OnItemTouchListener {
+
+        private int mLastTool = -1;
+
+        public ListeningGestureDetector(Context context, GestureListener listener) {
+            super(context, listener);
+            setOnDoubleTapListener(listener);
+        }
+
+        boolean mouseSpawnedLastEvent() {
+            return Events.isMouseType(mLastTool);
+        }
+
+        boolean touchSpawnedLastEvent() {
+            return Events.isTouchType(mLastTool);
+        }
+
+        @Override
+        public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
+            mLastTool = e.getToolType(0);
+            onTouchEvent(e);  // bounce this forward to our detecty heart
+            return false;
+        }
+
+        @Override
+        public void onTouchEvent(RecyclerView rv, MotionEvent e) {}
+
+        @Override
+        public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
+    }
+
+    /**
+     * The gesture listener for items in the list/grid view. Interprets gestures and sends the
+     * events to the target DocumentHolder, whence they are routed to the appropriate listener.
+     */
+    private class GestureListener extends GestureDetector.SimpleOnGestureListener {
+        @Override
+        public boolean onSingleTapUp(MotionEvent e) {
+            // Single tap logic:
+            // If the selection manager is active, it gets first whack at handling tap
+            // events. Otherwise, tap events are routed to the target DocumentHolder.
+            boolean handled = mSelectionManager.onSingleTapUp(
+                        new MotionInputEvent(e, mRecView));
+
+            if (handled) {
+                return handled;
+            }
+
+            // Give the DocumentHolder a crack at the event.
+            DocumentHolder holder = getTarget(e);
+            if (holder != null) {
+                handled = holder.onSingleTapUp(e);
+            }
+
+            return handled;
+        }
+
+        @Override
+        public void onLongPress(MotionEvent e) {
+            // Long-press events get routed directly to the selection manager. They can be
+            // changed to route through the DocumentHolder if necessary.
+            mSelectionManager.onLongPress(new MotionInputEvent(e, mRecView));
+        }
+
+        @Override
+        public boolean onDoubleTap(MotionEvent e) {
+            // Double-tap events are handled directly by the DirectoryFragment. They can be changed
+            // to route through the DocumentHolder if necessary.
+            return DirectoryFragment.this.onDoubleTap(e);
+        }
+
+        private @Nullable DocumentHolder getTarget(MotionEvent e) {
+            View childView = mRecView.findChildViewUnder(e.getX(), e.getY());
+            if (childView != null) {
+                return (DocumentHolder) mRecView.getChildViewHolder(childView);
+            } else {
+                return null;
+            }
+        }
+    }
 }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DocumentHolder.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DocumentHolder.java
index 9ac9057..8acf1af 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DocumentHolder.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DocumentHolder.java
@@ -16,17 +16,21 @@
 
 package com.android.documentsui.dirlist;
 
+import static com.android.internal.util.Preconditions.checkNotNull;
 import static com.android.internal.util.Preconditions.checkState;
 
 import android.content.Context;
 import android.database.Cursor;
+import android.graphics.Rect;
 import android.support.annotation.Nullable;
 import android.support.v7.widget.RecyclerView;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
 
+import com.android.documentsui.Events;
 import com.android.documentsui.R;
 import com.android.documentsui.State;
 
@@ -41,8 +45,9 @@
     final boolean mAlwaysShowSummary;
     final Context mContext;
 
-    private ListDocumentHolder.ClickListener mClickListener;
+    DocumentHolder.EventListener mEventListener;
     private View.OnKeyListener mKeyListener;
+    private View mSelectionHotspot;
 
     public DocumentHolder(Context context, ViewGroup parent, int layout) {
         this(context, inflateLayout(context, parent, layout));
@@ -58,6 +63,8 @@
         mDefaultItemColor = context.getColor(R.color.item_doc_background);
         mSelectedItemColor = context.getColor(R.color.item_doc_background_selected);
         mAlwaysShowSummary = context.getResources().getBoolean(R.bool.always_show_summary);
+
+        mSelectionHotspot = itemView.findViewById(R.id.icon_check);
     }
 
     /**
@@ -75,23 +82,21 @@
 
     @Override
     public boolean onKey(View v, int keyCode, KeyEvent event) {
+        // Event listener should always be set.
+        checkNotNull(mEventListener);
         // Intercept enter key-up events, and treat them as clicks.  Forward other events.
-        if (event.getAction() == KeyEvent.ACTION_UP &&
-                keyCode == KeyEvent.KEYCODE_ENTER) {
-            if (mClickListener != null) {
-                mClickListener.onClick(this);
-            }
-            return true;
+        if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_ENTER) {
+            return mEventListener.onActivate(this);
         } else if (mKeyListener != null) {
             return mKeyListener.onKey(v, keyCode, event);
         }
         return false;
     }
 
-    public void addClickListener(ListDocumentHolder.ClickListener listener) {
+    public void addEventListener(DocumentHolder.EventListener listener) {
         // Just handle one for now; switch to a list if necessary.
-        checkState(mClickListener == null);
-        mClickListener = listener;
+        checkState(mEventListener == null);
+        mEventListener = listener;
     }
 
     public void addOnKeyListener(View.OnKeyListener listener) {
@@ -104,6 +109,33 @@
         setEnabledRecursive(itemView, enabled);
     }
 
+    public boolean onSingleTapUp(MotionEvent event) {
+        if (Events.isMouseEvent(event)) {
+            // Mouse clicks select.
+            // TODO:  && input.isPrimaryButtonPressed(), but it is returning false.
+            if (mEventListener != null) {
+                return mEventListener.onSelect(this);
+            }
+        } else if (Events.isTouchEvent(event)) {
+            // Touch events select if they occur in the selection hotspot, otherwise they activate.
+            if (mEventListener == null) {
+                return false;
+            }
+
+            // Do everything in global coordinates - it makes things simpler.
+            Rect rect = new Rect();
+            mSelectionHotspot.getGlobalVisibleRect(rect);
+
+            // If the tap occurred within the icon rect, consider it a selection.
+            if (rect.contains((int)event.getRawX(), (int)event.getRawY())) {
+                return mEventListener.onSelect(this);
+            } else {
+                return mEventListener.onActivate(this);
+            }
+        }
+        return false;
+    }
+
     static void setEnabledRecursive(View itemView, boolean enabled) {
         if (itemView == null) return;
         if (itemView.isEnabled() == enabled) return;
@@ -122,7 +154,20 @@
         return inflater.inflate(layout, parent, false);
     }
 
-    interface ClickListener {
-        public void onClick(DocumentHolder doc);
+    /**
+     * Implement this in order to be able to respond to events coming from DocumentHolders.
+     */
+    interface EventListener {
+        /**
+         * @param doc The target DocumentHolder
+         * @return Whether the event was handled.
+         */
+        public boolean onActivate(DocumentHolder doc);
+
+        /**
+         * @param doc The target DocumentHolder
+         * @return Whether the event was handled.
+         */
+        public boolean onSelect(DocumentHolder doc);
     }
 }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/Model.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/Model.java
index cf21d15..075b3ea 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/Model.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/Model.java
@@ -24,11 +24,7 @@
 import static com.android.documentsui.model.DocumentInfo.getCursorString;
 import static com.android.internal.util.Preconditions.checkNotNull;
 
-import android.content.ContentProviderClient;
-import android.content.ContentResolver;
-import android.content.Context;
 import android.database.Cursor;
-import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.Looper;
 import android.provider.DocumentsContract;
@@ -39,7 +35,6 @@
 
 import com.android.documentsui.BaseActivity.SiblingProvider;
 import com.android.documentsui.DirectoryResult;
-import com.android.documentsui.DocumentsApplication;
 import com.android.documentsui.RootCursorWrapper;
 import com.android.documentsui.dirlist.MultiSelectManager.Selection;
 import com.android.documentsui.model.DocumentInfo;
@@ -56,7 +51,6 @@
 public class Model implements SiblingProvider {
     private static final String TAG = "Model";
 
-    private Context mContext;
     private boolean mIsLoading;
     private List<UpdateListener> mUpdateListeners = new ArrayList<>();
     @Nullable private Cursor mCursor;
@@ -73,10 +67,6 @@
     @Nullable String info;
     @Nullable String error;
 
-    Model(Context context) {
-        mContext = context;
-    }
-
     /**
      * Generates a Model ID for a cursor entry that refers to a document. The Model ID is a unique
      * string that can be used to identify the document referred to by the cursor.
@@ -406,91 +396,6 @@
         return mCursor;
     }
 
-    public void delete(Selection selected, DeletionListener listener) {
-        final ContentResolver resolver = mContext.getContentResolver();
-        new DeleteFilesTask(resolver, listener).execute(selected);
-    }
-
-    /**
-     * A Task which collects the DocumentInfo for documents that have been marked for deletion,
-     * and actually deletes them.
-     */
-    private class DeleteFilesTask extends AsyncTask<Selection, Void, Void> {
-        private ContentResolver mResolver;
-        private DeletionListener mListener;
-        private boolean mHadTrouble;
-
-        /**
-         * @param resolver A ContentResolver for performing the actual file deletions.
-         * @param errorCallback A Runnable that is executed in the event that one or more errors
-         *     occurred while copying files.  Execution will occur on the UI thread.
-         */
-        public DeleteFilesTask(ContentResolver resolver, DeletionListener listener) {
-            mResolver = resolver;
-            mListener = listener;
-        }
-
-        @Override
-        protected Void doInBackground(Selection... selected) {
-            List<DocumentInfo> toDelete = null;
-            try {
-                toDelete = getDocuments(selected[0]);
-            } catch (NullPointerException e) {
-                Log.w(TAG, "Failed to retrieve documents for delete.");
-                mHadTrouble = true;
-                return null;
-            }
-
-            for (DocumentInfo doc : toDelete) {
-                if (!doc.isDeleteSupported()) {
-                    Log.w(TAG, doc + " could not be deleted.  Skipping...");
-                    mHadTrouble = true;
-                    continue;
-                }
-
-                ContentProviderClient client = null;
-                try {
-                    if (DEBUG) Log.d(TAG, "Deleting: " + doc.displayName);
-                    client = DocumentsApplication.acquireUnstableProviderOrThrow(
-                        mResolver, doc.derivedUri.getAuthority());
-                    DocumentsContract.deleteDocument(client, doc.derivedUri);
-                } catch (Exception e) {
-                    Log.w(TAG, "Failed to delete " + doc, e);
-                    mHadTrouble = true;
-                } finally {
-                    ContentProviderClient.releaseQuietly(client);
-                }
-            }
-
-            return null;
-        }
-
-        @Override
-        protected void onPostExecute(Void _) {
-            if (mHadTrouble) {
-                // TODO show which files failed? b/23720103
-                mListener.onError();
-                if (DEBUG) Log.d(TAG, "Deletion task completed.  Some deletions failed.");
-            } else {
-                if (DEBUG) Log.d(TAG, "Deletion task completed successfully.");
-            }
-
-            mListener.onCompletion();
-        }
-    }
-
-    static class DeletionListener {
-        /**
-         * Called when deletion has completed (regardless of whether an error occurred).
-         */
-        void onCompletion() {}
-
-        /**
-         * Called at the end of a deletion operation that produced one or more errors.
-         */
-        void onError() {}
-    }
-
     void addUpdateListener(UpdateListener listener) {
         mUpdateListeners.add(listener);
     }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java
index d868fb4..71e87cb 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java
@@ -17,6 +17,8 @@
 package com.android.documentsui.dirlist;
 
 import static com.android.documentsui.Shared.DEBUG;
+import static com.android.documentsui.dirlist.ModelBackedDocumentsAdapter.ITEM_TYPE_DIRECTORY;
+import static com.android.documentsui.dirlist.ModelBackedDocumentsAdapter.ITEM_TYPE_DOCUMENT;
 import static com.android.internal.util.Preconditions.checkArgument;
 import static com.android.internal.util.Preconditions.checkNotNull;
 import static com.android.internal.util.Preconditions.checkState;
@@ -32,7 +34,6 @@
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 import android.util.SparseIntArray;
-import android.view.GestureDetector;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
@@ -90,29 +91,10 @@
             mBandManager = new BandController();
         }
 
-        GestureDetector.SimpleOnGestureListener listener =
-                new GestureDetector.SimpleOnGestureListener() {
-                    @Override
-                    public boolean onSingleTapUp(MotionEvent e) {
-                        return MultiSelectManager.this.onSingleTapUp(
-                                new MotionInputEvent(e, recyclerView));
-                    }
-                    @Override
-                    public void onLongPress(MotionEvent e) {
-                        MultiSelectManager.this.onLongPress(
-                                new MotionInputEvent(e, recyclerView));
-                    }
-                };
-
-        final GestureDetector detector = new GestureDetector(recyclerView.getContext(), listener);
-        detector.setOnDoubleTapListener(listener);
-
         recyclerView.addOnItemTouchListener(
                 new RecyclerView.OnItemTouchListener() {
                     @Override
                     public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
-                        detector.onTouchEvent(e);
-
                         if (mBandManager != null) {
                             return mBandManager.handleEvent(new MotionInputEvent(e, recyclerView));
                         }
@@ -287,13 +269,7 @@
     boolean onSingleTapUp(InputEvent input) {
         if (DEBUG) Log.d(TAG, "Processing tap event.");
         if (!hasSelection()) {
-            // if this is a mouse click on an item, start selection mode.
-            // TODO:  && input.isPrimaryButtonPressed(), but it is returning false.
-            if (input.isOverItem() && input.isMouseEvent()) {
-                int position = input.getItemPosition();
-                toggleSelection(position);
-                setSelectionRangeBegin(position);
-            }
+            // No selection active - do nothing.
             return false;
         }
 
@@ -814,6 +790,10 @@
         int getChildCount();
         int getVisibleChildCount();
         void focusItem(int position);
+        /**
+         * Layout items are excluded from the GridModel.
+         */
+        boolean isLayoutItem(int adapterPosition);
     }
 
     /** Recycler view facade implementation backed by good ol' RecyclerView. */
@@ -970,6 +950,20 @@
                     });
             }
         }
+
+        @Override
+        public boolean isLayoutItem(int pos) {
+            // The band selection model only operates on documents and directories. Exclude other
+            // types of adapter items (e.g. whitespace items like dividers).
+            RecyclerView.ViewHolder vh = mView.findViewHolderForAdapterPosition(pos);
+            switch (vh.getItemViewType()) {
+                case ITEM_TYPE_DOCUMENT:
+                case ITEM_TYPE_DIRECTORY:
+                    return false;
+                default:
+                    return true;
+            }
+        }
     }
 
     public interface Callback {
@@ -1433,7 +1427,8 @@
         private void recordVisibleChildren() {
             for (int i = 0; i < mHelper.getVisibleChildCount(); i++) {
                 int adapterPosition = mHelper.getAdapterPositionAt(i);
-                if (!mKnownPositions.get(adapterPosition)) {
+                if (!mHelper.isLayoutItem(adapterPosition) &&
+                        !mKnownPositions.get(adapterPosition)) {
                     mKnownPositions.put(adapterPosition, true);
                     recordItemData(mHelper.getAbsoluteRectForChildViewAt(i), adapterPosition);
                 }
@@ -1519,31 +1514,29 @@
          * @param rect Rectangle including all covered items.
          */
         private void updateSelection(Rect rect) {
-            int columnStartIndex =
+            int columnStart =
                     Collections.binarySearch(mColumnBounds, new Limits(rect.left, rect.left));
-            checkState(columnStartIndex >= 0);
-            int columnEndIndex = columnStartIndex;
+            checkState(columnStart >= 0);
+            int columnEnd = columnStart;
 
-            for (int i = columnStartIndex; i < mColumnBounds.size()
+            for (int i = columnStart; i < mColumnBounds.size()
                     && mColumnBounds.get(i).lowerLimit <= rect.right; i++) {
-                columnEndIndex = i;
+                columnEnd = i;
             }
 
-            SparseIntArray firstColumn =
-                    mColumns.get(mColumnBounds.get(columnStartIndex).lowerLimit);
-            int rowStartIndex = firstColumn.indexOfKey(rect.top);
-            if (rowStartIndex < 0) {
+            int rowStart = Collections.binarySearch(mRowBounds, new Limits(rect.top, rect.top));
+            if (rowStart < 0) {
                 mPositionNearestOrigin = NOT_SET;
                 return;
             }
 
-            int rowEndIndex = rowStartIndex;
-            for (int i = rowStartIndex;
-                    i < firstColumn.size() && firstColumn.keyAt(i) <= rect.bottom; i++) {
-                rowEndIndex = i;
+            int rowEnd = rowStart;
+            for (int i = rowStart; i < mRowBounds.size()
+                    && mRowBounds.get(i).lowerLimit <= rect.bottom; i++) {
+                rowEnd = i;
             }
 
-            updateSelection(columnStartIndex, columnEndIndex, rowStartIndex, rowEndIndex);
+            updateSelection(columnStart, columnEnd, rowStart, rowEnd);
         }
 
         /**
@@ -1552,13 +1545,17 @@
          */
         private void updateSelection(
                 int columnStartIndex, int columnEndIndex, int rowStartIndex, int rowEndIndex) {
+            if (DEBUG) Log.d(TAG, String.format("updateSelection: %d, %d, %d, %d",
+                    columnStartIndex, columnEndIndex, rowStartIndex, rowEndIndex));
+
             mSelection.clear();
             for (int column = columnStartIndex; column <= columnEndIndex; column++) {
                 SparseIntArray items = mColumns.get(mColumnBounds.get(column).lowerLimit);
                 for (int row = rowStartIndex; row <= rowEndIndex; row++) {
                     // The default return value for SparseIntArray.get is 0, which is a valid
                     // position. Use a sentry value to prevent erroneously selecting item 0.
-                    int position = items.get(items.keyAt(row), NOT_SET);
+                    final int rowKey = mRowBounds.get(row).lowerLimit;
+                    int position = items.get(rowKey, NOT_SET);
                     if (position != NOT_SET) {
                         String id = mAdapter.getModelId(position);
                         if (id != null) {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java b/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java
index b1932b8..f3195a7 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java
@@ -21,7 +21,6 @@
 import static android.provider.DocumentsContract.buildDocumentUri;
 import static android.provider.DocumentsContract.getDocumentId;
 import static android.provider.DocumentsContract.isChildDocument;
-import static com.android.documentsui.DocumentsApplication.acquireUnstableProviderOrThrow;
 import static com.android.documentsui.Shared.DEBUG;
 import static com.android.documentsui.model.DocumentInfo.getCursorLong;
 import static com.android.documentsui.model.DocumentInfo.getCursorString;
@@ -62,12 +61,7 @@
 class CopyJob extends Job {
     private static final String TAG = "CopyJob";
     private static final int PROGRESS_INTERVAL_MILLIS = 1000;
-    final List<DocumentInfo> mSrcFiles;
-
-    // Provider clients are acquired for the duration of each copy job. Note that there is an
-    // implicit assumption that all srcs come from the same authority.
-    ContentProviderClient srcClient;
-    ContentProviderClient dstClient;
+    final List<DocumentInfo> mSrcs;
 
     private long mStartTime = -1;
     private long mBatchSize;
@@ -86,11 +80,11 @@
      * @param srcs List of files to be copied.
      */
     CopyJob(Context service, Context appContext, Listener listener,
-            String id, DocumentStack destination, List<DocumentInfo> srcs) {
-        super(service, appContext, listener, OPERATION_COPY, id, destination);
+            String id, DocumentStack stack, List<DocumentInfo> srcs) {
+        super(service, appContext, listener, OPERATION_COPY, id, stack);
 
         checkArgument(!srcs.isEmpty());
-        this.mSrcFiles = srcs;
+        this.mSrcs = srcs;
     }
 
     /**
@@ -103,7 +97,7 @@
         super(service, appContext, listener, opType, id, destination);
 
         checkArgument(!srcs.isEmpty());
-        this.mSrcFiles = srcs;
+        this.mSrcs = srcs;
     }
 
     @Override
@@ -185,21 +179,13 @@
     void start() throws RemoteException {
         mStartTime = elapsedRealtime();
 
-        // Acquire content providers.
-        srcClient = acquireUnstableProviderOrThrow(
-                getContentResolver(),
-                mSrcFiles.get(0).authority);
-        dstClient = acquireUnstableProviderOrThrow(
-                getContentResolver(),
-                stack.peek().authority);
-
         // client
-        mBatchSize = calculateSize(srcClient, mSrcFiles);
+        mBatchSize = calculateSize(mSrcs);
 
         DocumentInfo srcInfo;
         DocumentInfo dstInfo;
-        for (int i = 0; i < mSrcFiles.size() && !isCanceled(); ++i) {
-            srcInfo = mSrcFiles.get(i);
+        for (int i = 0; i < mSrcs.size() && !isCanceled(); ++i) {
+            srcInfo = mSrcs.get(i);
             dstInfo = stack.peek();
 
             // Guard unsupported recursive operation.
@@ -233,24 +219,24 @@
     /**
      * Copies a the given document to the given location.
      *
-     * @param srcInfo DocumentInfos for the documents to copy.
+     * @param src DocumentInfos for the documents to copy.
      * @param dstDirInfo The destination directory.
      * @return True on success, false on failure.
      * @throws RemoteException
      */
-    boolean processDocument(DocumentInfo srcInfo, DocumentInfo dstDirInfo) throws RemoteException {
+    boolean processDocument(DocumentInfo src, DocumentInfo dstDirInfo) throws RemoteException {
 
         // TODO: When optimized copy kicks in, we'll not making any progress updates.
         // For now. Local storage isn't using optimized copy.
 
         // When copying within the same provider, try to use optimized copying.
         // If not supported, then fallback to byte-by-byte copy/move.
-        if (srcInfo.authority.equals(dstDirInfo.authority)) {
-            if ((srcInfo.flags & Document.FLAG_SUPPORTS_COPY) != 0) {
-                if (DocumentsContract.copyDocument(srcClient, srcInfo.derivedUri,
+        if (src.authority.equals(dstDirInfo.authority)) {
+            if ((src.flags & Document.FLAG_SUPPORTS_COPY) != 0) {
+                if (DocumentsContract.copyDocument(getClient(src), src.derivedUri,
                         dstDirInfo.derivedUri) == null) {
-                    onFileFailed(srcInfo,
-                            "Provider side copy failed for documents: " + srcInfo.derivedUri + ".");
+                    onFileFailed(src,
+                            "Provider side copy failed for documents: " + src.derivedUri + ".");
                     return false;
                 }
                 return true;
@@ -258,44 +244,44 @@
         }
 
         // If we couldn't do an optimized copy...we fall back to vanilla byte copy.
-        return byteCopyDocument(srcInfo, dstDirInfo);
+        return byteCopyDocument(src, dstDirInfo);
     }
 
-    boolean byteCopyDocument(DocumentInfo srcInfo, DocumentInfo dstDirInfo)
+    boolean byteCopyDocument(DocumentInfo src, DocumentInfo dest)
             throws RemoteException {
         final String dstMimeType;
         final String dstDisplayName;
 
-        if (DEBUG) Log.d(TAG, "Doing byte copy of document: " + srcInfo);
+        if (DEBUG) Log.d(TAG, "Doing byte copy of document: " + src);
         // If the file is virtual, but can be converted to another format, then try to copy it
         // as such format. Also, append an extension for the target mime type (if known).
-        if (srcInfo.isVirtualDocument()) {
+        if (src.isVirtualDocument()) {
             final String[] streamTypes = getContentResolver().getStreamTypes(
-                    srcInfo.derivedUri, "*/*");
+                    src.derivedUri, "*/*");
             if (streamTypes != null && streamTypes.length > 0) {
                 dstMimeType = streamTypes[0];
                 final String extension = MimeTypeMap.getSingleton().
                         getExtensionFromMimeType(dstMimeType);
-                dstDisplayName = srcInfo.displayName +
-                        (extension != null ? "." + extension : srcInfo.displayName);
+                dstDisplayName = src.displayName +
+                        (extension != null ? "." + extension : src.displayName);
             } else {
-                onFileFailed(srcInfo, "Cannot copy virtual file. No streamable formats available.");
+                onFileFailed(src, "Cannot copy virtual file. No streamable formats available.");
                 return false;
             }
         } else {
-            dstMimeType = srcInfo.mimeType;
-            dstDisplayName = srcInfo.displayName;
+            dstMimeType = src.mimeType;
+            dstDisplayName = src.displayName;
         }
 
         // Create the target document (either a file or a directory), then copy recursively the
         // contents (bytes or children).
-        final Uri dstUri = DocumentsContract.createDocument(dstClient,
-                dstDirInfo.derivedUri, dstMimeType, dstDisplayName);
+        final Uri dstUri = DocumentsContract.createDocument(
+                getClient(dest), dest.derivedUri, dstMimeType, dstDisplayName);
         if (dstUri == null) {
             // If this is a directory, the entire subdir will not be copied over.
-            onFileFailed(srcInfo,
+            onFileFailed(src,
                     "Couldn't create destination document " + dstDisplayName
-                    + " in directory " + dstDirInfo.displayName + ".");
+                    + " in directory " + dest.displayName + ".");
             return false;
         }
 
@@ -303,16 +289,16 @@
         try {
             dstInfo = DocumentInfo.fromUri(getContentResolver(), dstUri);
         } catch (FileNotFoundException e) {
-            onFileFailed(srcInfo,
+            onFileFailed(src,
                     "Could not load DocumentInfo for newly created file: " + dstUri + ".");
             return false;
         }
 
         final boolean success;
-        if (Document.MIME_TYPE_DIR.equals(srcInfo.mimeType)) {
-            success = copyDirectoryHelper(srcInfo, dstInfo);
+        if (Document.MIME_TYPE_DIR.equals(src.mimeType)) {
+            success = copyDirectoryHelper(src, dstInfo);
         } else {
-            success = copyFileHelper(srcInfo, dstInfo, dstMimeType);
+            success = copyFileHelper(src, dstInfo, dstMimeType);
         }
 
         return success;
@@ -322,13 +308,13 @@
      * Handles recursion into a directory and copying its contents. Note that in linux terms, this
      * does the equivalent of "cp src/* dst", not "cp -r src dst".
      *
-     * @param srcDirInfo Info of the directory to copy from. The routine will copy the directory's
+     * @param srcDir Info of the directory to copy from. The routine will copy the directory's
      *            contents, not the directory itself.
-     * @param dstDirInfo Info of the directory to copy to. Must be created beforehand.
+     * @param destDir Info of the directory to copy to. Must be created beforehand.
      * @return True on success, false if some of the children failed to copy.
      * @throws RemoteException
      */
-    private boolean copyDirectoryHelper(DocumentInfo srcDirInfo, DocumentInfo dstDirInfo)
+    private boolean copyDirectoryHelper(DocumentInfo srcDir, DocumentInfo destDir)
             throws RemoteException {
         // Recurse into directories. Copy children into the new subdirectory.
         final String queryColumns[] = new String[] {
@@ -342,13 +328,11 @@
         boolean success = true;
         try {
             // Iterate over srcs in the directory; copy to the destination directory.
-            final Uri queryUri = DocumentsContract.buildChildDocumentsUri(srcDirInfo.authority,
-                    srcDirInfo.documentId);
-            cursor = srcClient.query(queryUri, queryColumns, null, null, null);
-            DocumentInfo srcInfo;
+            final Uri queryUri = buildChildDocumentsUri(srcDir.authority, srcDir.documentId);
+            cursor = getClient(srcDir).query(queryUri, queryColumns, null, null, null);
             while (cursor.moveToNext() && !isCanceled()) {
-                srcInfo = DocumentInfo.fromCursor(cursor, srcDirInfo.authority);
-                success &= processDocument(srcInfo, dstDirInfo);
+                DocumentInfo src = DocumentInfo.fromCursor(cursor, srcDir.authority);
+                success &= processDocument(src, destDir);
             }
         } finally {
             IoUtils.closeQuietly(cursor);
@@ -366,50 +350,49 @@
      * @return True on success, false on error.
      * @throws RemoteException
      */
-    private boolean copyFileHelper(DocumentInfo srcInfo, DocumentInfo dstInfo, String mimeType)
+    private boolean copyFileHelper(DocumentInfo src, DocumentInfo dest, String mimeType)
             throws RemoteException {
-        // Copy an individual file.
         CancellationSignal canceller = new CancellationSignal();
         ParcelFileDescriptor srcFile = null;
         ParcelFileDescriptor dstFile = null;
-        InputStream src = null;
-        OutputStream dst = null;
+        InputStream in = null;
+        OutputStream out = null;
 
         boolean success = true;
         try {
             // If the file is virtual, but can be converted to another format, then try to copy it
             // as such format.
-            if (srcInfo.isVirtualDocument()) {
+            if (src.isVirtualDocument()) {
                 final AssetFileDescriptor srcFileAsAsset =
-                        srcClient.openTypedAssetFileDescriptor(
-                                srcInfo.derivedUri, mimeType, null, canceller);
+                        getClient(src).openTypedAssetFileDescriptor(
+                                src.derivedUri, mimeType, null, canceller);
                 srcFile = srcFileAsAsset.getParcelFileDescriptor();
-                src = new AssetFileDescriptor.AutoCloseInputStream(srcFileAsAsset);
+                in = new AssetFileDescriptor.AutoCloseInputStream(srcFileAsAsset);
             } else {
-                srcFile = srcClient.openFile(srcInfo.derivedUri, "r", canceller);
-                src = new ParcelFileDescriptor.AutoCloseInputStream(srcFile);
+                srcFile = getClient(src).openFile(src.derivedUri, "r", canceller);
+                in = new ParcelFileDescriptor.AutoCloseInputStream(srcFile);
             }
 
-            dstFile = dstClient.openFile(dstInfo.derivedUri, "w", canceller);
-            dst = new ParcelFileDescriptor.AutoCloseOutputStream(dstFile);
+            dstFile = getClient(dest).openFile(dest.derivedUri, "w", canceller);
+            out = new ParcelFileDescriptor.AutoCloseOutputStream(dstFile);
 
             byte[] buffer = new byte[32 * 1024];
             int len;
-            while ((len = src.read(buffer)) != -1) {
+            while ((len = in.read(buffer)) != -1) {
                 if (isCanceled()) {
                     if (DEBUG) Log.d(TAG, "Canceled copy mid-copy. Id:" + id);
                     success = false;
                     break;
                 }
-                dst.write(buffer, 0, len);
+                out.write(buffer, 0, len);
                 makeCopyProgress(len);
             }
 
             srcFile.checkError();
         } catch (IOException e) {
             success = false;
-            onFileFailed(srcInfo, "Exception thrown while copying from "
-                    + srcInfo.derivedUri + " to " + dstInfo.derivedUri + ".");
+            onFileFailed(src, "Exception thrown while copying from "
+                    + src.derivedUri + " to " + dest.derivedUri + ".");
 
             if (dstFile != null) {
                 try {
@@ -420,20 +403,20 @@
             }
         } finally {
             // This also ensures the file descriptors are closed.
-            IoUtils.closeQuietly(src);
-            IoUtils.closeQuietly(dst);
+            IoUtils.closeQuietly(in);
+            IoUtils.closeQuietly(out);
         }
 
         if (!success) {
             if (DEBUG) Log.d(TAG, "Cleaning up failed operation leftovers.");
             canceller.cancel();
             try {
-                DocumentsContract.deleteDocument(dstClient, dstInfo.derivedUri);
+                DocumentsContract.deleteDocument(getClient(dest), dest.derivedUri);
             } catch (RemoteException e) {
                 // RemoteExceptions usually signal that the connection is dead, so there's no
                 // point attempting to continue. Propagate the exception up so the copy job is
                 // cancelled.
-                Log.w(TAG, "Failed to cleanup after copy error: " + srcInfo.derivedUri, e);
+                Log.w(TAG, "Failed to cleanup after copy error: " + src.derivedUri, e);
                 throw e;
             }
         }
@@ -449,14 +432,14 @@
      * @return Size in bytes.
      * @throws RemoteException
      */
-    private static long calculateSize(ContentProviderClient client, List<DocumentInfo> srcs)
+    private long calculateSize(List<DocumentInfo> srcs)
             throws RemoteException {
         long result = 0;
 
         for (DocumentInfo src : srcs) {
             if (src.isDirectory()) {
                 // Directories need to be recursed into.
-                result += calculateFileSizesRecursively(client, src.derivedUri);
+                result += calculateFileSizesRecursively(getClient(src), src.derivedUri);
             } else {
                 result += src.size;
             }
@@ -503,20 +486,14 @@
         return result;
     }
 
-    @Override
-    void cleanup() {
-        ContentProviderClient.releaseQuietly(srcClient);
-        ContentProviderClient.releaseQuietly(dstClient);
-    }
-
     /**
      * Returns true if {@code doc} is a descendant of {@code parentDoc}.
      * @throws RemoteException
      */
-    boolean isDescendentOf(DocumentInfo doc, DocumentInfo parentDoc)
+    boolean isDescendentOf(DocumentInfo doc, DocumentInfo parent)
             throws RemoteException {
-        if (parentDoc.isDirectory() && doc.authority.equals(parentDoc.authority)) {
-            return isChildDocument(dstClient, doc.derivedUri, parentDoc.derivedUri);
+        if (parent.isDirectory() && doc.authority.equals(parent.authority)) {
+            return isChildDocument(getClient(doc), doc.derivedUri, parent.derivedUri);
         }
         return false;
     }
@@ -525,4 +502,16 @@
         Log.w(TAG, msg);
         onFileFailed(file);
     }
+
+    @Override
+    public String toString() {
+        return new StringBuilder()
+                .append("CopyJob")
+                .append("{")
+                .append("id=" + id)
+                .append("srcs=" + mSrcs)
+                .append(", destination=" + stack)
+                .append("}")
+                .toString();
+    }
 }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/DeleteJob.java b/packages/DocumentsUI/src/com/android/documentsui/services/DeleteJob.java
new file mode 100644
index 0000000..6a2a794
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/DeleteJob.java
@@ -0,0 +1,96 @@
+/*
+ * 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.documentsui.services;
+
+import static com.android.documentsui.Shared.DEBUG;
+import static com.android.documentsui.services.FileOperationService.OPERATION_DELETE;
+
+import android.app.Notification;
+import android.app.Notification.Builder;
+import android.content.Context;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.documentsui.R;
+import com.android.documentsui.model.DocumentInfo;
+import com.android.documentsui.model.DocumentStack;
+
+import java.util.List;
+
+final class DeleteJob extends Job {
+
+    private static final String TAG = "DeleteJob";
+    private List<DocumentInfo> mSrcs;
+
+    /**
+     * Moves files to a destination identified by {@code destination}.
+     * Performs most work by delegating to CopyJob, then deleting
+     * a file after it has been copied.
+     *
+     * @see @link {@link Job} constructor for most param descriptions.
+     *
+     * @param srcs List of files to delete
+     */
+    DeleteJob(Context service, Context appContext, Listener listener,
+            String id, DocumentStack stack, List<DocumentInfo> srcs) {
+        super(service, appContext, listener, OPERATION_DELETE, id, stack);
+        this.mSrcs = srcs;
+    }
+
+    @Override
+    Builder createProgressBuilder() {
+        return super.createProgressBuilder(
+                service.getString(R.string.move_notification_title),
+                R.drawable.ic_menu_copy,
+                service.getString(android.R.string.cancel),
+                R.drawable.ic_cab_cancel);
+    }
+
+    @Override
+    public Notification getSetupNotification() {
+        return getSetupNotification(service.getString(R.string.delete_preparing));
+    }
+
+    @Override
+    Notification getFailureNotification() {
+        return getFailureNotification(
+                R.plurals.delete_error_notification_title, R.drawable.ic_menu_delete);
+    }
+
+    @Override
+    void start() throws RemoteException {
+        for (DocumentInfo doc : mSrcs) {
+            if (DEBUG) Log.d(TAG, "Deleting document @ " + doc.derivedUri);
+            if (!deleteDocument(doc)) {
+                Log.w(TAG, "Failed to delete document @ " + doc.derivedUri);
+                onFileFailed(doc);
+            }
+        }
+    }
+
+    @Override
+    public String toString() {
+        return new StringBuilder()
+                .append("DeleteJob")
+                .append("{")
+                .append("id=" + id)
+                .append("srcs=" + mSrcs)
+                .append(", location=" + stack)
+                .append("}")
+                .toString();
+    }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/FileOperationService.java b/packages/DocumentsUI/src/com/android/documentsui/services/FileOperationService.java
index 1df20ac..aca2d7a 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/services/FileOperationService.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/FileOperationService.java
@@ -27,6 +27,7 @@
 import android.content.Intent;
 import android.os.IBinder;
 import android.os.PowerManager;
+import android.support.annotation.Nullable;
 import android.support.annotation.VisibleForTesting;
 import android.util.Log;
 
@@ -51,9 +52,11 @@
 
     private static final int DEFAULT_DELAY = 0;
     private static final int MAX_DELAY = 10 * 1000;  // ten seconds
+    private static final int POOL_SIZE = 2;  // "pool size", not *max* "pool size".
+    private static final int NOTIFICATION_ID_PROGRESS = 0;
+    private static final int NOTIFICATION_ID_FAILURE = 1;
 
     public static final String TAG = "FileOperationService";
-    private static final int POOL_SIZE = 2;  // "pool size", not *max* "pool size".
 
     public static final String EXTRA_JOB_ID = "com.android.documentsui.JOB_ID";
     public static final String EXTRA_DELAY = "com.android.documentsui.DELAY";
@@ -92,7 +95,7 @@
     @GuardedBy("mRunning")
     private Map<String, JobRecord> mRunning = new HashMap<>();
 
-    private int mLastStarted;
+    private int mLastServiceId;
 
     @Override
     public void onCreate() {
@@ -111,7 +114,18 @@
     }
 
     @Override
-    public int onStartCommand(Intent intent, int flags, int startTime) {
+    public void onDestroy() {
+        if (DEBUG) Log.d(TAG, "Shutting down executor.");
+        List<Runnable> unfinished = executor.shutdownNow();
+        if (!unfinished.isEmpty()) {
+            Log.w(TAG, "Shutting down, but executor reports running jobs: " + unfinished);
+        }
+        executor = null;
+        if (DEBUG) Log.d(TAG, "Destroyed.");
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int serviceId) {
         // TODO: Ensure we're not being called with retry or redeliver.
         // checkArgument(flags == 0);  // retry and redeliver are not supported.
 
@@ -123,17 +137,17 @@
             handleCancel(intent);
         } else {
             checkArgument(operationType != OPERATION_UNKNOWN);
-            handleOperation(intent, startTime, jobId, operationType);
+            handleOperation(intent, serviceId, jobId, operationType);
         }
 
         return START_NOT_STICKY;
     }
 
-    private void handleOperation(Intent intent, int startTime, String jobId, int operationType) {
-        if (DEBUG) Log.d(TAG, "onStartCommand: " + jobId + " with start time " + startTime);
+    private void handleOperation(Intent intent, int serviceId, String jobId, int operationType) {
+        if (DEBUG) Log.d(TAG, "onStartCommand: " + jobId + " with serviceId " + serviceId);
 
-        // Track start time so we can stop the service once we're out of work to do.
-        mLastStarted = startTime;
+        // Track the service supplied id so we can stop the service once we're out of work to do.
+        mLastServiceId = serviceId;
 
         Job job = null;
         synchronized (mRunning) {
@@ -147,12 +161,18 @@
 
             job = createJob(operationType, jobId, srcs, stack);
 
+            if (job == null) {
+                return;
+            }
+
             mWakeLock.acquire();
         }
 
         checkState(job != null);
         int delay = intent.getIntExtra(EXTRA_DELAY, DEFAULT_DELAY);
         checkArgument(delay <= MAX_DELAY);
+        if (DEBUG) Log.d(
+                TAG, "Scheduling job " + job.id + " to run in " + delay + " milliseconds.");
         ScheduledFuture<?> future = executor.schedule(job, delay, TimeUnit.MILLISECONDS);
         mRunning.put(jobId, new JobRecord(job, future));
     }
@@ -191,16 +211,24 @@
         // interactivity for the user in case the copy loop is stalled.
         // Try to cancel it even if we don't have a job id...in case there is some sad
         // orphan notification.
-        mNotificationManager.cancel(jobId, 0);
+        mNotificationManager.cancel(jobId, NOTIFICATION_ID_PROGRESS);
 
         // TODO: Guarantee the job is being finalized
     }
 
+    /**
+     * Creates a new job. Returns null if a job with {@code id} already exists.
+     * @return
+     */
     @GuardedBy("mRunning")
-    private Job createJob(
+    private @Nullable Job createJob(
             @OpType int operationType, String id, List<DocumentInfo> srcs, DocumentStack stack) {
 
-        checkArgument(!mRunning.containsKey(id));
+        if (mRunning.containsKey(id)) {
+            Log.w(TAG, "Duplicate job id: " + id
+                    + ". Ignoring job request for srcs: " + srcs + ", stack: " + stack + ".");
+            return null;
+        }
 
         Job job = null;
         switch (operationType) {
@@ -211,7 +239,8 @@
                 job = jobFactory.createMove(this, getApplicationContext(), this, id, stack, srcs);
                 break;
             case OPERATION_DELETE:
-                throw new UnsupportedOperationException();
+                job = jobFactory.createDelete(this, getApplicationContext(), this, id, stack, srcs);
+                break;
             default:
                 throw new UnsupportedOperationException();
         }
@@ -234,20 +263,21 @@
 
     /**
      * Most likely shuts down. Won't shut down if service has a pending
-     * message.
+     * message. Thread pool is deal with in onDestroy.
      */
     private void shutdown() {
-        if (DEBUG) Log.d(TAG, "Shutting down. Last start time: " + mLastStarted);
+        if (DEBUG) Log.d(TAG, "Shutting down. Last serviceId was " + mLastServiceId);
         mWakeLock.release();
         mWakeLock = null;
-        boolean gonnaStop = stopSelfResult(mLastStarted);
+
+        // Turns out, for us, stopSelfResult always returns false in tests,
+        // so we can't guard executor shutdown. For this reason we move
+        // executor shutdown to #onDestroy.
+        boolean gonnaStop = stopSelfResult(mLastServiceId);
         if (DEBUG) Log.d(TAG, "Stopping service: " + gonnaStop);
         if (!gonnaStop) {
             Log.w(TAG, "Service should be stopping, but reports otherwise.");
         }
-        // Sadly "gonnaStop" is always false in tests, so we can't guard executor shutdown.
-        List<Runnable> unfinished = executor.shutdownNow();
-        checkState(unfinished.isEmpty());
     }
 
     @VisibleForTesting
@@ -258,7 +288,7 @@
     @Override
     public void onStart(Job job) {
         if (DEBUG) Log.d(TAG, "onStart: " + job.id);
-        mNotificationManager.notify(job.id, 0, job.getSetupNotification());
+        mNotificationManager.notify(job.id, NOTIFICATION_ID_PROGRESS, job.getSetupNotification());
     }
 
     @Override
@@ -266,7 +296,7 @@
         if (DEBUG) Log.d(TAG, "onFinished: " + job.id);
 
         // Dismiss the ongoing copy notification when the copy is done.
-        mNotificationManager.cancel(job.id, 0);
+        mNotificationManager.cancel(job.id, NOTIFICATION_ID_PROGRESS);
 
         synchronized (mRunning) {
             deleteJob(job);
@@ -276,7 +306,8 @@
     @Override
     public void onProgress(CopyJob job) {
         if (DEBUG) Log.d(TAG, "onProgress: " + job.id);
-        mNotificationManager.notify(job.id, 0, job.getProgressNotification());
+        mNotificationManager.notify(
+                job.id, NOTIFICATION_ID_PROGRESS, job.getProgressNotification());
     }
 
     @Override
@@ -284,8 +315,8 @@
         if (DEBUG) Log.d(TAG, "onFailed: " + job.id);
         checkArgument(job.failed());
         Log.e(TAG, "Job failed on files: " + job.failedFiles.size() + ".");
-        mNotificationManager.notify(job.id, 0, job.getFailureNotification());
-        onFinished(job);  // failed jobs don't call finished, so we do.
+        mNotificationManager.notify(job.id, NOTIFICATION_ID_FAILURE, job.getFailureNotification());
+        onFinished(job);  // Failed jobs don't call finished, so we do.
     }
 
     private static final class JobRecord {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/FileOperations.java b/packages/DocumentsUI/src/com/android/documentsui/services/FileOperations.java
index 0f1730a3..f59a32a 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/services/FileOperations.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/FileOperations.java
@@ -22,6 +22,7 @@
 import static com.android.documentsui.Shared.asArrayList;
 import static com.android.documentsui.Shared.getQuantityString;
 import static com.android.documentsui.services.FileOperationService.EXTRA_CANCEL;
+import static com.android.documentsui.services.FileOperationService.EXTRA_DELAY;
 import static com.android.documentsui.services.FileOperationService.EXTRA_JOB_ID;
 import static com.android.documentsui.services.FileOperationService.EXTRA_OPERATION;
 import static com.android.documentsui.services.FileOperationService.EXTRA_SRC_LIST;
@@ -52,10 +53,12 @@
 
     private static final String TAG = "FileOperations";
 
+    private static final IdBuilder idBuilder = new IdBuilder();
+
     private FileOperations() {}
 
     public static String createJobId() {
-        return String.valueOf(elapsedRealtime());
+        return idBuilder.getNext();
     }
 
     /**
@@ -73,7 +76,7 @@
             case OPERATION_MOVE:
                 return FileOperations.move(activity, srcDocs, stack);
             case OPERATION_DELETE:
-                return FileOperations.delete(activity, srcDocs, stack);
+                throw new UnsupportedOperationException("Delete isn't currently supported.");
             default:
                 throw new UnsupportedOperationException("Unknown operation: " + operationType);
         }
@@ -151,14 +154,17 @@
      * @param jobId A unique jobid for this job.
      *     Use {@link #createJobId} if you don't have one handy.
      * @param srcDocs A list of src files to copy.
+     * @param delay Number of milliseconds to wait before executing the job.
      * @return Id of the job.
      */
     public static String delete(
-            Activity activity, List<DocumentInfo> srcDocs, DocumentStack location) {
+            Activity activity, List<DocumentInfo> srcDocs, DocumentStack location, int delay) {
         String jobId = createJobId();
-        if (DEBUG) Log.d(TAG, "Initiating 'delete' operation id: " + jobId);
+        if (DEBUG) Log.d(TAG, "Initiating 'delete' operation id " + jobId
+                + " delayed by " + delay + " milliseconds.");
 
         Intent intent = createBaseIntent(OPERATION_DELETE, activity, jobId, srcDocs, location);
+        intent.putExtra(EXTRA_DELAY, delay);
         activity.startService(intent);
 
         return jobId;
@@ -193,4 +199,24 @@
                 getQuantityString(activity, contentId, fileCount),
                 Snackbar.LENGTH_SHORT);
     }
+
+    private static final class IdBuilder {
+
+        // Remember last job time so we can guard against collisions.
+        private long mLastJobTime;
+
+        // If we detect a collision, use subId to make distinct.
+        private int mSubId;
+
+        public synchronized String getNext() {
+            long time = elapsedRealtime();
+            if (time == mLastJobTime) {
+                mSubId++;
+            } else {
+                mSubId = 0;
+            }
+            mLastJobTime = time;
+            return String.valueOf(mLastJobTime) + "-" + String.valueOf(mSubId);
+        }
+    }
 }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/Job.java b/packages/DocumentsUI/src/com/android/documentsui/services/Job.java
index c7939eb..f351df9 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/services/Job.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/Job.java
@@ -16,6 +16,7 @@
 
 package com.android.documentsui.services;
 
+import static com.android.documentsui.DocumentsApplication.acquireUnstableProviderOrThrow;
 import static com.android.documentsui.services.FileOperationService.EXTRA_CANCEL;
 import static com.android.documentsui.services.FileOperationService.EXTRA_FAILURE;
 import static com.android.documentsui.services.FileOperationService.EXTRA_JOB_ID;
@@ -24,18 +25,21 @@
 import static com.android.documentsui.services.FileOperationService.FAILURE_COPY;
 import static com.android.documentsui.services.FileOperationService.OPERATION_UNKNOWN;
 import static com.android.internal.util.Preconditions.checkArgument;
+import static com.android.internal.util.Preconditions.checkNotNull;
 
 import android.annotation.DrawableRes;
 import android.annotation.PluralsRes;
 import android.app.Notification;
 import android.app.Notification.Builder;
 import android.app.PendingIntent;
+import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.os.Parcelable;
 import android.os.RemoteException;
 import android.provider.DocumentsContract;
+import android.util.Log;
 
 import com.android.documentsui.FilesActivity;
 import com.android.documentsui.R;
@@ -45,7 +49,9 @@
 import com.android.documentsui.services.FileOperationService.OpType;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * A mashup of work item and ui progress update factory. Used by {@link FileOperationService}
@@ -53,6 +59,7 @@
  */
 abstract class Job implements Runnable {
 
+    private static final String TAG = "Job";
     final Context service;
     final Context appContext;
     final Listener listener;
@@ -64,6 +71,7 @@
     final ArrayList<DocumentInfo> failedFiles = new ArrayList<>();
     final Notification.Builder mProgressBuilder;
 
+    private final Map<String, ContentProviderClient> mClients = new HashMap<>();
     private volatile boolean mCanceled;
 
     /**
@@ -118,14 +126,31 @@
 
     abstract void start() throws RemoteException;
 
-    // Service will call this when it is done with the job.
-    abstract void cleanup();
-
     abstract Notification getSetupNotification();
     // TODO: Progress notification for deletes.
     // abstract Notification getProgressNotification(long bytesCopied);
     abstract Notification getFailureNotification();
 
+    ContentProviderClient getClient(DocumentInfo doc) throws RemoteException {
+        ContentProviderClient client = mClients.get(doc.authority);
+        if (client == null) {
+            // Acquire content providers.
+            client = acquireUnstableProviderOrThrow(
+                    getContentResolver(),
+                    doc.authority);
+
+            mClients.put(doc.authority, client);
+        }
+
+        return checkNotNull(client);
+    }
+
+    final void cleanup() {
+        for (ContentProviderClient client : mClients.values()) {
+            ContentProviderClient.releaseQuietly(client);
+        }
+    }
+
     final void cancel() {
         mCanceled = true;
     }
@@ -146,6 +171,17 @@
         return !failedFiles.isEmpty();
     }
 
+    final boolean deleteDocument(DocumentInfo doc) {
+        try {
+            DocumentsContract.deleteDocument(getClient(doc), doc.derivedUri);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to delete file: " + doc.derivedUri, e);
+            return false;
+        }
+
+        return true;  // victory dance!
+    }
+
     Notification getSetupNotification(String content) {
         mProgressBuilder.setProgress(0, 0, true);
         mProgressBuilder.setContentText(content);
@@ -215,6 +251,16 @@
         return cancelIntent;
     }
 
+    @Override
+    public String toString() {
+        return new StringBuilder()
+                .append("Job")
+                .append("{")
+                .append("id=" + id)
+                .append("}")
+                .toString();
+    }
+
     /**
      * Factory class that facilitates our testing FileOperationService.
      */
@@ -231,6 +277,11 @@
                 String id, DocumentStack stack, List<DocumentInfo> srcs) {
             return new MoveJob(service, appContext, listener, id, stack, srcs);
         }
+
+        Job createDelete(Context service, Context appContext, Listener listener,
+                String id, DocumentStack stack, List<DocumentInfo> srcs) {
+            return new DeleteJob(service, appContext, listener, id, stack, srcs);
+        }
     }
 
     /**
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/MoveJob.java b/packages/DocumentsUI/src/com/android/documentsui/services/MoveJob.java
index f46f234..7a238bd 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/services/MoveJob.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/MoveJob.java
@@ -76,40 +76,47 @@
     }
 
     @Override
-    boolean processDocument(DocumentInfo srcInfo, DocumentInfo dstDirInfo) throws RemoteException {
+    boolean processDocument(DocumentInfo src, DocumentInfo dest) throws RemoteException {
 
         // TODO: When optimized move kicks in, we're not making any progress updates. FIX IT!
 
         // When moving within the same provider, try to use optimized moving.
         // If not supported, then fallback to byte-by-byte copy/move.
-        if (srcInfo.authority.equals(dstDirInfo.authority)) {
-            if ((srcInfo.flags & Document.FLAG_SUPPORTS_MOVE) != 0) {
-                if (DocumentsContract.moveDocument(srcClient, srcInfo.derivedUri,
-                        dstDirInfo.derivedUri) == null) {
-                    onFileFailed(srcInfo);
+        if (src.authority.equals(dest.authority)) {
+            if ((src.flags & Document.FLAG_SUPPORTS_MOVE) != 0) {
+                if (DocumentsContract.moveDocument(getClient(src), src.derivedUri,
+                        dest.derivedUri) == null) {
+                    onFileFailed(src);
                     return false;
                 }
                 return true;
             }
         }
 
-        // If we couldn't do an optimized copy...we fall back to vanilla byte copy.
-        boolean copied = byteCopyDocument(srcInfo, dstDirInfo);
-
-        return copied && !isCanceled() && deleteSrcDocument(srcInfo);
-    }
-
-    private boolean deleteSrcDocument(DocumentInfo srcInfo) {
-        // This is racey. We should make sure that we never delete a directory after
-        // it changed, so we don't remove a file which had not been copied earlier
-        // to the target location.
-        try {
-            DocumentsContract.deleteDocument(srcClient, srcInfo.derivedUri);
-        } catch (RemoteException e) {
-            Log.w(TAG, "Failed to delete source after copy: " + srcInfo.derivedUri, e);
+        // Moving virtual files by bytes is not supported. This is because, it would involve
+        // conversion, and the source file should not be deleted in such case (as it's a different
+        // file).
+        if (src.isVirtualDocument()) {
+            Log.w(TAG, "Cannot move virtual files byte by byte.");
+            onFileFailed(src);
             return false;
         }
 
-        return true;  // victory dance!
+        // If we couldn't do an optimized copy...we fall back to vanilla byte copy.
+        boolean copied = byteCopyDocument(src, dest);
+
+        return copied && !isCanceled() && deleteDocument(src);
+    }
+
+    @Override
+    public String toString() {
+        return new StringBuilder()
+                .append("MoveJob")
+                .append("{")
+                .append("id=" + id)
+                .append("srcs=" + mSrcs)
+                .append(", destination=" + stack)
+                .append("}")
+                .toString();
     }
 }
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/DocumentHolderTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/DocumentHolderTest.java
new file mode 100644
index 0000000..16efc6e
--- /dev/null
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/DocumentHolderTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.documentsui.dirlist;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.MotionEvent.PointerCoords;
+import android.view.MotionEvent.PointerProperties;
+
+import com.android.documentsui.R;
+import com.android.documentsui.State;
+
+@SmallTest
+public class DocumentHolderTest extends AndroidTestCase {
+
+    DocumentHolder mHolder;
+    TestListener mListener;
+
+    public void setUp() throws Exception {
+        Context context = getContext();
+        LayoutInflater inflater = LayoutInflater.from(context);
+        mHolder = new DocumentHolder(getContext(), inflater.inflate(R.layout.item_doc_list, null)) {
+            @Override
+            public void bind(Cursor cursor, String modelId, State state) {}
+        };
+
+        mListener = new TestListener();
+        mHolder.addEventListener(mListener);
+
+        mHolder.itemView.requestLayout();
+        mHolder.itemView.invalidate();
+    }
+
+    public void testClickActivates() {
+        click();
+        mListener.assertSelected();
+    }
+
+    public void testTapActivates() {
+        tap();
+        mListener.assertActivated();
+    }
+
+    public void click() {
+        mHolder.onSingleTapUp(createEvent(MotionEvent.TOOL_TYPE_MOUSE));
+    }
+
+    public void tap() {
+        mHolder.onSingleTapUp(createEvent(MotionEvent.TOOL_TYPE_FINGER));
+    }
+
+    public MotionEvent createEvent(int tooltype) {
+        long time = SystemClock.uptimeMillis();
+
+        PointerProperties properties[] = new PointerProperties[] {
+                new PointerProperties()
+        };
+        properties[0].toolType = tooltype;
+
+        PointerCoords coords[] = new PointerCoords[] {
+                new PointerCoords()
+        };
+
+        Rect rect = new Rect();
+        mHolder.itemView.getHitRect(rect);
+        coords[0].x = rect.left;
+        coords[0].y = rect.top;
+
+        return MotionEvent.obtain(
+                time, // down time
+                time, // event time
+                MotionEvent.ACTION_UP, // action
+                1, // pointer count
+                properties, // pointer properties
+                coords, // pointer coords
+                0, // metastate
+                0, // button state
+                0, // xprecision
+                0, // yprecision
+                0, // deviceid
+                0, // edgeflags
+                0, // source
+                0 // flags
+                );
+    }
+
+    private class TestListener implements DocumentHolder.EventListener {
+        private boolean mActivated = false;
+        private boolean mSelected = false;
+
+        public void assertActivated() {
+            assertTrue(mActivated);
+            assertFalse(mSelected);
+        }
+
+        public void assertSelected() {
+            assertTrue(mSelected);
+            assertFalse(mActivated);
+        }
+
+        @Override
+        public boolean onActivate(DocumentHolder doc) {
+            mActivated = true;
+            return true;
+        }
+
+        @Override
+        public boolean onSelect(DocumentHolder doc) {
+            mSelected = true;
+            return true;
+        }
+
+    }
+}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java
index 5ce1823..2244be9 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java
@@ -51,7 +51,7 @@
     public void setUp() {
 
         final Context testContext = TestContext.createStorageTestContext(getContext(), AUTHORITY);
-        mModel = new TestModel(testContext, AUTHORITY);
+        mModel = new TestModel(AUTHORITY);
         mModel.update(NAMES);
 
         DocumentsAdapter.Environment env = new TestEnvironment(testContext);
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/ModelTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/ModelTest.java
index a5f0656..83299f0 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/ModelTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/ModelTest.java
@@ -38,7 +38,6 @@
 import java.util.List;
 import java.util.Random;
 import java.util.Set;
-import java.util.concurrent.CountDownLatch;
 
 @SmallTest
 public class ModelTest extends AndroidTestCase {
@@ -96,7 +95,7 @@
         r.cursor = cursor;
 
         // Instantiate the model with a dummy view adapter and listener that (for now) do nothing.
-        model = new Model(context);
+        model = new Model();
         model.addUpdateListener(new DummyListener());
         model.update(r);
     }
@@ -303,16 +302,6 @@
         }
     }
 
-    // Tests that Model.delete works correctly.
-    public void testDelete() throws Exception {
-        // Simulate deleting 2 files.
-        List<DocumentInfo> docsBefore = getDocumentInfo(2, 3);
-        delete(2, 3);
-
-        provider.assertWasDeleted(docsBefore.get(0));
-        provider.assertWasDeleted(docsBefore.get(1));
-    }
-
     private void setupTestContext() {
         final MockContentResolver resolver = new MockContentResolver();
         context = new ContextWrapper(getContext()) {
@@ -335,29 +324,6 @@
         return s;
     }
 
-    private void delete(int... positions) throws InterruptedException {
-        Selection s = positionToSelection(positions);
-        final CountDownLatch latch = new CountDownLatch(1);
-
-        model.delete(
-                s,
-                new Model.DeletionListener() {
-                    @Override
-                    public void onError() {
-                        latch.countDown();
-                    }
-                    @Override
-                    void onCompletion() {
-                        latch.countDown();
-                    }
-                });
-        latch.await();
-    }
-
-    private List<DocumentInfo> getDocumentInfo(int... positions) {
-        return model.getDocuments(positionToSelection(positions));
-    }
-
     private static class DummyListener implements Model.UpdateListener {
         public void onModelUpdate(Model model) {}
         public void onModelUpdateFailed(Exception e) {}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java
index 7a3b6d4..d3ef9aa 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java
@@ -23,7 +23,6 @@
 
 import com.android.documentsui.TestInputEvent;
 import com.android.documentsui.dirlist.MultiSelectManager.Selection;
-
 import com.google.common.collect.Lists;
 
 import java.util.ArrayList;
@@ -55,13 +54,21 @@
         mManager.addCallback(mCallback);
     }
 
-    public void testMouseClick_StartsSelectionMode() {
-        click(7);
+    public void testSelection() {
+        // Check selection.
+        mManager.toggleSelection(items.get(7));
         assertSelection(items.get(7));
+        // Check deselection.
+        mManager.toggleSelection(items.get(7));
+        assertSelectionSize(0);
     }
 
-    public void testMouseClick_NotifiesSelectionChanged() {
-        click(7);
+    public void testSelection_NotifiesSelectionChanged() {
+        // Selection should notify.
+        mManager.toggleSelection(items.get(7));
+        mCallback.assertSelectionChanged();
+        // Deselection should notify.
+        mManager.toggleSelection(items.get(7));
         mCallback.assertSelectionChanged();
     }
 
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManager_GridModelTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManager_GridModelTest.java
index 5c04db9..7920c50 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManager_GridModelTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManager_GridModelTest.java
@@ -334,5 +334,10 @@
         public void focusItem(int i) {
             throw new UnsupportedOperationException();
         }
+
+        @Override
+        public boolean isLayoutItem(int adapterPosition) {
+            return false;
+        }
     }
 }
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java
index 398885c..7c324e7 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java
@@ -23,15 +23,12 @@
 import android.support.v7.widget.RecyclerView;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
-import android.util.SparseArray;
 import android.view.ViewGroup;
 
 import com.android.documentsui.DirectoryResult;
 import com.android.documentsui.RootCursorWrapper;
 import com.android.documentsui.State;
 
-import java.util.List;
-
 @SmallTest
 public class SectionBreakDocumentsAdapterWrapperTest extends AndroidTestCase {
 
@@ -57,7 +54,7 @@
         final Context testContext = TestContext.createStorageTestContext(getContext(), AUTHORITY);
         DocumentsAdapter.Environment env = new TestEnvironment(testContext);
 
-        mModel = new TestModel(testContext, AUTHORITY);
+        mModel = new TestModel(AUTHORITY);
         mAdapter = new SectionBreakDocumentsAdapterWrapper(
             env,
             new ModelBackedDocumentsAdapter(
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/TestModel.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/TestModel.java
index f9cd3b2..d8c29db 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/TestModel.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/TestModel.java
@@ -16,16 +16,13 @@
 
 package com.android.documentsui.dirlist;
 
-import android.content.Context;
 import android.database.MatrixCursor;
 import android.provider.DocumentsContract.Document;
 
 import com.android.documentsui.DirectoryResult;
 import com.android.documentsui.RootCursorWrapper;
-import com.android.documentsui.dirlist.MultiSelectManager.Selection;
 
 import java.util.Random;
-import java.util.Set;
 
 public class TestModel extends Model {
 
@@ -39,14 +36,9 @@
     };
 
     private final String mAuthority;
-    private Set<String> mDeleted;
 
-    /**
-     * Creates a new context. context must be configured with provider for authority.
-     * @see TestContext#createStorageTestContext(Context, String).
-     */
-    public TestModel(Context context, String authority) {
-        super(context);
+    public TestModel(String authority) {
+        super();
         mAuthority = authority;
     }
 
@@ -75,12 +67,4 @@
     String idForPosition(int p) {
         return createModelId(mAuthority, Integer.toString(p));
     }
-
-    @Override
-    public void delete(Selection selected, DeletionListener listener) {
-        for (String id : selected.getAll()) {
-            mDeleted.add(id);
-        }
-        listener.onCompletion();
-    }
 }
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/TestSelectionEnvironment.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/TestSelectionEnvironment.java
index c4cfd3a..0e79561 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/TestSelectionEnvironment.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/TestSelectionEnvironment.java
@@ -110,4 +110,9 @@
     @Override
     public void focusItem(int position) {
     }
+
+    @Override
+    public boolean isLayoutItem(int adapterPosition) {
+        return false;
+    }
 }
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/BaseCopyJobTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/AbstractCopyJobTest.java
similarity index 72%
rename from packages/DocumentsUI/tests/src/com/android/documentsui/services/BaseCopyJobTest.java
rename to packages/DocumentsUI/tests/src/com/android/documentsui/services/AbstractCopyJobTest.java
index f57ce53..ec21150 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/services/BaseCopyJobTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/AbstractCopyJobTest.java
@@ -16,72 +16,18 @@
 
 package com.android.documentsui.services;
 
-import static com.android.documentsui.StubProvider.ROOT_0_ID;
-import static com.android.documentsui.StubProvider.ROOT_1_ID;
 import static com.google.common.collect.Lists.newArrayList;
 
-import android.content.ContentProviderClient;
-import android.content.ContentResolver;
-import android.content.Context;
 import android.net.Uri;
-import android.os.RemoteException;
 import android.provider.DocumentsContract;
-import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.MediumTest;
 
-import com.android.documentsui.DocumentsProviderHelper;
-import com.android.documentsui.StubProvider;
 import com.android.documentsui.model.DocumentInfo;
-import com.android.documentsui.model.RootInfo;
 
 import java.util.List;
 
 @MediumTest
-public abstract class BaseCopyJobTest extends AndroidTestCase {
-
-    static String AUTHORITY = StubProvider.DEFAULT_AUTHORITY;
-    static final byte[] HAM_BYTES = "ham and cheese".getBytes();
-    static final byte[] FRUITY_BYTES = "I love fruit cakes!".getBytes();
-
-    Context mContext;
-    ContentResolver mResolver;
-    ContentProviderClient mClient;
-    DocumentsProviderHelper mDocs;
-    TestJobListener mJobListener;
-    RootInfo mSrcRoot;
-    RootInfo mDestRoot;
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-
-        mJobListener = new TestJobListener();
-
-        // NOTE: Must be the "target" context, else security checks in content provider will fail.
-        mContext = getContext();
-        mResolver = mContext.getContentResolver();
-
-        mClient = mResolver.acquireContentProviderClient(AUTHORITY);
-        mDocs = new DocumentsProviderHelper(AUTHORITY, mClient);
-
-        initTestFiles();
-    }
-
-    @Override
-    protected void tearDown() throws Exception {
-        resetStorage();
-        mClient.release();
-        super.tearDown();
-    }
-
-    private void resetStorage() throws RemoteException {
-        mClient.call("clear", null, null);
-    }
-
-    private void initTestFiles() throws RemoteException {
-        mSrcRoot = mDocs.getRoot(ROOT_0_ID);
-        mDestRoot = mDocs.getRoot(ROOT_1_ID);
-    }
+public abstract class AbstractCopyJobTest<T extends CopyJob> extends AbstractJobTest<T> {
 
     public void runCopyFilesTest() throws Exception {
         Uri testFile1 = mDocs.createDocument(mSrcRoot, "text/plain", "test1.txt");
@@ -174,9 +120,9 @@
 
     public void runNoCopyDirToDescendentTest() throws Exception {
         Uri testDir = mDocs.createFolder(mSrcRoot, "someDir");
-        Uri descDir = mDocs.createFolder(testDir, "theDescendent");
+        Uri destDir = mDocs.createFolder(testDir, "theDescendent");
 
-        createJob(newArrayList(testDir), descDir).run();
+        createJob(newArrayList(testDir), destDir).run();
 
         mJobListener.waitForFinished();
         mJobListener.assertFailed();
@@ -201,10 +147,11 @@
         mDocs.assertChildCount(mDestRoot, 0);
     }
 
-    final CopyJob createJob(List<Uri> srcs) throws Exception {
+    /**
+     * Creates a job with a stack consisting to the default destination.
+     */
+    final T createJob(List<Uri> srcs) throws Exception {
         Uri destination = DocumentsContract.buildDocumentUri(AUTHORITY, mDestRoot.documentId);
         return createJob(srcs, destination);
     }
-
-    abstract CopyJob createJob(List<Uri> srcs, Uri destination) throws Exception;
 }
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/AbstractJobTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/AbstractJobTest.java
new file mode 100644
index 0000000..691af6a
--- /dev/null
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/AbstractJobTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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.documentsui.services;
+
+import static com.android.documentsui.StubProvider.ROOT_0_ID;
+import static com.android.documentsui.StubProvider.ROOT_1_ID;
+
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.provider.DocumentsContract;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+
+import com.android.documentsui.DocumentsProviderHelper;
+import com.android.documentsui.StubProvider;
+import com.android.documentsui.model.DocumentInfo;
+import com.android.documentsui.model.DocumentStack;
+import com.android.documentsui.model.RootInfo;
+
+import com.google.common.collect.Lists;
+
+import java.util.List;
+
+@MediumTest
+public abstract class AbstractJobTest<T extends Job> extends AndroidTestCase {
+
+    static String AUTHORITY = StubProvider.DEFAULT_AUTHORITY;
+    static final byte[] HAM_BYTES = "ham and cheese".getBytes();
+    static final byte[] FRUITY_BYTES = "I love fruit cakes!".getBytes();
+
+    Context mContext;
+    ContentResolver mResolver;
+    ContentProviderClient mClient;
+    DocumentsProviderHelper mDocs;
+    TestJobListener mJobListener;
+    RootInfo mSrcRoot;
+    RootInfo mDestRoot;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        mJobListener = new TestJobListener();
+
+        // NOTE: Must be the "target" context, else security checks in content provider will fail.
+        mContext = getContext();
+        mResolver = mContext.getContentResolver();
+
+        mClient = mResolver.acquireContentProviderClient(AUTHORITY);
+        mDocs = new DocumentsProviderHelper(AUTHORITY, mClient);
+
+        initTestFiles();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        resetStorage();
+        mClient.release();
+        super.tearDown();
+    }
+
+    private void resetStorage() throws RemoteException {
+        mClient.call("clear", null, null);
+    }
+
+    private void initTestFiles() throws RemoteException {
+        mSrcRoot = mDocs.getRoot(ROOT_0_ID);
+        mDestRoot = mDocs.getRoot(ROOT_1_ID);
+    }
+
+    final T createJob(List<Uri> srcs, Uri destination) throws Exception {
+        DocumentStack stack = new DocumentStack();
+        stack.push(DocumentInfo.fromUri(mResolver, destination));
+
+        List<DocumentInfo> srcDocs = Lists.newArrayList();
+        for (Uri src : srcs) {
+            srcDocs.add(DocumentInfo.fromUri(mResolver, src));
+        }
+
+        return createJob(srcDocs, stack);
+    }
+
+    abstract T createJob(List<DocumentInfo> srcs, DocumentStack destination) throws Exception;
+}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/CopyJobTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/CopyJobTest.java
index c0ce993..1acf2dc 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/services/CopyJobTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/CopyJobTest.java
@@ -16,18 +16,15 @@
 
 package com.android.documentsui.services;
 
-import android.net.Uri;
 import android.test.suitebuilder.annotation.MediumTest;
 
 import com.android.documentsui.model.DocumentInfo;
 import com.android.documentsui.model.DocumentStack;
 
-import com.google.common.collect.Lists;
-
 import java.util.List;
 
 @MediumTest
-public class CopyJobTest extends BaseCopyJobTest {
+public class CopyJobTest extends AbstractCopyJobTest<CopyJob> {
 
     public void testCopyFiles() throws Exception {
         runCopyFilesTest();
@@ -62,16 +59,8 @@
     }
 
     @Override
-    CopyJob createJob(List<Uri> srcs, Uri destination) throws Exception {
-        DocumentStack stack = new DocumentStack();
-        stack.push(DocumentInfo.fromUri(mResolver, destination));
-
-        List<DocumentInfo> srcDocs = Lists.newArrayList();
-        for (Uri src : srcs) {
-            srcDocs.add(DocumentInfo.fromUri(mResolver, src));
-        }
-
+    CopyJob createJob(List<DocumentInfo> srcs, DocumentStack stack) throws Exception {
         return new CopyJob(
-                mContext, mContext, mJobListener, FileOperations.createJobId(), stack, srcDocs);
+                mContext, mContext, mJobListener, FileOperations.createJobId(), stack, srcs);
     }
 }
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/DeleteJobTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/DeleteJobTest.java
new file mode 100644
index 0000000..d6d10239
--- /dev/null
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/DeleteJobTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.documentsui.services;
+
+import static com.google.common.collect.Lists.newArrayList;
+
+import android.net.Uri;
+import android.provider.DocumentsContract;
+import android.test.suitebuilder.annotation.MediumTest;
+
+import com.android.documentsui.model.DocumentInfo;
+import com.android.documentsui.model.DocumentStack;
+
+import java.util.List;
+
+@MediumTest
+public class DeleteJobTest extends AbstractJobTest<DeleteJob> {
+
+    public void testDeleteFiles() throws Exception {
+        Uri testFile1 = mDocs.createDocument(mSrcRoot, "text/plain", "test1.txt");
+        mDocs.writeDocument(testFile1, HAM_BYTES);
+
+        Uri testFile2 = mDocs.createDocument(mSrcRoot, "text/plain", "test2.txt");
+        mDocs.writeDocument(testFile2, FRUITY_BYTES);
+
+        createJob(newArrayList(testFile1, testFile2)).run();
+        mJobListener.waitForFinished();
+
+        mDocs.assertChildCount(mSrcRoot, 0);
+    }
+
+    /**
+     * Creates a job with a stack consisting to the default src directory.
+     */
+    private final DeleteJob createJob(List<Uri> srcs) throws Exception {
+        Uri stack = DocumentsContract.buildDocumentUri(AUTHORITY, mSrcRoot.documentId);
+        return createJob(srcs, stack);
+    }
+
+    @Override
+    DeleteJob createJob(List<DocumentInfo> srcs, DocumentStack stack) throws Exception {
+        return new DeleteJob(
+                mContext, mContext, mJobListener, FileOperations.createJobId(), stack, srcs);
+    }
+}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/FileOperationServiceTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/FileOperationServiceTest.java
index d55b6f0..4d5392e 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/services/FileOperationServiceTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/FileOperationServiceTest.java
@@ -114,8 +114,11 @@
     public void testShutdownStopsExecutor_AfterSuccess() throws Exception {
         startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC));
 
-        mExecutor.isAlive();
+        mExecutor.assertAlive();
+
         mExecutor.runAll();
+        shutdownService();
+
         mExecutor.assertShutdown();
     }
 
@@ -126,6 +129,8 @@
         mJobFactory.jobs.get(0).fail(ALPHA_DOC);
 
         mExecutor.runAll();
+        shutdownService();
+
         mExecutor.assertShutdown();
     }
 
@@ -137,6 +142,8 @@
         mJobFactory.jobs.get(1).fail(GAMMA_DOC);
 
         mExecutor.runAll();
+        shutdownService();
+
         mExecutor.assertShutdown();
     }
 
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/MoveJobTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/MoveJobTest.java
index 5e41524..69d2db7 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/services/MoveJobTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/MoveJobTest.java
@@ -16,18 +16,18 @@
 
 package com.android.documentsui.services;
 
+import static com.google.common.collect.Lists.newArrayList;
+
 import android.net.Uri;
 import android.test.suitebuilder.annotation.MediumTest;
 
 import com.android.documentsui.model.DocumentInfo;
 import com.android.documentsui.model.DocumentStack;
 
-import com.google.common.collect.Lists;
-
 import java.util.List;
 
 @MediumTest
-public class MoveJobTest extends BaseCopyJobTest {
+public class MoveJobTest extends AbstractCopyJobTest<MoveJob> {
 
     public void testMoveFiles() throws Exception {
         runCopyFilesTest();
@@ -36,15 +36,23 @@
     }
 
     public void testMoveVirtualTypedFile() throws Exception {
-        runCopyVirtualTypedFileTest();
+        Uri testFile = mDocs.createVirtualFile(
+                mSrcRoot, "/virtual.sth", "virtual/mime-type",
+                FRUITY_BYTES, "application/pdf", "text/html");
+        createJob(newArrayList(testFile)).run();
 
-        mDocs.assertChildCount(mSrcRoot, 0);
+        mJobListener.waitForFinished();
+
+        // Should have failed, source not deleted. Moving by bytes for virtual files
+        // is not supported.
+        mDocs.assertChildCount(mDestRoot, 0);
+        mDocs.assertChildCount(mSrcRoot, 1);
     }
 
     public void testMoveVirtualNonTypedFile() throws Exception {
         runCopyVirtualNonTypedFileTest();
 
-        // should have failed, source not deleted
+        // Should have failed, source not deleted.
         mDocs.assertChildCount(mSrcRoot, 1);
     }
 
@@ -82,16 +90,8 @@
     }
 
     @Override
-    CopyJob createJob(List<Uri> srcs, Uri destination) throws Exception {
-        DocumentStack stack = new DocumentStack();
-        stack.push(DocumentInfo.fromUri(mResolver, destination));
-
-        List<DocumentInfo> srcDocs = Lists.newArrayList();
-        for (Uri src : srcs) {
-            srcDocs.add(DocumentInfo.fromUri(mResolver, src));
-        }
-
+    MoveJob createJob(List<DocumentInfo> srcs, DocumentStack stack) throws Exception {
         return new MoveJob(
-                mContext, mContext, mJobListener, FileOperations.createJobId(), stack, srcDocs);
+                mContext, mContext, mJobListener, FileOperations.createJobId(), stack, srcs);
     }
 }
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestJob.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestJob.java
index 72da9a1..9c58780 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestJob.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestJob.java
@@ -46,9 +46,6 @@
         assertTrue(mStarted);
     }
 
-    @Override
-    void cleanup() {}
-
     void fail(DocumentInfo doc) {
         onFileFailed(doc);
     }
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestScheduledExecutorService.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestScheduledExecutorService.java
index 5c39b78..4d417cf 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestScheduledExecutorService.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestScheduledExecutorService.java
@@ -146,7 +146,7 @@
         scheduled.get(taskIndex).runnable.run();
     }
 
-    public void isAlive() {
+    public void assertAlive() {
         assertFalse(isShutdown());
     }
 
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index c1f97a8..4cdfcb4 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -229,7 +229,7 @@
                   android:resumeWhilePausing="true"
                   android:screenOrientation="behind"
                   android:resizeableActivity="true"
-                  android:configChanges="orientation|screenSize|smallestScreenSize"
+                  android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|layoutDirection"
                   android:theme="@style/RecentsTheme.Wallpaper">
             <intent-filter>
                 <action android:name="com.android.systemui.recents.TOGGLE_RECENTS" />
diff --git a/packages/SystemUI/res/drawable/recents_info_dark.xml b/packages/SystemUI/res/drawable/recents_info_dark.xml
new file mode 100644
index 0000000..b1a2242
--- /dev/null
+++ b/packages/SystemUI/res/drawable/recents_info_dark.xml
@@ -0,0 +1,24 @@
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="48.0dp"
+        android:height="48.0dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="@color/recents_task_bar_dark_icon_color"
+        android:pathData="M12.000000,2.000000C6.500000,2.000000 2.000000,6.500000 2.000000,12.000000s4.500000,10.000000 10.000000,10.000000c5.500000,0.000000 10.000000,-4.500000 10.000000,-10.000000S17.500000,2.000000 12.000000,2.000000zM13.000000,17.000000l-2.000000,0.000000l0.000000,-6.000000l2.000000,0.000000L13.000000,17.000000zM13.000000,9.000000l-2.000000,0.000000L11.000000,7.000000l2.000000,0.000000L13.000000,9.000000z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/recents_info_light.xml b/packages/SystemUI/res/drawable/recents_info_light.xml
new file mode 100644
index 0000000..bc58c3b
--- /dev/null
+++ b/packages/SystemUI/res/drawable/recents_info_light.xml
@@ -0,0 +1,24 @@
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="48.0dp"
+        android:height="48.0dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:pathData="M12.000000,2.000000C6.500000,2.000000 2.000000,6.500000 2.000000,12.000000s4.500000,10.000000 10.000000,10.000000c5.500000,0.000000 10.000000,-4.500000 10.000000,-10.000000S17.500000,2.000000 12.000000,2.000000zM13.000000,17.000000l-2.000000,0.000000l0.000000,-6.000000l2.000000,0.000000L13.000000,17.000000zM13.000000,9.000000l-2.000000,0.000000L11.000000,7.000000l2.000000,0.000000L13.000000,9.000000z"
+        android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/packages/SystemUI/res/layout/notification_public_default.xml b/packages/SystemUI/res/layout/notification_public_default.xml
deleted file mode 100644
index 044ba09..0000000
--- a/packages/SystemUI/res/layout/notification_public_default.xml
+++ /dev/null
@@ -1,74 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2014 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License
-  -->
-
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/status_bar_latest_event_content"
-    android:layout_width="match_parent"
-    android:layout_height="64dp"
-    >
-    <ImageView android:id="@+id/icon"
-        android:layout_width="40dp"
-        android:layout_height="40dp"
-        android:layout_marginTop="12dp"
-        android:layout_marginStart="12dp"
-        android:layout_marginEnd="12dp"
-        android:scaleType="centerInside"
-        />
-    <DateTimeView android:id="@+id/time"
-        android:textAppearance="@android:style/TextAppearance.Material.Notification.Time"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginEnd="8dp"
-        android:layout_alignParentEnd="true"
-        android:layout_alignBaseline="@+id/title"
-        android:singleLine="true"
-        android:gravity="center"
-        android:paddingStart="8dp"
-        android:visibility="gone"
-        />
-    <TextView android:id="@+id/title"
-        android:textAppearance="@android:style/TextAppearance.Material.Notification.Title"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_toEndOf="@id/icon"
-        android:layout_toStartOf="@id/time"
-        android:singleLine="true"
-        android:ellipsize="marquee"
-        android:fadingEdge="horizontal"
-        />
-    <ImageView android:id="@+id/profile_badge_line3"
-        android:layout_width="@*android:dimen/notification_badge_size"
-        android:layout_height="@*android:dimen/notification_badge_size"
-        android:layout_below="@id/title"
-        android:layout_marginStart="4dp"
-        android:layout_marginEnd="8dp"
-        android:layout_alignParentEnd="true"
-        android:scaleType="fitCenter"
-        android:visibility="gone"
-        />
-    <TextView android:id="@+id/text"
-        android:textAppearance="@android:style/TextAppearance.Material.Notification"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_alignStart="@id/title"
-        android:layout_below="@id/title"
-        android:layout_toStartOf="@id/profile_badge_line3"
-        android:singleLine="true"
-        android:ellipsize="marquee"
-        android:fadingEdge="horizontal"
-        />
-</RelativeLayout>
diff --git a/packages/SystemUI/res/layout/recents_task_view_header.xml b/packages/SystemUI/res/layout/recents_task_view_header.xml
index 07ac39a..5c67f80 100644
--- a/packages/SystemUI/res/layout/recents_task_view_header.xml
+++ b/packages/SystemUI/res/layout/recents_task_view_header.xml
@@ -23,12 +23,10 @@
     <com.android.systemui.recents.views.FixedSizeImageView
         android:id="@+id/icon"
         android:contentDescription="@string/recents_app_info_button_label"
-        android:layout_width="@dimen/recents_task_view_application_icon_size"
-        android:layout_height="@dimen/recents_task_view_application_icon_size"
-        android:layout_marginStart="8dp"
+        android:layout_width="@dimen/recents_task_view_header_icon_width"
+        android:layout_height="@dimen/recents_task_view_header_icon_height"
         android:layout_gravity="center_vertical|start"
-        android:padding="8dp"
-        android:background="@drawable/recents_button_bg" />
+        android:padding="9dp" />
     <TextView
         android:id="@+id/title"
         android:layout_width="match_parent"
@@ -46,30 +44,36 @@
         android:fadingEdge="horizontal" />
     <com.android.systemui.recents.views.FixedSizeImageView
         android:id="@+id/move_task"
-        android:layout_width="48dp"
-        android:layout_height="48dp"
-        android:layout_marginEnd="52dp"
+        android:layout_width="@dimen/recents_task_view_header_button_width"
+        android:layout_height="@dimen/recents_task_view_header_button_height"
+        android:layout_marginEnd="@dimen/recents_task_view_header_button_width"
         android:layout_gravity="center_vertical|end"
-        android:padding="12dp"
+        android:padding="15dp"
         android:background="@drawable/recents_button_bg"
         android:src="@drawable/star"
         android:visibility="gone" />
     <com.android.systemui.recents.views.FixedSizeImageView
         android:id="@+id/dismiss_task"
-        android:layout_width="48dp"
-        android:layout_height="48dp"
-        android:layout_marginEnd="4dp"
+        android:layout_width="@dimen/recents_task_view_header_button_width"
+        android:layout_height="@dimen/recents_task_view_header_button_height"
         android:layout_gravity="center_vertical|end"
-        android:padding="12dp"
+        android:padding="15dp"
         android:background="@drawable/recents_button_bg"
         android:visibility="invisible"
         android:src="@drawable/recents_dismiss_light" />
-    <ProgressBar
-        android:id="@+id/focus_timer_indicator"
-        style="?android:attr/progressBarStyleHorizontal"
-        android:layout_width="match_parent"
-        android:layout_height="5dp"
-        android:layout_gravity="bottom"
-        android:indeterminateOnly="false"
-        android:visibility="invisible" />
+
+    <!-- The progress indicator shows if auto-paging is enabled -->
+    <ViewStub android:id="@+id/focus_timer_indicator_stub"
+               android:inflatedId="@+id/focus_timer_indicator"
+               android:layout="@layout/recents_task_view_header_progress_bar"
+               android:layout_width="match_parent"
+               android:layout_height="5dp"
+               android:layout_gravity="bottom" />
+
+    <!-- The app overlay shows as the user long-presses on the app icon -->
+    <ViewStub android:id="@+id/app_overlay_stub"
+               android:inflatedId="@+id/app_overlay"
+               android:layout="@layout/recents_task_view_header_overlay"
+               android:layout_width="match_parent"
+               android:layout_height="match_parent" />
 </com.android.systemui.recents.views.TaskViewHeader>
diff --git a/packages/SystemUI/res/layout/recents_task_view_header_overlay.xml b/packages/SystemUI/res/layout/recents_task_view_header_overlay.xml
new file mode 100644
index 0000000..dabfc80
--- /dev/null
+++ b/packages/SystemUI/res/layout/recents_task_view_header_overlay.xml
@@ -0,0 +1,50 @@
+<?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.
+-->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <com.android.systemui.recents.views.FixedSizeImageView
+        android:id="@+id/app_icon"
+        android:contentDescription="@string/recents_app_info_button_label"
+        android:layout_width="@dimen/recents_task_view_header_icon_width"
+        android:layout_height="@dimen/recents_task_view_header_icon_height"
+        android:layout_gravity="center_vertical|start"
+        android:padding="9dp" />
+    <TextView
+        android:id="@+id/app_title"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_vertical|start"
+        android:layout_marginStart="64dp"
+        android:layout_marginEnd="112dp"
+        android:textSize="16sp"
+        android:textColor="#ffffffff"
+        android:text="@string/recents_empty_message"
+        android:fontFamily="sans-serif-medium"
+        android:singleLine="true"
+        android:maxLines="2"
+        android:ellipsize="marquee"
+        android:fadingEdge="horizontal" />
+    <com.android.systemui.recents.views.FixedSizeImageView
+        android:id="@+id/app_info"
+        android:layout_width="@dimen/recents_task_view_header_button_width"
+        android:layout_height="@dimen/recents_task_bar_height"
+        android:layout_gravity="center_vertical|end"
+        android:padding="15dp"
+        android:background="@drawable/recents_button_bg"
+        android:src="@drawable/recents_info_light" />
+</FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/recents_task_view_header_progress_bar.xml b/packages/SystemUI/res/layout/recents_task_view_header_progress_bar.xml
new file mode 100644
index 0000000..f352632
--- /dev/null
+++ b/packages/SystemUI/res/layout/recents_task_view_header_progress_bar.xml
@@ -0,0 +1,22 @@
+<?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.
+-->
+<ProgressBar
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    style="?android:attr/progressBarStyleHorizontal"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:indeterminateOnly="false"
+    android:visibility="invisible" />
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/signal_cluster_view.xml b/packages/SystemUI/res/layout/signal_cluster_view.xml
index 198e658..c634cd6 100644
--- a/packages/SystemUI/res/layout/signal_cluster_view.xml
+++ b/packages/SystemUI/res/layout/signal_cluster_view.xml
@@ -77,7 +77,7 @@
     </FrameLayout>
     <View
         android:id="@+id/wifi_signal_spacer"
-        android:layout_width="4dp"
+        android:layout_width="@dimen/status_bar_wifi_signal_spacer_width"
         android:layout_height="4dp"
         android:visibility="gone"
         />
@@ -112,7 +112,7 @@
     </FrameLayout>
     <View
         android:id="@+id/wifi_airplane_spacer"
-        android:layout_width="4dp"
+        android:layout_width="@dimen/status_bar_airplane_spacer_width"
         android:layout_height="4dp"
         android:visibility="gone"
         />
diff --git a/packages/SystemUI/res/layout/status_bar.xml b/packages/SystemUI/res/layout/status_bar.xml
index bea9f78..39c16d7 100644
--- a/packages/SystemUI/res/layout/status_bar.xml
+++ b/packages/SystemUI/res/layout/status_bar.xml
@@ -70,7 +70,8 @@
                 android:layout_width="wrap_content"
                 android:layout_height="match_parent"
                 android:singleLine="true"
-                android:paddingStart="7dp"
+                android:paddingStart="@dimen/status_bar_clock_starting_padding"
+                android:paddingEnd="@dimen/status_bar_clock_end_padding"
                 android:gravity="center_vertical|start"
                 />
         </com.android.keyguard.AlphaOptimizedLinearLayout>
diff --git a/packages/SystemUI/res/layout/system_icons.xml b/packages/SystemUI/res/layout/system_icons.xml
index 943e846..e9448db 100644
--- a/packages/SystemUI/res/layout/system_icons.xml
+++ b/packages/SystemUI/res/layout/system_icons.xml
@@ -30,11 +30,11 @@
         android:id="@+id/signal_cluster"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:layout_marginStart="2.5dp"/>
+        android:layout_marginStart="@dimen/signal_cluster_margin_start"/>
 
     <!-- battery must be padded below to match assets -->
     <com.android.systemui.BatteryMeterView android:id="@+id/battery"
-        android:layout_height="14.5dp"
-        android:layout_width="9.5dp"
+        android:layout_height="@dimen/status_bar_battery_icon_height"
+        android:layout_width="@dimen/status_bar_battery_icon_width"
         android:layout_marginBottom="@dimen/battery_margin_bottom"/>
 </LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values-sw600dp/config.xml b/packages/SystemUI/res/values-sw600dp/config.xml
index 6795da4..17ff195 100644
--- a/packages/SystemUI/res/values-sw600dp/config.xml
+++ b/packages/SystemUI/res/values-sw600dp/config.xml
@@ -35,7 +35,7 @@
 
     <!-- Recents: The relative range of visible tasks from the current scroll position
          while the stack is focused. -->
-    <item name="recents_layout_focused_range_min" format="float" type="integer">-4</item>
+    <item name="recents_layout_focused_range_min" format="float" type="integer">-3</item>
     <item name="recents_layout_focused_range_max" format="float" type="integer">3</item>
 
     <!-- Recents: The relative range of visible tasks from the current scroll position
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index a80a5de..e7fd295 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -75,13 +75,13 @@
     <color name="notification_legacy_background_color">#ff1a1a1a</color>
 
     <!-- The color of the material notification background -->
-    <color name="notification_material_background_color">#fffafafa</color>
+    <color name="notification_material_background_color">#ffffffff</color>
 
     <!-- The color of the material notification background when dimmed -->
-    <color name="notification_material_background_dimmed_color">#d4ffffff</color>
+    <color name="notification_material_background_dimmed_color">#f2ffffff</color>
 
     <!-- The color of the material notification background when low priority -->
-    <color name="notification_material_background_low_priority_color">#ffe0e0e0</color>
+    <color name="notification_material_background_low_priority_color">#fff5f5f5</color>
 
     <!-- The color of the material notification background for media notifications when no custom
          color is specified -->
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index e98ec82..7a87314 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -147,6 +147,9 @@
     <!-- The duration for animating the task decorations in after transitioning from an app. -->
     <integer name="recents_task_enter_from_app_duration">200</integer>
 
+    <!-- The duration for animating the task decorations in after transitioning from an app. -->
+    <integer name="recents_task_enter_from_affiliated_app_duration">125</integer>
+
     <!-- The duration for animating the task decorations out before transitioning to an app. -->
     <integer name="recents_task_exit_to_app_duration">125</integer>
 
@@ -194,7 +197,7 @@
 
     <!-- Recents: The relative range of visible tasks from the current scroll position
          while the stack is focused. -->
-    <item name="recents_layout_focused_range_min" format="float" type="integer">-4</item>
+    <item name="recents_layout_focused_range_min" format="float" type="integer">-3</item>
     <item name="recents_layout_focused_range_max" format="float" type="integer">3</item>
 
     <!-- Recents: The relative range of visible tasks from the current scroll position
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 47eb05a..d7bd86f 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -33,9 +33,30 @@
     <!-- Height of notification icons in the status bar -->
     <dimen name="status_bar_icon_size">@*android:dimen/status_bar_icon_size</dimen>
 
-    <!-- The font size for the clock -->
+    <!-- Height of the battery icon in the status bar. -->
+    <dimen name="status_bar_battery_icon_height">14.5dp</dimen>
+
+    <!-- Width of the battery icon in the status bar. -->
+    <dimen name="status_bar_battery_icon_width">9.5dp</dimen>
+
+    <!-- The font size for the clock in the status bar. -->
     <dimen name="status_bar_clock_size">14sp</dimen>
 
+    <!-- The starting padding for the clock in the status bar. -->
+    <dimen name="status_bar_clock_starting_padding">7dp</dimen>
+
+    <!-- The end padding for the clock in the status bar. -->
+    <dimen name="status_bar_clock_end_padding">0dp</dimen>
+
+    <!-- Spacing after the wifi signals that is present if there are any icons following it. -->
+    <dimen name="status_bar_wifi_signal_spacer_width">4dp</dimen>
+
+    <!-- Spacing before the airplane mode icon if there are any icons preceding it. -->
+    <dimen name="status_bar_airplane_spacer_width">4dp</dimen>
+
+    <!-- The amount to scale each of the status bar icons by. A value of 1 means no scaling. -->
+    <item name="status_bar_icon_scale_factor" format="float" type="dimen">1.0</item>
+
     <!-- Height of a small notification in the status bar-->
     <dimen name="notification_min_height">84dp</dimen>
 
@@ -191,8 +212,13 @@
     <!-- Default distance from each snap target that GlowPadView considers a "hit" -->
     <dimen name="glowpadview_inner_radius">15dip</dimen>
 
-    <!-- The size of the application icon in the recents task view. -->
-    <dimen name="recents_task_view_application_icon_size">48dp</dimen>
+    <!-- The size of the icon in the recents task view header. -->
+    <dimen name="recents_task_view_header_icon_width">64dp</dimen>
+    <dimen name="recents_task_view_header_icon_height">@dimen/recents_task_bar_height</dimen>
+
+    <!-- The size of a button in the recents task view header. -->
+    <dimen name="recents_task_view_header_button_width">@dimen/recents_task_bar_height</dimen>
+    <dimen name="recents_task_view_header_button_height">@dimen/recents_task_bar_height</dimen>
 
     <!-- The radius of the rounded corners on a task view. -->
     <dimen name="recents_task_view_rounded_corners_radius">2dp</dimen>
@@ -210,7 +236,7 @@
     <dimen name="recents_task_view_highlight">1dp</dimen>
 
     <!-- The amount to offset when animating into an affiliate group. -->
-    <dimen name="recents_task_view_affiliate_group_enter_offset">64dp</dimen>
+    <dimen name="recents_task_view_affiliate_group_enter_offset">32dp</dimen>
 
     <!-- The height of a task view bar. -->
     <dimen name="recents_task_bar_height">56dp</dimen>
@@ -516,6 +542,9 @@
 
     <dimen name="fake_shadow_size">8dp</dimen>
 
+    <!-- Starting margin before the signal cluster -->
+    <dimen name="signal_cluster_margin_start">2.5dp</dimen>
+
     <!-- Padding between signal cluster and battery icon -->
     <dimen name="signal_cluster_battery_padding">7dp</dimen>
 
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index 13128b7..0bd350b 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -38,8 +38,8 @@
     <item type="id" name="qs_icon_tag"/>
     <item type="id" name="scrim"/>
     <item type="id" name="scrim_target"/>
-    <item type="id" name="hun_scrim_alpha_start"/>
-    <item type="id" name="hun_scrim_alpha_end"/>
+    <item type="id" name="scrim_alpha_start"/>
+    <item type="id" name="scrim_alpha_end"/>
     <item type="id" name="notification_power"/>
     <item type="id" name="notification_screenshot"/>
     <item type="id" name="notification_hidden"/>
@@ -50,6 +50,11 @@
 
     <!-- For notification icons for which targetSdk < L, this caches whether the icon is grayscale -->
     <item type="id" name="icon_is_grayscale" />
+    <item type="id" name="clip_children_tag" />
+    <item type="id" name="clip_children_set_tag" />
+    <item type="id" name="clip_to_padding_tag" />
+    <item type="id" name="image_icon_tag" />
+    <item type="id" name="contains_transformed_view" />
     <item type="id" name="is_clicked_heads_up_tag" />
 </resources>
 
diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
index d28da41..fd4161f 100644
--- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
@@ -367,6 +367,7 @@
         }
         anim.addListener(new AnimatorListenerAdapter() {
             public void onAnimationEnd(Animator animation) {
+                updateSwipeProgressFromOffset(animView, canAnimViewBeDismissed);
                 mCallback.onChildDismissed(view);
                 if (endAction != null) {
                     endAction.run();
@@ -381,9 +382,17 @@
                 updateSwipeProgressFromOffset(animView, canAnimViewBeDismissed);
             }
         });
+        prepareDismissAnimation(animView, anim);
         anim.start();
     }
 
+    /**
+     * Called to update the dismiss animation.
+     */
+    protected void prepareDismissAnimation(View view, Animator anim) {
+        // Do nothing
+    }
+
     public void snapChild(final View view, float velocity) {
         final View animView = mCallback.getChildContentView(view);
         final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(animView);
@@ -401,14 +410,14 @@
                 mCallback.onChildSnappedBack(animView);
             }
         });
-        updateSnapBackAnimation(anim);
+        prepareSnapBackAnimation(animView, anim);
         anim.start();
     }
 
     /**
      * Called to update the snap back animation.
      */
-    protected void updateSnapBackAnimation(Animator anim) {
+    protected void prepareSnapBackAnimation(View view, Animator anim) {
         // Do nothing
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
index db55f28..3a3b19d 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
@@ -183,7 +183,7 @@
         RecentsConfiguration config = Recents.getConfiguration();
         RecentsActivityLaunchState launchState = config.getLaunchState();
         if (!plan.hasTasks()) {
-            loader.preloadTasks(plan, launchState.launchedFromHome);
+            loader.preloadTasks(plan, -1, launchState.launchedFromHome);
         }
         RecentsTaskLoadPlan.Options loadOpts = new RecentsTaskLoadPlan.Options();
         loadOpts.runningTaskId = launchState.launchedToTaskId;
@@ -192,24 +192,14 @@
         loader.loadTasks(this, plan, loadOpts);
 
         TaskStack stack = plan.getTaskStack();
-        ArrayList<Task> tasks = stack.getStackTasks();
-        int taskCount = stack.getTaskCount();
         mRecentsView.setTaskStack(stack);
 
-        // Mark the task that is the launch target
-        int launchTaskIndexInStack = 0;
-        if (launchState.launchedToTaskId != -1) {
-            for (int j = 0; j < taskCount; j++) {
-                Task t = tasks.get(j);
-                if (t.key.id == launchState.launchedToTaskId) {
-                    t.isLaunchTarget = true;
-                    launchTaskIndexInStack = tasks.size() - j - 1;
-                    break;
-                }
-            }
-        }
-
         // Animate the SystemUI scrims into view
+        Task launchTarget = stack.getLaunchTarget();
+        int taskCount = stack.getTaskCount();
+        int launchTaskIndexInStack = launchTarget != null
+                ? stack.indexOfStackTask(launchTarget)
+                : 0;
         boolean hasStatusBarScrim = taskCount > 0;
         boolean animateStatusBarScrim = launchState.launchedFromHome;
         boolean hasNavBarScrim = (taskCount > 0) && !config.hasTransposedNavBar;
@@ -527,7 +517,7 @@
             launchOpts.loadThumbnails = false;
             launchOpts.onlyLoadForCache = true;
             RecentsTaskLoadPlan loadPlan = loader.createLoadPlan(this);
-            loader.preloadTasks(loadPlan, false);
+            loader.preloadTasks(loadPlan, -1, false);
             loader.loadTasks(this, loadPlan, launchOpts);
             EventBus.getDefault().send(new TaskStackUpdatedEvent(loadPlan.getTaskStack()));
         }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsDebugFlags.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsDebugFlags.java
index 3151fd7..881aa6a 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsDebugFlags.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsDebugFlags.java
@@ -37,6 +37,8 @@
         public static final boolean EnableSearchBar = false;
         // This disables the bitmap and icon caches
         public static final boolean DisableBackgroundCache = false;
+        // Enables the task affiliations
+        public static final boolean EnableAffiliatedTaskGroups = true;
         // Enables the simulated task affiliations
         public static final boolean EnableSimulatedTaskGroups = false;
         // Defines the number of mock task affiliations per group
@@ -75,7 +77,7 @@
      * @return whether we are enabling the fast toggle indicator.
      */
     public boolean isFastToggleIndicatorEnabled() {
-        return mFastToggleIndicator;
+        return mFastToggleRecents && mFastToggleIndicator;
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
index 7c25d24..3eee087 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
@@ -118,7 +118,7 @@
 
                 // Load the next task only if we aren't svelte
                 RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext);
-                loader.preloadTasks(plan, true);
+                loader.preloadTasks(plan, -1, true /* isTopTaskHome */);
                 RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options();
                 // This callback is made when a new activity is launched and the old one is paused
                 // so ignore the current activity and try and preload the thumbnail for the
@@ -198,7 +198,7 @@
         // We can use a new plan since the caches will be the same.
         RecentsTaskLoader loader = Recents.getTaskLoader();
         RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext);
-        loader.preloadTasks(plan, true /* isTopTaskHome */);
+        loader.preloadTasks(plan, -1, true /* isTopTaskHome */);
         RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options();
         launchOpts.numVisibleTasks = loader.getIconCacheSize();
         launchOpts.numVisibleTaskThumbnails = loader.getThumbnailCacheSize();
@@ -370,7 +370,7 @@
         sInstanceLoadPlan = loader.createLoadPlan(mContext);
         if (topTask != null && !ssp.isRecentsTopMost(topTask, topTaskHome)) {
             sInstanceLoadPlan.preloadRawTasks(topTaskHome.value);
-            loader.preloadTasks(sInstanceLoadPlan, topTaskHome.value);
+            loader.preloadTasks(sInstanceLoadPlan, topTask.id, topTaskHome.value);
             TaskStack stack = sInstanceLoadPlan.getTaskStack();
             if (stack.getTaskCount() > 0) {
                 // We try and draw the thumbnail transition bitmap in parallel before
@@ -399,7 +399,7 @@
         SystemServicesProxy ssp = Recents.getSystemServices();
         RecentsTaskLoader loader = Recents.getTaskLoader();
         RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext);
-        loader.preloadTasks(plan, true /* isTopTaskHome */);
+        loader.preloadTasks(plan, -1, true /* isTopTaskHome */);
         TaskStack focusedStack = plan.getTaskStack();
 
         // Return early if there are no tasks in the focused stack
@@ -451,7 +451,7 @@
         SystemServicesProxy ssp = Recents.getSystemServices();
         RecentsTaskLoader loader = Recents.getTaskLoader();
         RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext);
-        loader.preloadTasks(plan, true /* isTopTaskHome */);
+        loader.preloadTasks(plan, -1, true /* isTopTaskHome */);
         TaskStack focusedStack = plan.getTaskStack();
 
         // Return early if there are no tasks in the focused stack
@@ -756,29 +756,18 @@
     private TaskViewTransform getThumbnailTransitionTransform(TaskStack stack,
             TaskStackView stackView, int runningTaskId, Task runningTaskOut) {
         // Find the running task in the TaskStack
-        Task task = null;
-        ArrayList<Task> tasks = stack.getStackTasks();
-        if (runningTaskId != -1) {
-            // Otherwise, try and find the task with the
-            int taskCount = tasks.size();
-            for (int i = taskCount - 1; i >= 0; i--) {
-                Task t = tasks.get(i);
-                if (t.key.id == runningTaskId) {
-                    task = t;
-                    runningTaskOut.copyFrom(t);
-                    break;
-                }
-            }
-        }
-        if (task == null) {
+        Task launchTask = stack.getLaunchTarget();
+        if (launchTask != null) {
+            runningTaskOut.copyFrom(launchTask);
+        } else {
             // If no task is specified or we can not find the task just use the front most one
-            task = tasks.get(tasks.size() - 1);
-            runningTaskOut.copyFrom(task);
+            launchTask = stack.getStackFrontMostTask();
+            runningTaskOut.copyFrom(launchTask);
         }
 
         // Get the transform for the running task
         stackView.getScroller().setStackScrollToInitialState();
-        mTmpTransform = stackView.getStackAlgorithm().getStackTransform(task,
+        mTmpTransform = stackView.getStackAlgorithm().getStackTransform(launchTask,
                 stackView.getScroller().getStackScroll(), mTmpTransform, null);
         return mTmpTransform;
     }
@@ -826,7 +815,7 @@
             sInstanceLoadPlan = loader.createLoadPlan(mContext);
         }
         if (mReloadTasks || mTriggeredFromAltTab || !sInstanceLoadPlan.hasTasks()) {
-            loader.preloadTasks(sInstanceLoadPlan, isTopTaskHome);
+            loader.preloadTasks(sInstanceLoadPlan, topTask.id, isTopTaskHome);
         }
         TaskStack stack = sInstanceLoadPlan.getTaskStack();
 
diff --git a/packages/SystemUI/src/com/android/systemui/recents/history/RecentsHistoryAdapter.java b/packages/SystemUI/src/com/android/systemui/recents/history/RecentsHistoryAdapter.java
index 80597bc..f6655c7 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/history/RecentsHistoryAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/history/RecentsHistoryAdapter.java
@@ -36,6 +36,7 @@
 import com.android.systemui.recents.model.RecentsTaskLoader;
 import com.android.systemui.recents.model.Task;
 import com.android.systemui.recents.model.TaskStack;
+import com.android.systemui.recents.views.TaskViewAnimation;
 
 import java.util.ArrayList;
 import java.util.Calendar;
@@ -143,7 +144,6 @@
 
     private Context mContext;
     private LayoutInflater mLayoutInflater;
-    private final List<Task> mTasks = new ArrayList<>();
     private final List<Row> mRows = new ArrayList<>();
     private final SparseIntArray mTaskRowCount = new SparseIntArray();
     private TaskStack mStack;
@@ -268,8 +268,8 @@
 
     public void onTaskRemoved(Task task, int position) {
         // Since this is removed from the history, we need to update the stack as well to ensure
-        // that the model is correct
-        mStack.removeTask(task);
+        // that the model is correct. Since the stack is hidden, we can update it immediately.
+        mStack.removeTask(task, TaskViewAnimation.IMMEDIATE);
         removeTaskRow(position);
         if (mRows.isEmpty()) {
             dismissHistory();
diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/RectFEvaluator.java b/packages/SystemUI/src/com/android/systemui/recents/misc/RectFEvaluator.java
new file mode 100644
index 0000000..72511de
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/misc/RectFEvaluator.java
@@ -0,0 +1,52 @@
+/*
+ * 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.misc;
+
+import android.animation.TypeEvaluator;
+import android.graphics.RectF;
+
+/**
+ * This evaluator can be used to perform type interpolation between <code>RectF</code> values.
+ */
+public class RectFEvaluator implements TypeEvaluator<RectF> {
+
+    private RectF mRect = new RectF();
+
+    /**
+     * This function returns the result of linearly interpolating the start and
+     * end Rect values, with <code>fraction</code> representing the proportion
+     * between the start and end values. The calculation is a simple parametric
+     * calculation on each of the separate components in the Rect objects
+     * (left, top, right, and bottom).
+     *
+     * <p>The object returned will be the <code>reuseRect</code> passed into the constructor.</p>
+     *
+     * @param fraction   The fraction from the starting to the ending values
+     * @param startValue The start Rect
+     * @param endValue   The end Rect
+     * @return A linear interpolation between the start and end values, given the
+     *         <code>fraction</code> parameter.
+     */
+    @Override
+    public RectF evaluate(float fraction, RectF startValue, RectF endValue) {
+        float left = startValue.left + ((endValue.left - startValue.left) * fraction);
+        float top = startValue.top + ((endValue.top - startValue.top) * fraction);
+        float right = startValue.right + ((endValue.right - startValue.right) * fraction);
+        float bottom = startValue.bottom + ((endValue.bottom - startValue.bottom) * fraction);
+        mRect.set(left, top, right, bottom);
+        return mRect;
+    }
+}
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 adc1e92..3f52ae8 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java
@@ -30,6 +30,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
@@ -554,39 +555,41 @@
         }
     }
 
-    /** Returns the activity label */
-    public String getActivityLabel(ActivityInfo info) {
+    /**
+     * Returns the activity label, badging if necessary.
+     */
+    public String getBadgedActivityLabel(ActivityInfo info, int userId) {
         if (mPm == null) return null;
 
         // If we are mocking, then return a mock label
         if (RecentsDebugFlags.Static.EnableSystemServicesProxy) {
-            return "Recent Task";
+            return "Recent Task: " + userId;
         }
 
-        return info.loadLabel(mPm).toString();
+        return getBadgedLabel(info.loadLabel(mPm).toString(), userId);
     }
 
-    /** Returns the application label */
-    public String getApplicationLabel(Intent baseIntent, int userId) {
+    /**
+     * Returns the application label, badging if necessary.
+     */
+    public String getBadgedApplicationLabel(ApplicationInfo appInfo, int userId) {
         if (mPm == null) return null;
 
         // If we are mocking, then return a mock label
         if (RecentsDebugFlags.Static.EnableSystemServicesProxy) {
-            return "Recent Task";
+            return "Recent Task App: " + userId;
         }
 
-        ResolveInfo ri = mPm.resolveActivityAsUser(baseIntent, 0, userId);
-        CharSequence label = (ri != null) ? ri.loadLabel(mPm) : null;
-        return (label != null) ? label.toString() : null;
+        return getBadgedLabel(appInfo.loadLabel(mPm).toString(), userId);
     }
 
-    /** Returns the content description for a given task */
-    public String getContentDescription(Intent baseIntent, int userId, String activityLabel,
-            Resources res) {
-        String applicationLabel = getApplicationLabel(baseIntent, userId);
-        if (applicationLabel == null) {
-            return getBadgedLabel(activityLabel, userId);
-        }
+    /**
+     * Returns the content description for a given task, badging it if necessary.  The content
+     * description joins the app and activity labels.
+     */
+    public String getBadgedContentDescription(ActivityInfo info, int userId, Resources res) {
+        String activityLabel = info.loadLabel(mPm).toString();
+        String applicationLabel = info.applicationInfo.loadLabel(mPm).toString();
         String badgedApplicationLabel = getBadgedLabel(applicationLabel, userId);
         return applicationLabel.equals(activityLabel) ? badgedApplicationLabel
                 : res.getString(R.string.accessibility_recents_task_header,
@@ -610,6 +613,22 @@
     }
 
     /**
+     * Returns the application icon for the ApplicationInfo for a user, badging if
+     * necessary.
+     */
+    public Drawable getBadgedApplicationIcon(ApplicationInfo appInfo, int userId) {
+        if (mPm == null) return null;
+
+        // If we are mocking, then return a mock label
+        if (RecentsDebugFlags.Static.EnableSystemServicesProxy) {
+            return new ColorDrawable(0xFF666666);
+        }
+
+        Drawable icon = appInfo.loadIcon(mPm);
+        return getBadgedIcon(icon, userId);
+    }
+
+    /**
      * Returns the task description icon, loading and badging it if it necessary.
      */
     public Drawable getBadgedTaskDescriptionIcon(ActivityManager.TaskDescription taskDescription,
diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java
index 822ad77..9cdd703 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java
@@ -25,7 +25,8 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.ArraySet;
-
+import android.util.SparseArray;
+import android.util.SparseIntArray;
 import com.android.systemui.Prefs;
 import com.android.systemui.R;
 import com.android.systemui.recents.Recents;
@@ -92,7 +93,7 @@
     }
 
     /**
-     * An optimization to preload the raw list of tasks.  The raw tasks are saved in least-recent
+     * An optimization to preload the raw list of tasks. The raw tasks are saved in least-recent
      * to most-recent order.
      */
     public synchronized void preloadRawTasks(boolean isTopTaskHome) {
@@ -107,7 +108,7 @@
     }
 
     /**
-     * Preloads the list of recent tasks from the system.  After this call, the TaskStack will
+     * Preloads the list of recent tasks from the system. After this call, the TaskStack will
      * have a list of all the recent tasks with their metadata, not including icons or
      * thumbnails which were not cached and have to be loaded.
      *
@@ -115,13 +116,16 @@
      * - least-recent to most-recent stack tasks
      * - least-recent to most-recent freeform tasks
      */
-    public synchronized void preloadPlan(RecentsTaskLoader loader, boolean isTopTaskHome) {
+    public synchronized void preloadPlan(RecentsTaskLoader loader, int topTaskId,
+            boolean isTopTaskHome) {
         Resources res = mContext.getResources();
         ArrayList<Task> allTasks = new ArrayList<>();
         if (mRawTasks == null) {
             preloadRawTasks(isTopTaskHome);
         }
 
+        SparseArray<Task.TaskKey> affiliatedTasks = new SparseArray<>();
+        SparseIntArray affiliatedTaskCounts = new SparseIntArray();
         String dismissDescFormat = mContext.getString(
                 R.string.accessibility_recents_item_will_be_dismissed);
         long lastStackActiveTime = Prefs.getLong(mContext,
@@ -131,6 +135,17 @@
         for (int i = 0; i < taskCount; i++) {
             ActivityManager.RecentTaskInfo t = mRawTasks.get(i);
 
+            // Affiliated tasks are returned in a specific order from ActivityManager but without a
+            // lastActiveTime since it hasn't yet been started. However, we later sort the task list
+            // by lastActiveTime, which rearranges the tasks. For now, we need to workaround this
+            // by updating the lastActiveTime of this task to the lastActiveTime of the task it is
+            // affiliated with, in the same order that we encounter it in the original list (just
+            // its index in the task group for the task it is affiliated with).
+            if (t.persistentId != t.affiliatedTaskId) {
+                t.lastActiveTime = affiliatedTasks.get(t.affiliatedTaskId).lastActiveTime +
+                        affiliatedTaskCounts.get(t.affiliatedTaskId, 0) + 1;
+            }
+
             // Compose the task key
             Task.TaskKey taskKey = new Task.TaskKey(t.persistentId, t.stackId, t.baseIntent,
                     t.userId, t.firstActiveTime, t.lastActiveTime);
@@ -140,13 +155,14 @@
             boolean isFreeformTask = SystemServicesProxy.isFreeformStack(t.stackId);
             boolean isStackTask = isFreeformTask || (!isHistoricalTask(t) ||
                     (t.lastActiveTime >= lastStackActiveTime && i >= (taskCount - MIN_NUM_TASKS)));
+            boolean isLaunchTarget = taskKey.id == topTaskId;
             if (isStackTask && newLastStackActiveTime < 0) {
                 newLastStackActiveTime = t.lastActiveTime;
             }
 
             // Load the title, icon, and color
             String title = loader.getAndUpdateActivityTitle(taskKey, t.taskDescription);
-            String contentDescription = loader.getAndUpdateContentDescription(taskKey, title, res);
+            String contentDescription = loader.getAndUpdateContentDescription(taskKey, res);
             String dismissDescription = String.format(dismissDescFormat, contentDescription);
             Drawable icon = isStackTask
                     ? loader.getAndUpdateActivityIcon(taskKey, t.taskDescription, res, false)
@@ -157,9 +173,11 @@
             // Add the task to the stack
             Task task = new Task(taskKey, t.affiliatedTaskId, t.affiliatedTaskColor, icon,
                     thumbnail, title, contentDescription, dismissDescription, activityColor,
-                    !isStackTask, t.bounds, t.taskDescription);
+                    !isStackTask, isLaunchTarget, t.bounds, t.taskDescription);
 
             allTasks.add(task);
+            affiliatedTaskCounts.put(taskKey.id, affiliatedTaskCounts.get(taskKey.id, 0) + 1);
+            affiliatedTasks.put(taskKey.id, taskKey);
         }
         if (newLastStackActiveTime != -1) {
             Prefs.putLong(mContext, Prefs.Key.OVERVIEW_LAST_STACK_TASK_ACTIVE_TIME,
diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java
index 28338d83..44ad239 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java
@@ -314,8 +314,8 @@
     }
 
     /** Preloads recents tasks using the specified plan to store the output. */
-    public void preloadTasks(RecentsTaskLoadPlan plan, boolean isTopTaskHome) {
-        plan.preloadPlan(this, isTopTaskHome);
+    public void preloadTasks(RecentsTaskLoadPlan plan, int topTaskId, boolean isTopTaskHome) {
+        plan.preloadPlan(this, topTaskId, isTopTaskHome);
     }
 
     /** Begins loading the heavy task data according to the specified options. */
@@ -436,7 +436,7 @@
         // All short paths failed, load the label from the activity info and cache it
         ActivityInfo activityInfo = getAndUpdateActivityInfo(taskKey);
         if (activityInfo != null) {
-            label = ssp.getActivityLabel(activityInfo);
+            label = ssp.getBadgedActivityLabel(activityInfo, taskKey.userId);
             mActivityLabelCache.put(taskKey, label);
             return label;
         }
@@ -449,8 +449,7 @@
      * Returns the cached task content description if the task key is not expired, updating the
      * cache if it is.
      */
-    String getAndUpdateContentDescription(Task.TaskKey taskKey, String activityLabel,
-            Resources res) {
+    String getAndUpdateContentDescription(Task.TaskKey taskKey, Resources res) {
         SystemServicesProxy ssp = Recents.getSystemServices();
 
         // Return the cached content description if it exists
@@ -458,13 +457,11 @@
         if (label != null) {
             return label;
         }
-        // If the given activity label is empty, don't compute or cache the content description
-        if (activityLabel.isEmpty()) {
-            return "";
-        }
 
-        label = ssp.getContentDescription(taskKey.baseIntent, taskKey.userId, activityLabel, res);
-        if (label != null) {
+        // All short paths failed, load the label from the activity info and cache it
+        ActivityInfo activityInfo = getAndUpdateActivityInfo(taskKey);
+        if (activityInfo != null) {
+            label = ssp.getBadgedContentDescription(activityInfo, taskKey.userId, res);
             mContentDescriptionCache.put(taskKey, label);
             return label;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/Task.java b/packages/SystemUI/src/com/android/systemui/recents/model/Task.java
index 29e7077..193bd17 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/model/Task.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/Task.java
@@ -154,7 +154,8 @@
     public Task(TaskKey key, int affiliationTaskId, int affiliationColor, Drawable icon,
                 Bitmap thumbnail, String title, String contentDescription,
                 String dismissDescription, int colorPrimary, boolean isHistorical,
-                Rect bounds, ActivityManager.TaskDescription taskDescription) {
+                boolean isLaunchTarget, Rect bounds,
+                ActivityManager.TaskDescription taskDescription) {
         boolean isInAffiliationGroup = (affiliationTaskId != key.id);
         boolean hasAffiliationGroupColor = isInAffiliationGroup && (affiliationColor != 0);
         this.key = key;
@@ -170,6 +171,7 @@
                 Color.WHITE) > 3f;
         this.bounds = bounds;
         this.taskDescription = taskDescription;
+        this.isLaunchTarget = isLaunchTarget;
         this.isHistorical = isHistorical;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java
index 327cdf8..c73273e 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java
@@ -42,6 +42,7 @@
 import com.android.systemui.recents.misc.SystemServicesProxy;
 import com.android.systemui.recents.misc.Utilities;
 import com.android.systemui.recents.views.DropTarget;
+import com.android.systemui.recents.views.TaskViewAnimation;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -226,12 +227,12 @@
          * Notifies when a task has been removed from the stack.
          */
         void onStackTaskRemoved(TaskStack stack, Task removedTask, boolean wasFrontMostTask,
-            Task newFrontMostTask);
+            Task newFrontMostTask, TaskViewAnimation animation);
 
         /**
          * Notifies when a task has been removed from the history.
          */
-        void onHistoryTaskRemoved(TaskStack stack, Task removedTask);
+        void onHistoryTaskRemoved(TaskStack stack, Task removedTask, TaskViewAnimation animation);
     }
 
     /**
@@ -520,21 +521,24 @@
         }
     }
 
-    /** Removes a task */
-    public void removeTask(Task t) {
+    /**
+     * Removes a task from the stack, with an additional {@param animation} hint to the callbacks on
+     * how they should update themselves.
+     */
+    public void removeTask(Task t, TaskViewAnimation animation) {
         if (mStackTaskList.contains(t)) {
             boolean wasFrontMostTask = (getStackFrontMostTask() == t);
             removeTaskImpl(mStackTaskList, t);
             Task newFrontMostTask = getStackFrontMostTask();
             if (mCb != null) {
                 // Notify that a task has been removed
-                mCb.onStackTaskRemoved(this, t, wasFrontMostTask, newFrontMostTask);
+                mCb.onStackTaskRemoved(this, t, wasFrontMostTask, newFrontMostTask, animation);
             }
         } else if (mHistoryTaskList.contains(t)) {
             removeTaskImpl(mHistoryTaskList, t);
             if (mCb != null) {
                 // Notify that a task has been removed
-                mCb.onHistoryTaskRemoved(this, t);
+                mCb.onHistoryTaskRemoved(this, t, animation);
             }
         }
         mRawTaskList.remove(t);
@@ -564,7 +568,8 @@
             Task task = mRawTaskList.get(i);
             if (!newTasksMap.containsKey(task.key)) {
                 if (notifyStackChanges) {
-                    mCb.onStackTaskRemoved(this, task, i == (taskCount - 1), null);
+                    mCb.onStackTaskRemoved(this, task, i == (taskCount - 1), null,
+                            TaskViewAnimation.IMMEDIATE);
                 }
             }
             task.setGroup(null);
@@ -843,12 +848,17 @@
             for (int i = 0; i < taskCount; i++) {
                 Task t = tasks.get(i);
                 TaskGrouping group;
-                int affiliation = t.affiliationTaskId > 0 ? t.affiliationTaskId :
-                        IndividualTaskIdOffset + t.key.id;
-                if (mAffinitiesGroups.containsKey(affiliation)) {
-                    group = getGroupWithAffiliation(affiliation);
+                if (RecentsDebugFlags.Static.EnableAffiliatedTaskGroups) {
+                    int affiliation = t.affiliationTaskId > 0 ? t.affiliationTaskId :
+                            IndividualTaskIdOffset + t.key.id;
+                    if (mAffinitiesGroups.containsKey(affiliation)) {
+                        group = getGroupWithAffiliation(affiliation);
+                    } else {
+                        group = new TaskGrouping(affiliation);
+                        addGroup(group);
+                    }
                 } else {
-                    group = new TaskGrouping(affiliation);
+                    group = new TaskGrouping(t.key.id);
                     addGroup(group);
                 }
                 group.addTask(t);
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
index e727652..e448101 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
@@ -513,7 +513,7 @@
                     taskViewRect.right, taskViewRect.bottom);
 
             // Remove the task view after it is docked
-            mTaskStackView.updateLayout(false /* boundScroll */);
+            mTaskStackView.updateLayoutAlgorithm(false /* boundScroll */);
             stackLayout.getStackTransform(event.task, stackScroller.getStackScroll(), tmpTransform,
                     null);
             tmpTransform.alpha = 0;
@@ -529,11 +529,14 @@
                                     ssp.startTaskInDockedMode(getContext(), event.taskView,
                                             event.task.key.id, dockState.createMode);
 
-                                    mTaskStackView.getStack().removeTask(event.task);
+                                    // Animate the stack accordingly
+                                    TaskViewAnimation stackAnim = new TaskViewAnimation(
+                                            TaskStackView.DEFAULT_SYNC_STACK_DURATION,
+                                            mFastOutSlowInInterpolator);
+                                    mTaskStackView.getStack().removeTask(event.task, stackAnim);
                                 }
                             }));
 
-
             MetricsLogger.action(mContext,
                     MetricsLogger.ACTION_WINDOW_DOCK_DRAG_DROP);
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackAnimationHelper.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackAnimationHelper.java
index 2930f4d..ccbb329 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackAnimationHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackAnimationHelper.java
@@ -130,6 +130,7 @@
                     // Move the task view slightly lower so we can animate it in
                     RectF bounds = new RectF(mTmpTransform.rect);
                     bounds.offset(0, taskViewAffiliateGroupEnterOffset);
+                    tv.setClipViewInStack(false);
                     tv.setAlpha(0f);
                     tv.setLeftTopRightBottom((int) bounds.left, (int) bounds.top,
                             (int) bounds.right, (int) bounds.bottom);
@@ -165,6 +166,8 @@
 
         int taskViewEnterFromAppDuration = res.getInteger(
                 R.integer.recents_task_enter_from_app_duration);
+        int taskViewEnterFromAffiliatedAppDuration = res.getInteger(
+                R.integer.recents_task_enter_from_affiliated_app_duration);
         int taskViewEnterFromHomeDuration = res.getInteger(
                 R.integer.recents_task_enter_from_home_duration);
         int taskViewEnterFromHomeStaggerDelay = res.getInteger(
@@ -174,7 +177,7 @@
         List<TaskView> taskViews = mStackView.getTaskViews();
         int taskViewCount = taskViews.size();
         for (int i = taskViewCount - 1; i >= 0; i--) {
-            TaskView tv = taskViews.get(i);
+            final TaskView tv = taskViews.get(i);
             Task task = tv.getTask();
             boolean currentTaskOccludesLaunchTarget = false;
             if (launchTargetTask != null) {
@@ -195,8 +198,14 @@
                     // Animate the task up if it was occluding the launch target
                     if (currentTaskOccludesLaunchTarget) {
                         TaskViewAnimation taskAnimation = new TaskViewAnimation(
-                                taskViewEnterFromAppDuration, PhoneStatusBar.ALPHA_IN,
-                                postAnimationTrigger.decrementOnAnimationEnd());
+                                taskViewEnterFromAffiliatedAppDuration, PhoneStatusBar.ALPHA_IN,
+                                new AnimatorListenerAdapter() {
+                                    @Override
+                                    public void onAnimationEnd(Animator animation) {
+                                        postAnimationTrigger.decrement();
+                                        tv.setClipViewInStack(false);
+                                    }
+                                });
                         postAnimationTrigger.increment();
                         mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
                     }
@@ -286,7 +295,7 @@
             } else if (currentTaskOccludesLaunchTarget) {
                 // Animate this task out of view
                 TaskViewAnimation taskAnimation = new TaskViewAnimation(
-                        taskViewExitToAppDuration, mFastOutLinearInInterpolator,
+                        taskViewExitToAppDuration, PhoneStatusBar.ALPHA_OUT,
                         postAnimationTrigger.decrementOnAnimationEnd());
                 postAnimationTrigger.increment();
 
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
index 68ff63c..e99509c 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
@@ -373,7 +373,7 @@
      * Computes the minimum and maximum scroll progress values and the progress values for each task
      * in the stack.
      */
-    void update(TaskStack stack, ArraySet<Task> ignoreTasksSet) {
+    void update(TaskStack stack, ArraySet<Task.TaskKey> ignoreTasksSet) {
         SystemServicesProxy ssp = Recents.getSystemServices();
 
         // Clear the progress map
@@ -393,7 +393,7 @@
         ArrayList<Task> stackTasks = new ArrayList<>();
         for (int i = 0; i < tasks.size(); i++) {
             Task task = tasks.get(i);
-            if (ignoreTasksSet.contains(task)) {
+            if (ignoreTasksSet.contains(task.key)) {
                 continue;
             }
             if (task.isFreeformTask()) {
@@ -434,19 +434,25 @@
             mFreeformLayoutAlgorithm.update(freeformTasks, this);
             mInitialScrollP = mMaxScrollP;
         } else {
+            Task launchTask = stack.getLaunchTarget();
+            int launchTaskIndex = launchTask != null
+                    ? stack.indexOfStackTask(launchTask)
+                    : mNumStackTasks - 1;
             if (!ssp.hasFreeformWorkspaceSupport() && mNumStackTasks == 1) {
                 mInitialScrollP = mMinScrollP;
             } else if (getDefaultFocusState() > 0f) {
                 RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState();
                 if (launchState.launchedFromHome) {
-                    mInitialScrollP = Math.max(mMinScrollP, mNumStackTasks - 1);
+                    mInitialScrollP = Math.max(mMinScrollP, Math.min(mMaxScrollP, launchTaskIndex));
                 } else {
-                    mInitialScrollP = Math.max(mMinScrollP, mNumStackTasks - 2);
+                    mInitialScrollP = Math.max(mMinScrollP, Math.min(mMaxScrollP,
+                            launchTaskIndex - 1));
                 }
             } else {
                 float offsetPct = (float) (mTaskRect.height() / 2) / mStackRect.height();
                 float normX = mUnfocusedCurveInterpolator.getX(offsetPct);
-                mInitialScrollP = (mNumStackTasks - 1) - mUnfocusedRange.getAbsoluteX(normX);
+                mInitialScrollP = Math.max(mMinScrollP, Math.min(mMaxScrollP,
+                        launchTaskIndex - mUnfocusedRange.getAbsoluteX(normX)));
             }
         }
     }
@@ -553,7 +559,8 @@
 
             boolean isFrontMostTaskInGroup = task.group == null || task.group.isFrontMostTask(task);
             if (isFrontMostTaskInGroup) {
-                getStackTransform(taskProgress, mInitialScrollP, tmpTransform, null);
+                getStackTransform(taskProgress, mInitialScrollP, tmpTransform, null,
+                        false /* ignoreSingleTaskCase */);
                 float screenY = tmpTransform.rect.top;
                 boolean hasVisibleThumbnail = (prevScreenY - screenY) > taskBarHeight;
                 if (hasVisibleThumbnail) {
@@ -596,13 +603,21 @@
                 return transformOut;
             }
             return getStackTransform(mTaskIndexMap.get(task.key), stackScroll, transformOut,
-                    frontTransform);
+                    frontTransform, false /* ignoreSingleTaskCase */);
         }
     }
 
-    /** Update/get the transform */
+    /**
+     * Update/get the transform.
+     *
+     * @param ignoreSingleTaskCase When set, will ensure that the transform computed does not take
+     *                             into account the special single-task case.  This is only used
+     *                             internally to ensure that we can calculate the transform for any
+     *                             position in the stack.
+     */
     public TaskViewTransform getStackTransform(float taskProgress, float stackScroll,
-            TaskViewTransform transformOut, TaskViewTransform frontTransform) {
+            TaskViewTransform transformOut, TaskViewTransform frontTransform,
+            boolean ignoreSingleTaskCase) {
         SystemServicesProxy ssp = Recents.getSystemServices();
 
         // Compute the focused and unfocused offset
@@ -632,7 +647,7 @@
         int y;
         float z;
         float relP;
-        if (!ssp.hasFreeformWorkspaceSupport() && mNumStackTasks == 1) {
+        if (!ssp.hasFreeformWorkspaceSupport() && mNumStackTasks == 1 && !ignoreSingleTaskCase) {
             // When there is exactly one task, then decouple the task from the stack and just move
             // in screen space
             p = (mMinScrollP - stackScroll) / mNumStackTasks;
@@ -762,8 +777,8 @@
                 mFocusState * (mFocusedRange.relativeMin - mUnfocusedRange.relativeMin);
         float max = mUnfocusedRange.relativeMax +
                 mFocusState * (mFocusedRange.relativeMax - mUnfocusedRange.relativeMax);
-        getStackTransform(min, 0f, mBackOfStackTransform, null);
-        getStackTransform(max, 0f, mFrontOfStackTransform, null);
+        getStackTransform(min, 0f, mBackOfStackTransform, null, true /* ignoreSingleTaskCase */);
+        getStackTransform(max, 0f, mFrontOfStackTransform, null, true /* ignoreSingleTaskCase */);
         mBackOfStackTransform.visible = true;
         mFrontOfStackTransform.visible = true;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
index 7583a19..809d4ee 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
@@ -30,6 +30,7 @@
 import android.provider.Settings;
 import android.util.ArrayMap;
 import android.util.ArraySet;
+import android.util.MutableBoolean;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
@@ -77,7 +78,6 @@
 import com.android.systemui.recents.model.TaskStack;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 
 import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID;
@@ -100,10 +100,12 @@
     private static final float SHOW_HISTORY_BUTTON_SCROLL_THRESHOLD = 0.3f;
     private static final float HIDE_HISTORY_BUTTON_SCROLL_THRESHOLD = 0.3f;
 
-    private static final int DEFAULT_SYNC_STACK_DURATION = 200;
+    public static final int DEFAULT_SYNC_STACK_DURATION = 200;
     private static final int DRAG_SCALE_DURATION = 175;
     private static final float DRAG_SCALE_FACTOR = 1.05f;
 
+    private static final ArraySet<Task.TaskKey> EMPTY_TASK_SET = new ArraySet<>();
+
     TaskStack mStack;
     TaskStackLayoutAlgorithm mLayoutAlgorithm;
     TaskStackViewScroller mStackScroller;
@@ -116,7 +118,8 @@
 
     ArrayList<TaskView> mTaskViews = new ArrayList<>();
     ArrayList<TaskViewTransform> mCurrentTaskTransforms = new ArrayList<>();
-    TaskViewAnimation mDeferredTaskViewUpdateAnimation = null;
+    ArraySet<Task.TaskKey> mIgnoreTasks = new ArraySet<>();
+    TaskViewAnimation mDeferredTaskViewLayoutAnimation = null;
 
     DozeTrigger mUIDozeTrigger;
     Task mFocusedTask;
@@ -137,7 +140,6 @@
     int[] mTmpVisibleRange = new int[2];
     Rect mTmpRect = new Rect();
     ArrayMap<Task.TaskKey, TaskView> mTmpTaskViewMap = new ArrayMap<>();
-    ArraySet<Task> mTmpTaskSet = new ArraySet<>();
     List<TaskView> mTmpTaskViews = new ArrayList<>();
     TaskViewTransform mTmpTransform = new TaskViewTransform();
     LayoutInflater mInflater;
@@ -345,30 +347,82 @@
     }
 
     /**
-     * Gets the stack transforms of a list of tasks, and returns the visible range of tasks.
-     * This call ignores freeform tasks.
+     * Adds a task to the ignored set.
      */
-    private boolean updateStackTransforms(ArrayList<TaskViewTransform> taskTransforms,
-            ArrayList<Task> tasks, float stackScroll,
-            int[] visibleRangeOut, ArraySet<Task> ignoreTasksSet) {
-        int taskTransformCount = taskTransforms.size();
+    void addIgnoreTask(Task task) {
+        mIgnoreTasks.add(task.key);
+    }
+
+    /**
+     * Removes a task from the ignored set.
+     */
+    void removeIgnoreTask(Task task) {
+        mIgnoreTasks.remove(task.key);
+    }
+
+    /**
+     * Returns whether the specified {@param task} is ignored.
+     */
+    boolean isIgnoredTask(Task task) {
+        return mIgnoreTasks.contains(task.key);
+    }
+
+    /**
+     * Computes the task transforms at the current stack scroll for all visible tasks. If a valid
+     * target stack scroll is provided (ie. is different than {@param curStackScroll}), then the
+     * visible range includes all tasks at the target stack scroll. This is useful for ensure that
+     * all views necessary for a transition or animation will be visible at the start.
+     *
+     * This call ignores freeform tasks.
+     *
+     * @param taskTransforms The set of task view transforms to reuse, this list will be sized to
+     *                       match the size of {@param tasks}
+     * @param tasks The set of tasks for which to generate transforms
+     * @param curStackScroll The current stack scroll
+     * @param targetStackScroll The stack scroll that we anticipate we are going to be scrolling to.
+     *                          The range of the union of the visible views at the current and
+     *                          target stack scrolls will be returned.
+     * @param ignoreTasksSet The set of tasks to skip for purposes of calculaing the visible range.
+     *                       Transforms will still be calculated for the ignore tasks.
+     */
+    boolean computeVisibleTaskTransforms(ArrayList<TaskViewTransform> taskTransforms,
+            ArrayList<Task> tasks, float curStackScroll, float targetStackScroll,
+            int[] visibleRangeOut, ArraySet<Task.TaskKey> ignoreTasksSet) {
         int taskCount = tasks.size();
         int frontMostVisibleIndex = -1;
         int backMostVisibleIndex = -1;
+        boolean useTargetStackScroll = Float.compare(curStackScroll, targetStackScroll) != 0;
 
         // We can reuse the task transforms where possible to reduce object allocation
         Utilities.matchTaskListSize(tasks, taskTransforms);
 
         // Update the stack transforms
         TaskViewTransform frontTransform = null;
+        TaskViewTransform frontTransformAtTarget = null;
+        TaskViewTransform transform = null;
+        TaskViewTransform transformAtTarget = null;
         for (int i = taskCount - 1; i >= 0; i--) {
             Task task = tasks.get(i);
-            if (ignoreTasksSet.contains(task)) {
-                continue;
+
+            // Calculate the current and (if necessary) the target transform for the task
+            transform = mLayoutAlgorithm.getStackTransform(task, curStackScroll,
+                    taskTransforms.get(i), frontTransform);
+            if (useTargetStackScroll && !transform.visible) {
+                // If we have a target stack scroll and the task is not currently visible, then we
+                // just update the transform at the new scroll
+                // TODO: Optimize this
+                transformAtTarget = mLayoutAlgorithm.getStackTransform(task,
+                        targetStackScroll, new TaskViewTransform(), frontTransformAtTarget);
+                if (transformAtTarget.visible) {
+                    transform.copyFrom(transformAtTarget);
+                }
             }
 
-            TaskViewTransform transform = mLayoutAlgorithm.getStackTransform(task, stackScroll,
-                    taskTransforms.get(i), frontTransform);
+            // For ignore tasks, only calculate the stack transform and skip the calculation of the
+            // visible stack indices
+            if (ignoreTasksSet.contains(task.key)) {
+                continue;
+            }
 
             // For freeform tasks, only calculate the stack transform and skip the calculation of
             // the visible stack indices
@@ -392,7 +446,9 @@
                     break;
                 }
             }
+
             frontTransform = transform;
+            frontTransformAtTarget = transformAtTarget;
         }
         if (visibleRangeOut != null) {
             visibleRangeOut[0] = frontMostVisibleIndex;
@@ -402,33 +458,48 @@
     }
 
     /**
-     * Updates the children {@link TaskView}s to match the tasks in the current {@link TaskStack}.
-     * This call does not update the {@link TaskView}s to their position in the layout except when
-     * they are initially picked up from the pool, when they will be placed in a suitable initial
-     * position.
+     * Binds the visible {@link TaskView}s at the given target scroll.
+     *
+     * @see #bindVisibleTaskViews(float, ArraySet<Task.TaskKey>)
      */
-    private void bindTaskViewsWithStack(ArraySet<Task> ignoreTasksSet) {
-        final float stackScroll = mStackScroller.getStackScroll();
+    void bindVisibleTaskViews(float targetStackScroll) {
+        bindVisibleTaskViews(targetStackScroll, mIgnoreTasks);
+    }
+
+    /**
+     * Synchronizes the set of children {@link TaskView}s to match the visible set of tasks in the
+     * current {@link TaskStack}. This call does not continue on to update their position to the
+     * computed {@link TaskViewTransform}s of the visible range, but only ensures that they will
+     * be added/removed from the view hierarchy and placed in the correct Z order and initial
+     * position (if not currently on screen).
+     *
+     * @param targetStackScroll If provided, will ensure that the set of visible {@link TaskView}s
+     *                          includes those visible at the current stack scroll, and all at the
+     *                          target stack scroll.
+     * @param ignoreTasksSet The set of tasks to ignore in this rebinding of the visible
+     *                       {@link TaskView}s
+     */
+    void bindVisibleTaskViews(float targetStackScroll, ArraySet<Task.TaskKey> ignoreTasksSet) {
         final int[] visibleStackRange = mTmpVisibleRange;
 
         // Get all the task transforms
         final ArrayList<Task> tasks = mStack.getStackTasks();
-        final boolean isValidVisibleStackRange = updateStackTransforms(mCurrentTaskTransforms,
-                tasks, stackScroll, visibleStackRange, ignoreTasksSet);
+        final boolean isValidVisibleRange = computeVisibleTaskTransforms(mCurrentTaskTransforms,
+                tasks, mStackScroller.getStackScroll(), targetStackScroll, visibleStackRange,
+                ignoreTasksSet);
 
         // Return all the invisible children to the pool
-        final List<TaskView> taskViews = getTaskViews();
-        final int taskViewCount = taskViews.size();
-        int lastFocusedTaskIndex = -1;
         mTmpTaskViewMap.clear();
-        mTmpTaskViewMap.ensureCapacity(tasks.size());
+        List<TaskView> taskViews = getTaskViews();
+        int lastFocusedTaskIndex = -1;
+        int taskViewCount = taskViews.size();
         for (int i = taskViewCount - 1; i >= 0; i--) {
-            final TaskView tv = taskViews.get(i);
-            final Task task = tv.getTask();
-            final int taskIndex = mStack.indexOfStackTask(task);
+            TaskView tv = taskViews.get(i);
+            Task task = tv.getTask();
+            int taskIndex = mStack.indexOfStackTask(task);
 
             // Skip ignored tasks
-            if (ignoreTasksSet.contains(task)) {
+            if (ignoreTasksSet.contains(task.key)) {
                 continue;
             }
 
@@ -445,13 +516,13 @@
         }
 
         // Pick up all the newly visible children
-        int lastVisStackIndex = isValidVisibleStackRange ? visibleStackRange[1] : 0;
-        for (int i = mStack.getTaskCount() - 1; i >= lastVisStackIndex; i--) {
-            final Task task = tasks.get(i);
-            final TaskViewTransform transform = mCurrentTaskTransforms.get(i);
+        int lastVisStackIndex = isValidVisibleRange ? visibleStackRange[1] : 0;
+        for (int i = tasks.size() - 1; i >= lastVisStackIndex; i--) {
+            Task task = tasks.get(i);
+            TaskViewTransform transform = mCurrentTaskTransforms.get(i);
 
             // Skip ignored tasks
-            if (ignoreTasksSet.contains(task)) {
+            if (ignoreTasksSet.contains(task.key)) {
                 continue;
             }
 
@@ -502,26 +573,34 @@
     }
 
     /**
-     * Cancels any existing {@link TaskView} animations, and updates each {@link TaskView} to its
-     * current position as defined by the {@link TaskStackLayoutAlgorithm}.
+     * Relayout the the visible {@link TaskView}s to their current transforms as specified by the
+     * {@link TaskStackLayoutAlgorithm} with the given {@param animation}. This call cancels any
+     * animations that are current running on those task views, and will ensure that the children
+     * {@link TaskView}s will match the set of visible tasks in the stack.
      *
-     * @param ignoreTasks the set of tasks to ignore in the relayout
+     * @see #relayoutTaskViews(TaskViewAnimation, ArraySet<Task.TaskKey>)
      */
-    private void updateTaskViewsToLayout(TaskViewAnimation animation, Task... ignoreTasks) {
-        // Keep track of the ignore tasks
-        ArraySet<Task> ignoreTasksSet = mTmpTaskSet;
-        ignoreTasksSet.clear();
-        ignoreTasksSet.ensureCapacity(ignoreTasks.length);
-        Collections.addAll(ignoreTasksSet, ignoreTasks);
+    void relayoutTaskViews(TaskViewAnimation animation) {
+        relayoutTaskViews(animation, mIgnoreTasks);
+    }
 
+    /**
+     * Relayout the the visible {@link TaskView}s to their current transforms as specified by the
+     * {@link TaskStackLayoutAlgorithm} with the given {@param animation}. This call cancels any
+     * animations that are current running on those task views, and will ensure that the children
+     * {@link TaskView}s will match the set of visible tasks in the stack.
+     *
+     * @param ignoreTasksSet the set of tasks to ignore in the relayout
+     */
+    void relayoutTaskViews(TaskViewAnimation animation, ArraySet<Task.TaskKey> ignoreTasksSet) {
         // If we had a deferred animation, cancel that
-        mDeferredTaskViewUpdateAnimation = null;
+        mDeferredTaskViewLayoutAnimation = null;
 
         // Cancel all task view animations
         cancelAllTaskViewAnimations();
 
-        // Fetch the current set of TaskViews
-        bindTaskViewsWithStack(ignoreTasksSet);
+        // Synchronize the current set of TaskViews
+        bindVisibleTaskViews(mStackScroller.getStackScroll(), ignoreTasksSet);
 
         // Animate them to their final transforms with the given animation
         List<TaskView> taskViews = getTaskViews();
@@ -531,7 +610,7 @@
             final int taskIndex = mStack.indexOfStackTask(tv.getTask());
             final TaskViewTransform transform = mCurrentTaskTransforms.get(taskIndex);
 
-            if (ignoreTasksSet.contains(tv.getTask())) {
+            if (ignoreTasksSet.contains(tv.getTask().key)) {
                 continue;
             }
 
@@ -542,8 +621,8 @@
     /**
      * Posts an update to synchronize the {@link TaskView}s with the stack on the next frame.
      */
-    private void updateTaskViewsToLayoutOnNextFrame(TaskViewAnimation animation) {
-        mDeferredTaskViewUpdateAnimation = animation;
+    void relayoutTaskViewsOnNextFrame(TaskViewAnimation animation) {
+        mDeferredTaskViewLayoutAnimation = animation;
         postInvalidateOnAnimation();
     }
 
@@ -558,13 +637,62 @@
     }
 
     /**
-     * Cancels all {@link TaskView} animations.
+     * Returns the current task transforms of all tasks, falling back to the stack layout if there
+     * is no {@link TaskView} for the task.
      */
-    private void cancelAllTaskViewAnimations() {
+    public void getCurrentTaskTransforms(ArrayList<Task> tasks,
+            ArrayList<TaskViewTransform> transformsOut) {
+        Utilities.matchTaskListSize(tasks, transformsOut);
+        for (int i = tasks.size() - 1; i >= 0; i--) {
+            Task task = tasks.get(i);
+            TaskViewTransform transform = transformsOut.get(i);
+            TaskView tv = getChildViewForTask(task);
+            if (tv != null) {
+                transform.fillIn(tv);
+            } else {
+                mLayoutAlgorithm.getStackTransform(task, mStackScroller.getStackScroll(),
+                        transform, null);
+            }
+            transform.visible = true;
+        }
+    }
+
+    /**
+     * Returns the task transforms for all the tasks in the stack if the stack was at the given
+     * {@param stackScroll}.
+     */
+    public void getLayoutTaskTransforms(float stackScroll, ArrayList<Task> tasks,
+            ArrayList<TaskViewTransform> transformsOut) {
+        Utilities.matchTaskListSize(tasks, transformsOut);
+        for (int i = tasks.size() - 1; i >= 0; i--) {
+            Task task = tasks.get(i);
+            TaskViewTransform transform = transformsOut.get(i);
+            mLayoutAlgorithm.getStackTransform(task, stackScroll, transform, null);
+            transform.visible = true;
+        }
+    }
+
+    /**
+     * Cancels all {@link TaskView} animations.
+     *
+     * @see #cancelAllTaskViewAnimations(ArraySet<Task.TaskKey>)
+     */
+    void cancelAllTaskViewAnimations() {
+        cancelAllTaskViewAnimations(mIgnoreTasks);
+    }
+
+    /**
+     * Cancels all {@link TaskView} animations.
+     *
+     * @param ignoreTasksSet The set of tasks to continue running their animations.
+     */
+    void cancelAllTaskViewAnimations(ArraySet<Task.TaskKey> ignoreTasksSet) {
         List<TaskView> taskViews = getTaskViews();
         for (int i = taskViews.size() - 1; i >= 0; i--) {
             final TaskView tv = taskViews.get(i);
-            tv.cancelTransformAnimation();
+            if (!ignoreTasksSet.contains(tv.getTask().key)) {
+                tv.cancelTransformAnimation();
+            }
         }
     }
 
@@ -577,11 +705,22 @@
         // Update the clip on each task child
         List<TaskView> taskViews = getTaskViews();
         TaskView tmpTv = null;
+        TaskView prevVisibleTv = null;
         int taskViewCount = taskViews.size();
         for (int i = 0; i < taskViewCount; i++) {
             TaskView tv = taskViews.get(i);
             TaskView frontTv = null;
             int clipBottom = 0;
+
+            if (mIgnoreTasks.contains(tv.getTask().key)) {
+                // For each of the ignore tasks, update the translationZ of its TaskView to be
+                // between the translationZ of the tasks immediately underneath it
+                if (prevVisibleTv != null) {
+                    tv.setTranslationZ(Math.max(tv.getTranslationZ(),
+                            prevVisibleTv.getTranslationZ() + 0.1f));
+                }
+            }
+
             if (i < (taskViewCount - 1) && tv.shouldClipViewInStack()) {
                 // Find the next view to clip against
                 for (int j = i + 1; j < taskViewCount; j++) {
@@ -609,33 +748,37 @@
             if (!config.useHardwareLayers) {
                 tv.mThumbnailView.updateThumbnailVisibility(clipBottom - tv.getPaddingBottom());
             }
+            prevVisibleTv = tv;
         }
         mTaskViewsClipDirty = false;
     }
 
     /**
+     * Updates the layout algorithm min and max virtual scroll bounds.
+     *
+     * @see #updateLayoutAlgorithm(boolean, ArraySet<Task.TaskKey>)
+     */
+    void updateLayoutAlgorithm(boolean boundScrollToNewMinMax) {
+        updateLayoutAlgorithm(boundScrollToNewMinMax, mIgnoreTasks);
+    }
+
+    /**
      * Updates the min and max virtual scroll bounds.
      *
-     * @param ignoreTasks the set of tasks to ignore in the relayout
+     * @param ignoreTasksSet the set of tasks to ignore in the relayout
      */
-    void updateLayout(boolean boundScrollToNewMinMax, Task... ignoreTasks) {
-        // Keep track of the ingore tasks
-        ArraySet<Task> ignoreTasksSet = mTmpTaskSet;
-        ignoreTasksSet.clear();
-        ignoreTasksSet.ensureCapacity(ignoreTasks.length);
-        Collections.addAll(ignoreTasksSet, ignoreTasks);
-
+    void updateLayoutAlgorithm(boolean boundScrollToNewMinMax,
+            ArraySet<Task.TaskKey> ignoreTasksSet) {
         // Compute the min and max scroll values
         mLayoutAlgorithm.update(mStack, ignoreTasksSet);
 
-        // Update the freeform workspace
+        // Update the freeform workspace background
         SystemServicesProxy ssp = Recents.getSystemServices();
         if (ssp.hasFreeformWorkspaceSupport()) {
             mTmpRect.set(mLayoutAlgorithm.mFreeformRect);
             mFreeformWorkspaceBackground.setBounds(mTmpRect);
         }
 
-        // Debug logging
         if (boundScrollToNewMinMax) {
             mStackScroller.boundScroll();
         }
@@ -671,8 +814,6 @@
 
         // Reset the last focused task state if changed
         if (mFocusedTask != null) {
-            resetFocusedTask(mFocusedTask);
-
             // Cancel the timer indicator, if applicable
             if (showTimerIndicator) {
                 final TaskView tv = getChildViewForTask(mFocusedTask);
@@ -680,6 +821,8 @@
                     tv.getHeaderView().cancelFocusTimerIndicator();
                 }
             }
+
+            resetFocusedTask(mFocusedTask);
         }
 
         boolean willScroll = false;
@@ -937,10 +1080,10 @@
             // Notify accessibility
             sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SCROLLED);
         }
-        if (mDeferredTaskViewUpdateAnimation != null) {
-            updateTaskViewsToLayout(mDeferredTaskViewUpdateAnimation);
+        if (mDeferredTaskViewLayoutAnimation != null) {
+            relayoutTaskViews(mDeferredTaskViewLayoutAnimation);
             mTaskViewsClipDirty = true;
-            mDeferredTaskViewUpdateAnimation = null;
+            mDeferredTaskViewLayoutAnimation = null;
         }
         if (mTaskViewsClipDirty) {
             clipTaskViews();
@@ -948,30 +1091,16 @@
     }
 
     /**
-     * Computes the stack and task rects.
-     *
-     * @param ignoreTasks the set of tasks to ignore in the relayout
-     */
-    public void computeRects(Rect taskStackBounds, boolean boundScroll, Task... ignoreTasks) {
-        // Compute the rects in the stack algorithm
-        mLayoutAlgorithm.initialize(taskStackBounds,
-                TaskStackLayoutAlgorithm.StackState.getStackStateForStack(mStack));
-
-        // Update the scroll bounds
-        updateLayout(boundScroll, ignoreTasks);
-    }
-
-    /**
      * This is ONLY used from the Recents component to update the dummy stack view for purposes
      * of getting the task rect to animate to.
      */
     public void updateLayoutForStack(TaskStack stack) {
         mStack = stack;
-        updateLayout(false);
+        updateLayoutAlgorithm(false /* boundScroll */, EMPTY_TASK_SET);
     }
 
     /**
-     * Computes the maximum number of visible tasks and thumbnails.  Requires that
+     * Computes the maximum number of visible tasks and thumbnails. Requires that
      * updateLayoutForStack() is called first.
      */
     public TaskStackLayoutAlgorithm.VisibilityReport computeStackVisibilityReport() {
@@ -1002,16 +1131,18 @@
         int width = MeasureSpec.getSize(widthMeasureSpec);
         int height = MeasureSpec.getSize(heightMeasureSpec);
 
-        // Compute our stack/task rects
-        computeRects(mStackBounds, false);
+        // Compute the rects in the stack algorithm
+        mLayoutAlgorithm.initialize(mStackBounds,
+                TaskStackLayoutAlgorithm.StackState.getStackStateForStack(mStack));
+        updateLayoutAlgorithm(false /* boundScroll */, EMPTY_TASK_SET);
 
         // If this is the first layout, then scroll to the front of the stack, then update the
         // TaskViews with the stack so that we can lay them out
         if (mAwaitingFirstLayout) {
             mStackScroller.setStackScrollToInitialState();
         }
-        mTmpTaskSet.clear();
-        bindTaskViewsWithStack(mTmpTaskSet);
+        // Rebind all the views, including the ignore ones
+        bindVisibleTaskViews(mStackScroller.getStackScroll(), EMPTY_TASK_SET);
 
         // Measure each of the TaskViews
         mTmpTaskViews.clear();
@@ -1066,7 +1197,8 @@
                 mStackScroller.boundScroll();
             }
         }
-        updateTaskViewsToLayout(TaskViewAnimation.IMMEDIATE);
+        // Relayout all of the task views including the ignored ones
+        relayoutTaskViews(TaskViewAnimation.IMMEDIATE, EMPTY_TASK_SET);
         clipTaskViews();
 
         if (mAwaitingFirstLayout || !mEnterAnimationComplete) {
@@ -1088,9 +1220,12 @@
 
         // Set the task focused state without requesting view focus, and leave the focus animations
         // until after the enter-animation
+        Task launchTask = mStack.getLaunchTarget();
         RecentsConfiguration config = Recents.getConfiguration();
         RecentsActivityLaunchState launchState = config.getLaunchState();
-        int focusedTaskIndex = launchState.getInitialFocusTaskIndex(mStack.getTaskCount());
+        int focusedTaskIndex = launchTask != null
+                ? mStack.indexOfStackTask(launchTask)
+                : launchState.getInitialFocusTaskIndex(mStack.getTaskCount());
         if (focusedTaskIndex != -1) {
             setFocusedTask(focusedTaskIndex, false /* scrollToTask */,
                     false /* requestViewFocus */);
@@ -1106,8 +1241,29 @@
     }
 
     public boolean isTouchPointInView(float x, float y, TaskView tv) {
-        return (tv.getLeft() <= x && x <= tv.getRight()) &&
-                (tv.getTop() <= y && y <= tv.getBottom());
+        mTmpRect.set(tv.getLeft(), tv.getTop(), tv.getRight(), tv.getBottom());
+        mTmpRect.offset((int) tv.getTranslationX(), (int) tv.getTranslationY());
+        return mTmpRect.contains((int) x, (int) y);
+    }
+
+    /**
+     * Returns a non-ignored task in the {@param tasks} list that can be used as an achor when
+     * calculating the scroll position before and after a layout change.
+     */
+    public Task findAnchorTask(List<Task> tasks, MutableBoolean isFrontMostTask) {
+        for (int i = tasks.size() - 1; i >= 0; i--) {
+            Task task = tasks.get(i);
+
+            // Ignore deleting tasks
+            if (mIgnoreTasks.contains(task.key)) {
+                if (i == tasks.size() - 1) {
+                    isFrontMostTask.value = true;
+                }
+                continue;
+            }
+            return task;
+        }
+        return null;
     }
 
     @Override
@@ -1152,70 +1308,38 @@
     @Override
     public void onStackTaskAdded(TaskStack stack, Task newTask) {
         // Update the min/max scroll and animate other task views into their new positions
-        updateLayout(true);
+        updateLayoutAlgorithm(true /* boundScroll */);
 
         // Animate all the tasks into place
-        updateTaskViewsToLayout(new TaskViewAnimation(DEFAULT_SYNC_STACK_DURATION,
+        relayoutTaskViews(new TaskViewAnimation(DEFAULT_SYNC_STACK_DURATION,
                 mFastOutSlowInInterpolator));
     }
 
+    /**
+     * We expect that the {@link TaskView} associated with the removed task is already hidden.
+     */
     @Override
     public void onStackTaskRemoved(TaskStack stack, Task removedTask, boolean wasFrontMostTask,
-            Task newFrontMostTask) {
+            Task newFrontMostTask, TaskViewAnimation animation) {
         if (mFocusedTask == removedTask) {
             resetFocusedTask(removedTask);
         }
 
-        if (!removedTask.isFreeformTask()) {
-            // Remove the view associated with this task, we can't rely on updateTransforms
-            // to work here because the task is no longer in the list
-            TaskView tv = getChildViewForTask(removedTask);
-            if (tv != null) {
-                mViewPool.returnViewToPool(tv);
-            }
-
-            // Get the stack scroll of the task to anchor to (since we are removing something, the
-            // front most task will be our anchor task)
-            Task anchorTask = mStack.getStackFrontMostTask();
-            float prevAnchorTaskScroll = 0;
-            if (anchorTask != null) {
-                prevAnchorTaskScroll = mLayoutAlgorithm.getStackScrollForTask(anchorTask);
-            }
-
-            // Update the min/max scroll and animate other task views into their new positions
-            updateLayout(true);
-
-            if (wasFrontMostTask) {
-                // Since the max scroll progress is offset from the bottom of the stack, just scroll
-                // to ensure that the new front most task is now fully visible
-                mStackScroller.setStackScroll(mLayoutAlgorithm.mMaxScrollP);
-            } else if (anchorTask != null) {
-                // Otherwise, offset the scroll by the movement of the anchor task
-                float anchorTaskScroll = mLayoutAlgorithm.getStackScrollForTask(anchorTask);
-                float stackScrollOffset = (anchorTaskScroll - prevAnchorTaskScroll);
-                if (mLayoutAlgorithm.getFocusState() != TaskStackLayoutAlgorithm.STATE_FOCUSED) {
-                    // If we are focused, we don't want the front task to move, but otherwise, we
-                    // allow the back task to move up, and the front task to move back
-                    stackScrollOffset /= 2;
-                }
-                mStackScroller.setStackScroll(mStackScroller.getStackScroll() + stackScrollOffset);
-                mStackScroller.boundScroll();
-            }
-        } else {
-            // Remove the view associated with this task, we can't rely on updateTransforms
-            // to work here because the task is no longer in the list
-            TaskView tv = getChildViewForTask(removedTask);
-            if (tv != null) {
-                mViewPool.returnViewToPool(tv);
-            }
-
-            // Update the min/max scroll and animate other task views into their new positions
-            updateLayout(true);
+        // Remove the view associated with this task, we can't rely on updateTransforms
+        // to work here because the task is no longer in the list
+        TaskView tv = getChildViewForTask(removedTask);
+        if (tv != null) {
+            mViewPool.returnViewToPool(tv);
         }
 
-        // Animate all the tasks into place
-        updateTaskViewsToLayout(new TaskViewAnimation(DEFAULT_SYNC_STACK_DURATION,
-                mFastOutSlowInInterpolator));
+        // Remove the task from the ignored set
+        removeIgnoreTask(removedTask);
+
+        // If requested, relayout with the given animation
+        if (animation != null) {
+            updateLayoutAlgorithm(true /* boundScroll */);
+            relayoutTaskViews(animation);
+        }
 
         // Update the new front most task's action button
         if (mScreenPinningEnabled && newFrontMostTask != null) {
@@ -1232,7 +1356,8 @@
     }
 
     @Override
-    public void onHistoryTaskRemoved(TaskStack stack, Task removedTask) {
+    public void onHistoryTaskRemoved(TaskStack stack, Task removedTask,
+            TaskViewAnimation animation) {
         // To be implemented
     }
 
@@ -1316,7 +1441,10 @@
 
     @Override
     public void onTaskViewClipStateChanged(TaskView tv) {
-        clipTaskViews();
+        if (!mTaskViewsClipDirty) {
+            mTaskViewsClipDirty = true;
+            invalidate();
+        }
     }
 
     /**** TaskStackViewScroller.TaskStackViewScrollerCallbacks ****/
@@ -1324,7 +1452,9 @@
     @Override
     public void onScrollChanged(float prevScroll, float curScroll, TaskViewAnimation animation) {
         mUIDozeTrigger.poke();
-        updateTaskViewsToLayoutOnNextFrame(animation);
+        if (animation != null) {
+            relayoutTaskViewsOnNextFrame(animation);
+        }
 
         if (shouldShowHistoryButton() &&
                 prevScroll > SHOW_HISTORY_BUTTON_SCROLL_THRESHOLD &&
@@ -1354,7 +1484,7 @@
                     tv.dismissTask();
                 } else {
                     // Otherwise, remove the task from the stack immediately
-                    mStack.removeTask(t);
+                    mStack.removeTask(t, TaskViewAnimation.IMMEDIATE);
                 }
             }
         }
@@ -1402,7 +1532,7 @@
     }
 
     public final void onBusEvent(TaskViewDismissedEvent event) {
-        removeTaskViewFromStack(event.taskView);
+        removeTaskViewFromStack(event.taskView, event.task);
         EventBus.getDefault().send(new DeleteTaskDataEvent(event.task));
     }
 
@@ -1419,7 +1549,10 @@
         // Poke the doze trigger on user interaction
         mUIDozeTrigger.poke();
         if (event.showTimerIndicator && mFocusedTask != null) {
-            getChildViewForTask(mFocusedTask).getHeaderView().cancelFocusTimerIndicator();
+            TaskView tv = getChildViewForTask(mFocusedTask);
+            if (tv != null) {
+                tv.getHeaderView().cancelFocusTimerIndicator();
+            }
         }
     }
 
@@ -1455,23 +1588,29 @@
     }
 
     public final void onBusEvent(DragDropTargetChangedEvent event) {
+        TaskViewAnimation animation = new TaskViewAnimation(250, mFastOutSlowInInterpolator);
         if (event.dropTarget instanceof TaskStack.DockState) {
             // Calculate the new task stack bounds that matches the window size that Recents will
             // have after the drop
+            addIgnoreTask(event.task);
             final TaskStack.DockState dockState = (TaskStack.DockState) event.dropTarget;
             mStackBounds.set(dockState.getDockedTaskStackBounds(getMeasuredWidth(),
                     getMeasuredHeight(), mDividerSize, mLayoutAlgorithm.mSystemInsets,
                     getResources()));
-            computeRects(mStackBounds, true /* boundScroll */, event.task /* ignoreTask */);
-            updateTaskViewsToLayout(new TaskViewAnimation(250, mFastOutSlowInInterpolator),
-                    event.task /* ignoreTask */);
+            mLayoutAlgorithm.initialize(mStackBounds,
+                    TaskStackLayoutAlgorithm.StackState.getStackStateForStack(mStack));
+            updateLayoutAlgorithm(true /* boundScroll */);
         } else {
-            // Restore the pre-drag task stack bounds
+            // Restore the pre-drag task stack bounds, but ensure that we don't layout the dragging
+            // task view, so add it back to the ignore set after updating the layout
             mStackBounds.set(mStableStackBounds);
-            computeRects(mStackBounds, true /* boundScroll */);
-            updateTaskViewsToLayout(new TaskViewAnimation(250, mFastOutSlowInInterpolator),
-                    event.task /* ignoreTask */);
+            removeIgnoreTask(event.task);
+            mLayoutAlgorithm.initialize(mStackBounds,
+                    TaskStackLayoutAlgorithm.StackState.getStackStateForStack(mStack));
+            updateLayoutAlgorithm(true /* boundScroll */);
+            addIgnoreTask(event.task);
         }
+        relayoutTaskViews(animation);
     }
 
     public final void onBusEvent(final DragEndEvent event) {
@@ -1487,14 +1626,14 @@
 
         if (hasChangedStacks) {
             // Move the task to the right position in the stack (ie. the front of the stack if
-            // freeform or the front of the stack if fullscreen).  Note, we MUST move the tasks
+            // freeform or the front of the stack if fullscreen). Note, we MUST move the tasks
             // before we update their stack ids, otherwise, the keys will have changed.
             if (event.dropTarget == mFreeformWorkspaceDropTarget) {
                 mStack.moveTaskToStack(event.task, FREEFORM_WORKSPACE_STACK_ID);
             } else if (event.dropTarget == mStackDropTarget) {
                 mStack.moveTaskToStack(event.task, FULLSCREEN_WORKSPACE_STACK_ID);
             }
-            updateLayout(true);
+            updateLayoutAlgorithm(true /* boundScroll */);
 
             // Move the task to the new stack in the system after the animation completes
             event.addPostAnimationCallback(new Runnable() {
@@ -1522,11 +1661,12 @@
         mLayoutAlgorithm.getStackTransform(event.task, getScroller().getStackScroll(),
                 mTmpTransform, null);
         event.getAnimationTrigger().increment();
-        updateTaskViewsToLayout(new TaskViewAnimation(DEFAULT_SYNC_STACK_DURATION,
+        relayoutTaskViews(new TaskViewAnimation(DEFAULT_SYNC_STACK_DURATION,
                 mFastOutSlowInInterpolator));
         updateTaskViewToTransform(event.taskView, mTmpTransform,
                 new TaskViewAnimation(DEFAULT_SYNC_STACK_DURATION, mFastOutSlowInInterpolator,
                         event.getAnimationTrigger().decrementOnAnimationEnd()));
+        removeIgnoreTask(event.task);
     }
 
     public final void onBusEvent(StackViewScrolledEvent event) {
@@ -1593,15 +1733,14 @@
      * Removes the task from the stack, and updates the focus to the next task in the stack if the
      * removed TaskView was focused.
      */
-    private void removeTaskViewFromStack(TaskView tv) {
-        Task task = tv.getTask();
-
+    private void removeTaskViewFromStack(TaskView tv, Task task) {
         // Announce for accessibility
         tv.announceForAccessibility(getContext().getString(
-                R.string.accessibility_recents_item_dismissed, tv.getTask().title));
+                R.string.accessibility_recents_item_dismissed, task.title));
 
         // Remove the task from the stack
-        mStack.removeTask(task);
+        mStack.removeTask(task, new TaskViewAnimation(DEFAULT_SYNC_STACK_DURATION,
+                mFastOutSlowInInterpolator));
     }
 
     /**
@@ -1622,7 +1761,7 @@
     }
 
     /**
-     * Returns the insert index for the task in the current set of task views.  If the given task
+     * Returns the insert index for the task in the current set of task views. If the given task
      * is already in the task view list, then this method returns the insert index assuming it
      * is first removed at the previous index.
      *
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java
index 32f02ac..5335b14 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java
@@ -151,6 +151,7 @@
 
     /** Animates the stack scroll into bounds */
     ObjectAnimator animateBoundScroll() {
+        // TODO: Take duration for snap back
         float curScroll = getStackScroll();
         float newScroll = getBoundedStackScroll(curScroll);
         if (Float.compare(newScroll, curScroll) != 0) {
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
index 4813c19..e9f6f39 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
@@ -21,13 +21,16 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Rect;
-import android.util.Log;
+import android.util.ArrayMap;
+import android.util.MutableBoolean;
 import android.view.InputDevice;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 import android.view.View;
 import android.view.ViewConfiguration;
 import android.view.ViewParent;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
 import com.android.internal.logging.MetricsLogger;
 import com.android.systemui.R;
 import com.android.systemui.SwipeHelper;
@@ -37,19 +40,25 @@
 import com.android.systemui.recents.events.activity.HideRecentsEvent;
 import com.android.systemui.recents.events.ui.StackViewScrolledEvent;
 import com.android.systemui.recents.events.ui.TaskViewDismissedEvent;
+import com.android.systemui.recents.misc.RectFEvaluator;
 import com.android.systemui.recents.misc.SystemServicesProxy;
 import com.android.systemui.recents.misc.Utilities;
+import com.android.systemui.recents.model.Task;
 import com.android.systemui.statusbar.FlingAnimationUtils;
 
+import java.util.ArrayList;
 import java.util.List;
 
-/* Handles touch events for a TaskStackView. */
+/**
+ * Handles touch events for a TaskStackView.
+ */
 class TaskStackViewTouchHandler implements SwipeHelper.Callback {
 
-    private static final String TAG = "TaskStackViewTouchHandler";
-    private static final boolean DEBUG = false;
+    private static final int INACTIVE_POINTER_ID = -1;
 
-    private static int INACTIVE_POINTER_ID = -1;
+    private static final RectFEvaluator RECT_EVALUATOR = new RectFEvaluator();
+    private static final Interpolator STACK_TRANSFORM_INTERPOLATOR =
+            new PathInterpolator(0.73f, 0.33f, 0.42f, 0.85f);
 
     Context mContext;
     TaskStackView mSv;
@@ -74,6 +83,15 @@
     final int mWindowTouchSlop;
 
     private final StackViewScrolledEvent mStackViewScrolledEvent = new StackViewScrolledEvent();
+
+    // The current and final set of task transforms, sized to match the list of tasks in the stack
+    private ArrayList<Task> mCurrentTasks = new ArrayList<>();
+    private ArrayList<TaskViewTransform> mCurrentTaskTransforms = new ArrayList<>();
+    private ArrayList<TaskViewTransform> mFinalTaskTransforms = new ArrayList<>();
+    private ArrayMap<View, Animator> mSwipeHelperAnimations = new ArrayMap<>();
+    private TaskViewTransform mTmpTransform = new TaskViewTransform();
+    private float mTargetStackScroll;
+
     SwipeHelper mSwipeHelper;
     boolean mInterceptedBySwipeHelper;
 
@@ -97,8 +115,14 @@
             }
 
             @Override
-            protected void updateSnapBackAnimation(Animator anim) {
+            protected void prepareDismissAnimation(View v, Animator anim) {
+                mSwipeHelperAnimations.put(v, anim);
+            }
+
+            @Override
+            protected void prepareSnapBackAnimation(View v, Animator anim) {
                 anim.setInterpolator(mSv.mFastOutSlowInInterpolator);
+                mSwipeHelperAnimations.put(v, anim);
             }
         };
         mSwipeHelper.setDisableHardwareLayers(true);
@@ -119,21 +143,6 @@
         }
     }
 
-    /** Returns the view at the specified coordinates */
-    TaskView findViewAtPoint(int x, int y) {
-        List<TaskView> taskViews = mSv.getTaskViews();
-        int taskViewCount = taskViews.size();
-        for (int i = taskViewCount - 1; i >= 0; i--) {
-            TaskView tv = taskViews.get(i);
-            if (tv.getVisibility() == View.VISIBLE) {
-                if (mSv.isTouchPointInView(x, y, tv)) {
-                    return tv;
-                }
-            }
-        }
-        return null;
-    }
-
     /** Touch preprocessing for handling below */
     public boolean onInterceptTouchEvent(MotionEvent ev) {
         // Pass through to swipe helper if we are swiping
@@ -179,6 +188,15 @@
                 mScroller.stopBoundScrollAnimation();
                 Utilities.cancelAnimationWithoutCallbacks(mScrollFlingAnimator);
 
+                // Finish any existing task animations from the delete
+                mSv.cancelAllTaskViewAnimations();
+                // Finish any of the swipe helper animations
+                ArrayMap<View, Animator> existingAnimators = new ArrayMap<>(mSwipeHelperAnimations);
+                for (int i = 0; i < existingAnimators.size(); i++) {
+                    existingAnimators.get(existingAnimators.keyAt(i)).end();
+                }
+                mSwipeHelperAnimations.clear();
+
                 // Initialize the velocity tracker
                 initOrResetVelocityTracker();
                 mVelocityTracker.addMovement(ev);
@@ -214,9 +232,6 @@
                     float deltaP = layoutAlgorithm.getDeltaPForY(mDownY, y);
                     float curScrollP = mDownScrollP + deltaP;
                     mScroller.setStackScroll(curScrollP);
-                    if (DEBUG) {
-                        Log.d(TAG, "scroll: " + curScrollP);
-                    }
                     mStackViewScrolledEvent.updateY(y - mLastY);
                     EventBus.getDefault().send(mStackViewScrolledEvent);
                 }
@@ -343,12 +358,19 @@
 
     @Override
     public View getChildAtPosition(MotionEvent ev) {
-        return findViewAtPoint((int) ev.getX(), (int) ev.getY());
+        TaskView tv = findViewAtPoint((int) ev.getX(), (int) ev.getY());
+        if (tv != null && canChildBeDismissed(tv)) {
+            return tv;
+        }
+        return null;
     }
 
     @Override
     public boolean canChildBeDismissed(View v) {
-        return true;
+        // Disallow dismissing an already dismissed task
+        TaskView tv = (TaskView) v;
+        return !mSwipeHelperAnimations.containsKey(v) &&
+                (mSv.getStack().indexOfStackTask(tv.getTask()) != -1);
     }
 
     @Override
@@ -364,34 +386,113 @@
         if (parent != null) {
             parent.requestDisallowInterceptTouchEvent(true);
         }
+
+        // Add this task to the set of tasks we are deleting
+        mSv.addIgnoreTask(tv.getTask());
+
+        // Determine if we are animating the other tasks while dismissing this task
+        mCurrentTasks = mSv.getStack().getStackTasks();
+        MutableBoolean isFrontMostTask = new MutableBoolean(false);
+        Task anchorTask = mSv.findAnchorTask(mCurrentTasks, isFrontMostTask);
+        TaskStackViewScroller stackScroller = mSv.getScroller();
+        if (anchorTask != null) {
+            // Get the current set of task transforms
+            mSv.getCurrentTaskTransforms(mCurrentTasks, mCurrentTaskTransforms);
+
+            // Get the stack scroll of the task to anchor to (since we are removing something, the
+            // front most task will be our anchor task)
+            float prevAnchorTaskScroll = 0;
+            boolean pullStackForward = mCurrentTasks.size() > 0;
+            if (pullStackForward) {
+                prevAnchorTaskScroll = mSv.getStackAlgorithm().getStackScrollForTask(anchorTask);
+            }
+
+            // Calculate where the views would be without the deleting tasks
+            mSv.updateLayoutAlgorithm(false /* boundScroll */);
+
+            float newStackScroll = stackScroller.getStackScroll();
+            if (isFrontMostTask.value) {
+                // Bound the stack scroll to pull tasks forward if necessary
+                newStackScroll = stackScroller.getBoundedStackScroll(newStackScroll);
+            } else if (pullStackForward) {
+                // Otherwise, offset the scroll by the movement of the anchor task
+                float anchorTaskScroll = mSv.getStackAlgorithm().getStackScrollForTask(anchorTask);
+                float stackScrollOffset = (anchorTaskScroll - prevAnchorTaskScroll);
+                if (mSv.getStackAlgorithm().getFocusState() !=
+                        TaskStackLayoutAlgorithm.STATE_FOCUSED) {
+                    // If we are focused, we don't want the front task to move, but otherwise, we
+                    // allow the back task to move up, and the front task to move back
+                    stackScrollOffset /= 2;
+                }
+                newStackScroll = stackScroller.getBoundedStackScroll(stackScroller.getStackScroll()
+                        + stackScrollOffset);
+            }
+
+            // Pick up the newly visible views, not including the deleting tasks
+            mSv.bindVisibleTaskViews(newStackScroll);
+
+            // Get the final set of task transforms (with task removed)
+            mSv.getLayoutTaskTransforms(newStackScroll, mCurrentTasks, mFinalTaskTransforms);
+
+            // Set the target to scroll towards upon dismissal
+            mTargetStackScroll = newStackScroll;
+
+            /*
+             * Post condition: All views that will be visible as a part of the gesture are retrieved
+             *                 and at their initial positions.  The stack is still at the current
+             *                 scroll, but the layout is updated without the task currently being
+             *                 dismissed.
+             */
+        }
     }
 
     @Override
     public boolean updateSwipeProgress(View v, boolean dismissable, float swipeProgress) {
+        updateTaskViewTransforms(getDismissFraction(v));
         return true;
     }
 
+    /**
+     * Called after the {@link TaskView} is finished animating away.
+     */
     @Override
     public void onChildDismissed(View v) {
         TaskView tv = (TaskView) v;
+
         // Re-enable clipping with the stack (we will reuse this view)
         tv.setClipViewInStack(true);
         // Re-enable touch events from this task view
         tv.setTouchEnabled(true);
+        // Update the scroll to the final scroll position from onBeginDrag()
+        mSv.getScroller().setStackScroll(mTargetStackScroll, null);
         // Remove the task view from the stack
         EventBus.getDefault().send(new TaskViewDismissedEvent(tv.getTask(), tv));
+        // Stop tracking this deletion animation
+        mSwipeHelperAnimations.remove(v);
         // Keep track of deletions by keyboard
         MetricsLogger.histogram(tv.getContext(), "overview_task_dismissed_source",
                 Constants.Metrics.DismissSourceSwipeGesture);
     }
 
+    /**
+     * Called after the {@link TaskView} is finished animating back into the list.
+     * onChildDismissed() calls.
+     */
     @Override
     public void onChildSnappedBack(View v) {
         TaskView tv = (TaskView) v;
+
         // Re-enable clipping with the stack
         tv.setClipViewInStack(true);
         // Re-enable touch events from this task view
         tv.setTouchEnabled(true);
+
+        // Stop tracking this deleting task, and update the layout to include this task again.  The
+        // stack scroll does not need to be reset, since the scroll has not actually changed in
+        // onBeginDrag().
+        mSv.removeIgnoreTask(tv.getTask());
+        mSv.updateLayoutAlgorithm(false /* boundScroll */);
+        mSwipeHelperAnimations.remove(v);
     }
 
     @Override
@@ -414,4 +515,59 @@
         return 0;
     }
 
+    /**
+     * Interpolates the non-deleting tasks to their final transforms from their current transforms.
+     */
+    private void updateTaskViewTransforms(float dismissFraction) {
+        List<TaskView> taskViews = mSv.getTaskViews();
+        int taskViewCount = taskViews.size();
+        for (int i = 0; i < taskViewCount; i++) {
+            TaskView tv = taskViews.get(i);
+            Task task = tv.getTask();
+
+            if (mSv.isIgnoredTask(task)) {
+                continue;
+            }
+
+            int taskIndex = mCurrentTasks.indexOf(task);
+            TaskViewTransform fromTransform = mCurrentTaskTransforms.get(taskIndex);
+            TaskViewTransform toTransform = mFinalTaskTransforms.get(taskIndex);
+
+            mTmpTransform.copyFrom(fromTransform);
+            // We only really need to interpolate the bounds, progress and translation
+            mTmpTransform.rect.set(RECT_EVALUATOR.evaluate(dismissFraction, fromTransform.rect,
+                    toTransform.rect));
+            mTmpTransform.p = fromTransform.p + (toTransform.p - fromTransform.p) * dismissFraction;
+            mTmpTransform.translationZ = fromTransform.translationZ +
+                    (toTransform.translationZ - fromTransform.translationZ) * dismissFraction;
+
+            mSv.updateTaskViewToTransform(tv, mTmpTransform, TaskViewAnimation.IMMEDIATE);
+        }
+    }
+
+    /** Returns the view at the specified coordinates */
+    private TaskView findViewAtPoint(int x, int y) {
+        List<Task> tasks = mSv.getStack().getStackTasks();
+        int taskCount = tasks.size();
+        for (int i = taskCount - 1; i >= 0; i--) {
+            TaskView tv = mSv.getChildViewForTask(tasks.get(i));
+            if (tv != null && tv.getVisibility() == View.VISIBLE) {
+                if (mSv.isTouchPointInView(x, y, tv)) {
+                    return tv;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the fraction which we should interpolate the other task views based on the dismissal
+     * of this given task.
+     *
+     * TODO: We can interpolate this to adjust when the other tasks should respond to the dismissal
+     */
+    private float getDismissFraction(View v) {
+        float fraction = Math.min(1f, Math.abs(v.getTranslationX() / mSv.getWidth()));
+        return STACK_TRANSFORM_INTERPOLATOR.getInterpolation(fraction);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
index 9b72702..32bebb3 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
@@ -290,6 +290,7 @@
         setDim(0);
         setVisibility(View.VISIBLE);
         getViewBounds().reset();
+        getHeaderView().reset();
         TaskViewTransform.reset(this);
 
         mActionButtonView.setScaleX(1f);
@@ -459,16 +460,14 @@
     public void showActionButton(boolean fadeIn, int fadeInDuration) {
         mActionButtonView.setVisibility(View.VISIBLE);
 
-        if (fadeIn) {
-            if (mActionButtonView.getAlpha() < 1f) {
-                mActionButtonView.animate()
-                        .alpha(1f)
-                        .scaleX(1f)
-                        .scaleY(1f)
-                        .setDuration(fadeInDuration)
-                        .setInterpolator(PhoneStatusBar.ALPHA_IN)
-                        .start();
-            }
+        if (fadeIn && mActionButtonView.getAlpha() < 1f) {
+            mActionButtonView.animate()
+                    .alpha(1f)
+                    .scaleX(1f)
+                    .scaleY(1f)
+                    .setDuration(fadeInDuration)
+                    .setInterpolator(PhoneStatusBar.ALPHA_IN)
+                    .start();
         } else {
             mActionButtonView.setScaleX(1f);
             mActionButtonView.setScaleY(1f);
@@ -484,29 +483,27 @@
      */
     public void hideActionButton(boolean fadeOut, int fadeOutDuration, boolean scaleDown,
             final Animator.AnimatorListener animListener) {
-        if (fadeOut) {
-            if (mActionButtonView.getAlpha() > 0f) {
-                if (scaleDown) {
-                    float toScale = 0.9f;
-                    mActionButtonView.animate()
-                            .scaleX(toScale)
-                            .scaleY(toScale);
-                }
+        if (fadeOut && mActionButtonView.getAlpha() > 0f) {
+            if (scaleDown) {
+                float toScale = 0.9f;
                 mActionButtonView.animate()
-                        .alpha(0f)
-                        .setDuration(fadeOutDuration)
-                        .setInterpolator(PhoneStatusBar.ALPHA_OUT)
-                        .withEndAction(new Runnable() {
-                            @Override
-                            public void run() {
-                                if (animListener != null) {
-                                    animListener.onAnimationEnd(null);
-                                }
-                                mActionButtonView.setVisibility(View.INVISIBLE);
-                            }
-                        })
-                        .start();
+                        .scaleX(toScale)
+                        .scaleY(toScale);
             }
+            mActionButtonView.animate()
+                    .alpha(0f)
+                    .setDuration(fadeOutDuration)
+                    .setInterpolator(PhoneStatusBar.ALPHA_OUT)
+                    .withEndAction(new Runnable() {
+                        @Override
+                        public void run() {
+                            if (animListener != null) {
+                                animListener.onAnimationEnd(null);
+                            }
+                            mActionButtonView.setVisibility(View.INVISIBLE);
+                        }
+                    })
+                    .start();
         } else {
             mActionButtonView.setAlpha(0f);
             mActionButtonView.setVisibility(View.INVISIBLE);
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java
index e7717ac..827ee40 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java
@@ -16,22 +16,27 @@
 
 package com.android.systemui.recents.views;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.annotation.Nullable;
+import android.content.ComponentName;
 import android.content.Context;
+import android.content.pm.ActivityInfo;
 import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.ColorFilter;
 import android.graphics.Paint;
-import android.graphics.PorterDuff;
 import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
-import android.graphics.drawable.RippleDrawable;
-import android.support.v4.graphics.ColorUtils;
 import android.os.CountDownTimer;
+import android.support.v4.graphics.ColorUtils;
 import android.util.AttributeSet;
 import android.view.View;
+import android.view.ViewAnimationUtils;
+import android.view.ViewStub;
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
 import android.widget.FrameLayout;
@@ -39,10 +44,10 @@
 import android.widget.ProgressBar;
 import android.widget.TextView;
 import com.android.internal.logging.MetricsLogger;
-
 import com.android.systemui.R;
 import com.android.systemui.recents.Constants;
 import com.android.systemui.recents.Recents;
+import com.android.systemui.recents.RecentsDebugFlags;
 import com.android.systemui.recents.events.EventBus;
 import com.android.systemui.recents.events.activity.LaunchTaskEvent;
 import com.android.systemui.recents.events.ui.ShowApplicationInfoEvent;
@@ -59,6 +64,8 @@
         implements View.OnClickListener, View.OnLongClickListener {
 
     private static final float HIGHLIGHT_LIGHTNESS_INCREMENT = 0.125f;
+    private static final float OVERLAY_LIGHTNESS_INCREMENT = -0.0625f;
+    private static final int OVERLAY_REVEAL_DURATION = 250;
     private static final long FOCUS_INDICATOR_INTERVAL_MS = 30;
 
     /**
@@ -69,8 +76,6 @@
         private Paint mHighlightPaint = new Paint();
         private Paint mBackgroundPaint = new Paint();
 
-        private float[] mTmpHSL = new float[3];
-
         public HighlightColorDrawable() {
             mBackgroundPaint.setColor(Color.argb(255, 0, 0, 0));
             mBackgroundPaint.setAntiAlias(true);
@@ -122,11 +127,16 @@
     Task mTask;
 
     // Header views
-    ImageView mMoveTaskButton;
-    ImageView mDismissButton;
     ImageView mIconView;
     TextView mTitleView;
-    int mMoveTaskTargetStackId = INVALID_STACK_ID;
+    ImageView mMoveTaskButton;
+    ImageView mDismissButton;
+    ViewStub mAppOverlayViewStub;
+    FrameLayout mAppOverlayView;
+    ImageView mAppIconView;
+    ImageView mAppInfoView;
+    TextView mAppTitleView;
+    ViewStub mFocusTimerIndicatorStub;
     ProgressBar mFocusTimerIndicator;
 
     // Header drawables
@@ -140,21 +150,26 @@
     Drawable mDarkFreeformIcon;
     Drawable mLightFullscreenIcon;
     Drawable mDarkFullscreenIcon;
+    Drawable mLightInfoIcon;
+    Drawable mDarkInfoIcon;
     int mTaskBarViewLightTextColor;
     int mTaskBarViewDarkTextColor;
+    int mMoveTaskTargetStackId = INVALID_STACK_ID;
 
     // Header background
     private HighlightColorDrawable mBackground;
+    private HighlightColorDrawable mOverlayBackground;
+    private float[] mTmpHSL = new float[3];
 
     // Header dim, which is only used when task view hardware layers are not used
     private Paint mDimLayerPaint = new Paint();
 
     Interpolator mFastOutSlowInInterpolator;
     Interpolator mFastOutLinearInInterpolator;
+    Interpolator mLinearOutSlowInInterpolator;
 
-    long mFocusIndicatorProgress;
     private CountDownTimer mFocusTimerCountDown;
-    long mFocusTimerDuration;
+    private long mFocusTimerDuration;
 
     public TaskViewHeader(Context context) {
         this(context, null);
@@ -184,36 +199,45 @@
         mDarkFreeformIcon = context.getDrawable(R.drawable.recents_move_task_freeform_dark);
         mLightFullscreenIcon = context.getDrawable(R.drawable.recents_move_task_fullscreen_light);
         mDarkFullscreenIcon = context.getDrawable(R.drawable.recents_move_task_fullscreen_dark);
+        mLightInfoIcon = context.getDrawable(R.drawable.recents_info_light);
+        mDarkInfoIcon = context.getDrawable(R.drawable.recents_info_dark);
 
         mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context,
                 com.android.internal.R.interpolator.fast_out_slow_in);
         mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(context,
                 com.android.internal.R.interpolator.fast_out_linear_in);
+        mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(context,
+                com.android.internal.R.interpolator.linear_out_slow_in);
 
         // Configure the background and dim
         mBackground = new HighlightColorDrawable();
         mBackground.setColorAndDim(Color.argb(255, 0, 0, 0), 0f);
         setBackground(mBackground);
+        mOverlayBackground = new HighlightColorDrawable();
         mDimLayerPaint.setColor(Color.argb(255, 0, 0, 0));
         mDimLayerPaint.setAntiAlias(true);
         mFocusTimerDuration = res.getInteger(R.integer.recents_auto_advance_duration);
     }
 
+    /**
+     * Resets this header along with the TaskView.
+     */
+    public void reset() {
+        hideAppOverlay(true /* immediate */);
+    }
+
     @Override
     protected void onFinishInflate() {
         // Initialize the icon and description views
         mIconView = (ImageView) findViewById(R.id.icon);
+        mIconView.setClickable(false);
         mIconView.setOnLongClickListener(this);
         mTitleView = (TextView) findViewById(R.id.title);
         mDismissButton = (ImageView) findViewById(R.id.dismiss_task);
         mDismissButton.setOnClickListener(this);
         mMoveTaskButton = (ImageView) findViewById(R.id.move_task);
-        mFocusTimerIndicator = (ProgressBar) findViewById(R.id.focus_timer_indicator);
-
-        // Hide the backgrounds if they are ripple drawables
-        if (mIconView.getBackground() instanceof RippleDrawable) {
-            mIconView.setBackground(null);
-        }
+        mFocusTimerIndicatorStub = (ViewStub) findViewById(R.id.focus_timer_indicator_stub);
+        mAppOverlayViewStub = (ViewStub) findViewById(R.id.app_overlay_stub);
     }
 
     /**
@@ -228,6 +252,7 @@
 
         mTaskViewRect.set(0, 0, width, height);
         boolean updateMoveTaskButton = mMoveTaskButton.getVisibility() != View.GONE;
+        boolean isFreeformTask = (mTask != null) && mTask.isFreeformTask();
         int appIconWidth = mIconView.getMeasuredWidth();
         int activityDescWidth = (mTask != null)
                 ? (int) mTitleView.getPaint().measureText(mTask.title)
@@ -239,19 +264,20 @@
 
         // Priority-wise, we show the activity icon first, the dismiss icon if there is room, the
         // move-task icon if there is room, and then finally, the activity label if there is room
-        if (width < (appIconWidth + dismissIconWidth)) {
+        if (isFreeformTask && width < (appIconWidth + dismissIconWidth)) {
             mTitleView.setVisibility(View.INVISIBLE);
             if (updateMoveTaskButton) {
                 mMoveTaskButton.setVisibility(View.INVISIBLE);
             }
             mDismissButton.setVisibility(View.INVISIBLE);
-        } else if (width < (appIconWidth + dismissIconWidth + moveTaskIconWidth)) {
+        } else if (isFreeformTask && width < (appIconWidth + dismissIconWidth +
+                moveTaskIconWidth)) {
             mTitleView.setVisibility(View.INVISIBLE);
             if (updateMoveTaskButton) {
                 mMoveTaskButton.setVisibility(View.INVISIBLE);
             }
             mDismissButton.setVisibility(View.VISIBLE);
-        } else if (width < (appIconWidth + dismissIconWidth + moveTaskIconWidth +
+        } else if (isFreeformTask && width < (appIconWidth + dismissIconWidth + moveTaskIconWidth +
                 activityDescWidth)) {
             mTitleView.setVisibility(View.INVISIBLE);
             if (updateMoveTaskButton) {
@@ -273,11 +299,6 @@
     }
 
     @Override
-    protected boolean verifyDrawable(Drawable who) {
-        return super.verifyDrawable(who) || (who == mBackground);
-    }
-
-    @Override
     protected void dispatchDraw(Canvas canvas) {
         super.dispatchDraw(canvas);
 
@@ -288,6 +309,10 @@
 
     /** Starts the focus timer. */
     public void startFocusTimerIndicator() {
+        if (mFocusTimerIndicator == null) {
+            return;
+        }
+
         mFocusTimerIndicator.setVisibility(View.VISIBLE);
         mFocusTimerIndicator.setMax((int) mFocusTimerDuration);
         if (mFocusTimerCountDown == null) {
@@ -308,7 +333,11 @@
 
     /** Cancels the focus timer. */
     public void cancelFocusTimerIndicator() {
-        if (mFocusTimerCountDown != null && mFocusTimerIndicator != null) {
+        if (mFocusTimerIndicator == null) {
+            return;
+        }
+
+        if (mFocusTimerCountDown != null) {
             mFocusTimerCountDown.cancel();
             mFocusTimerIndicator.setProgress(0);
             mFocusTimerIndicator.setVisibility(View.INVISIBLE);
@@ -337,6 +366,10 @@
     private void updateBackgroundColor(float dimAlpha) {
         if (mTask != null) {
             mBackground.setColorAndDim(mTask.colorPrimary, dimAlpha);
+            // TODO: Consider using the saturation of the color to adjust the lightness as well
+            ColorUtils.colorToHSL(mTask.colorPrimary, mTmpHSL);
+            mTmpHSL[2] = Math.min(1f, mTmpHSL[2] + OVERLAY_LIGHTNESS_INCREMENT * (1.0f - dimAlpha));
+            mOverlayBackground.setColorAndDim(ColorUtils.HSLToColor(mTmpHSL), dimAlpha);
             mDimLayerPaint.setAlpha((int) (dimAlpha * 255));
         }
     }
@@ -382,10 +415,15 @@
             mMoveTaskButton.setOnClickListener(this);
         }
 
-        mFocusTimerIndicator.getProgressDrawable()
-                .setColorFilter(
-                        getSecondaryColor(t.colorPrimary, t.useLightOnPrimaryColor),
-                        PorterDuff.Mode.SRC_IN);
+        if (Recents.getDebugFlags().isFastToggleIndicatorEnabled()) {
+            if (mFocusTimerIndicator == null) {
+                mFocusTimerIndicator = (ProgressBar) mFocusTimerIndicatorStub.inflate();
+            }
+            mFocusTimerIndicator.getProgressDrawable()
+                    .setColorFilter(
+                            getSecondaryColor(t.colorPrimary, t.useLightOnPrimaryColor),
+                            PorterDuff.Mode.SRC_IN);
+        }
 
         // In accessibility, a single click on the focused app info button will show it
         if (ssp.isTouchExplorationEnabled()) {
@@ -447,8 +485,11 @@
     @Override
     public void onClick(View v) {
         if (v == mIconView) {
-            // In accessibility, a single click on the focused app info button will show it
-            EventBus.getDefault().send(new ShowApplicationInfoEvent(mTask));
+            SystemServicesProxy ssp = Recents.getSystemServices();
+            if (ssp.isTouchExplorationEnabled()) {
+                // In accessibility, a single click on the focused app info button will show it
+                EventBus.getDefault().send(new ShowApplicationInfoEvent(mTask));
+            }
         } else if (v == mDismissButton) {
             TaskView tv = Utilities.findParent(this, TaskView.class);
             tv.dismissTask();
@@ -463,15 +504,95 @@
                     : new Rect();
             EventBus.getDefault().send(new LaunchTaskEvent(tv, mTask, bounds,
                     mMoveTaskTargetStackId, false));
+        } else if (v == mAppInfoView) {
+            EventBus.getDefault().send(new ShowApplicationInfoEvent(mTask));
+        } else if (v == mAppIconView) {
+            hideAppOverlay(false /* immediate */);
         }
     }
 
     @Override
     public boolean onLongClick(View v) {
         if (v == mIconView) {
-            EventBus.getDefault().send(new ShowApplicationInfoEvent(mTask));
+            showAppOverlay();
+            return true;
+        } else if (v == mAppIconView) {
+            hideAppOverlay(false /* immediate */);
             return true;
         }
         return false;
     }
+
+    /**
+     * Shows the application overlay.
+     */
+    private void showAppOverlay() {
+        // Skip early if the task is invalid
+        SystemServicesProxy ssp = Recents.getSystemServices();
+        ComponentName cn = mTask.key.getComponent();
+        int userId = mTask.key.userId;
+        ActivityInfo activityInfo = ssp.getActivityInfo(cn, userId);
+        if (activityInfo == null) {
+            return;
+        }
+
+        // Inflate the overlay if necessary
+        if (mAppOverlayView == null) {
+            mAppOverlayView = (FrameLayout) mAppOverlayViewStub.inflate();
+            mAppOverlayView.setBackground(mOverlayBackground);
+            mAppIconView = (ImageView) mAppOverlayView.findViewById(R.id.app_icon);
+            mAppIconView.setOnClickListener(this);
+            mAppIconView.setOnLongClickListener(this);
+            mAppInfoView = (ImageView) mAppOverlayView.findViewById(R.id.app_info);
+            mAppInfoView.setOnClickListener(this);
+            mAppTitleView = (TextView) mAppOverlayView.findViewById(R.id.app_title);
+        }
+
+        // Update the overlay contents for the current app
+        mAppTitleView.setText(ssp.getBadgedApplicationLabel(activityInfo.applicationInfo, userId));
+        mAppTitleView.setTextColor(mTask.useLightOnPrimaryColor ?
+                mTaskBarViewLightTextColor : mTaskBarViewDarkTextColor);
+        mAppIconView.setImageDrawable(ssp.getBadgedApplicationIcon(activityInfo.applicationInfo,
+                userId));
+        mAppInfoView.setImageDrawable(mTask.useLightOnPrimaryColor
+                ? mLightInfoIcon
+                : mDarkInfoIcon);
+        mAppOverlayView.setVisibility(View.VISIBLE);
+
+        int x = mIconView.getLeft() + mIconView.getWidth() / 2;
+        int y = mIconView.getTop() + mIconView.getHeight() / 2;
+        Animator revealAnim = ViewAnimationUtils.createCircularReveal(mAppOverlayView, x, y, 0,
+                getWidth());
+        revealAnim.setDuration(OVERLAY_REVEAL_DURATION);
+        revealAnim.setInterpolator(mLinearOutSlowInInterpolator);
+        revealAnim.start();
+    }
+
+    /**
+     * Hide the application overlay.
+     */
+    private void hideAppOverlay(boolean immediate) {
+        // Skip if we haven't even loaded the overlay yet
+        if (mAppOverlayView == null) {
+            return;
+        }
+
+        if (immediate) {
+            mAppOverlayView.setVisibility(View.GONE);
+        } else {
+            int x = mIconView.getLeft() + mIconView.getWidth() / 2;
+            int y = mIconView.getTop() + mIconView.getHeight() / 2;
+            Animator revealAnim = ViewAnimationUtils.createCircularReveal(mAppOverlayView, x, y,
+                    getWidth(), 0);
+            revealAnim.setDuration(OVERLAY_REVEAL_DURATION);
+            revealAnim.setInterpolator(mLinearOutSlowInInterpolator);
+            revealAnim.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    mAppOverlayView.setVisibility(View.GONE);
+                }
+            });
+            revealAnim.start();
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java
index 538c248..85b7c82 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java
@@ -88,12 +88,39 @@
     public float alpha = 1f;
 
     public boolean visible = false;
-    float p = 0f;
+
+    // This is the relative task progress of this task, relative to the stack scroll at which this
+    // transform was computed
+    public float p = 0f;
 
     // This is a window-space rect used for positioning the task in the stack and freeform workspace
     public RectF rect = new RectF();
 
     /**
+     * Fills int this transform from the state of the given TaskView.
+     */
+    public void fillIn(TaskView tv) {
+        translationZ = tv.getTranslationZ();
+        scale = tv.getScaleX();
+        alpha = tv.getAlpha();
+        visible = true;
+        p = tv.getTaskProgress();
+        rect.set(tv.getLeft(), tv.getTop(), tv.getRight(), tv.getBottom());
+    }
+
+    /**
+     * Copies the transform state from another {@link TaskViewTransform}.
+     */
+    public void copyFrom(TaskViewTransform other) {
+        translationZ = other.translationZ;
+        scale = other.scale;
+        alpha = other.alpha;
+        visible = other.visible;
+        p = other.p;
+        rect.set(other.rect);
+    }
+
+    /**
      * Resets the current transform.
      */
     public void reset() {
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java
index c16703e8..9e83dcf 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java
@@ -167,8 +167,7 @@
     public boolean startDragging(boolean animate) {
         mHandle.setTouching(true, animate);
         mDockSide = mWindowManagerProxy.getDockSide();
-        mSnapAlgorithm = new DividerSnapAlgorithm(getContext().getResources(),
-                mFlingAnimationUtils.getMinVelocityPxPerSecond(), mDisplayWidth,
+        mSnapAlgorithm = new DividerSnapAlgorithm(getContext().getResources(), mDisplayWidth,
                 mDisplayHeight, mDividerSize, isHorizontalDivision(), mStableInsets);
         if (mDockSide != WindowManager.DOCKED_INVALID) {
             mWindowManagerProxy.setResizing(true);
@@ -180,9 +179,9 @@
         }
     }
 
-    public void stopDragging(int position, float velocity) {
+    public void stopDragging(int position, float velocity, boolean avoidDismissStart) {
         mHandle.setTouching(false, true /* animate */);
-        fling(position, velocity);
+        fling(position, velocity, avoidDismissStart);
         mWindowManager.setSlippery(true);
         releaseBackground();
     }
@@ -225,7 +224,7 @@
                 if (mMoving && mDockSide != WindowManager.DOCKED_INVALID) {
                     int position = calculatePosition(x, y);
                     SnapTarget snapTarget = mSnapAlgorithm.calculateSnapTarget(position,
-                            0 /* velocity */);
+                            0 /* velocity */, false /* hardDismiss */);
                     resizeStack(calculatePosition(x, y), snapTarget.position, snapTarget);
                 }
                 break;
@@ -239,7 +238,7 @@
                 mVelocityTracker.computeCurrentVelocity(1000);
                 int position = calculatePosition(x, y);
                 stopDragging(position, isHorizontalDivision() ? mVelocityTracker.getYVelocity()
-                        : mVelocityTracker.getXVelocity());
+                        : mVelocityTracker.getXVelocity(), false /* avoidDismissStart */);
                 mMoving = false;
                 break;
         }
@@ -250,8 +249,12 @@
         event.setLocation(event.getRawX(), event.getRawY());
     }
 
-    private void fling(int position, float velocity) {
-        final SnapTarget snapTarget = mSnapAlgorithm.calculateSnapTarget(position, velocity);
+    private void fling(int position, float velocity, boolean avoidDismissStart) {
+        SnapTarget snapTarget = mSnapAlgorithm.calculateSnapTarget(position, velocity);
+        if (avoidDismissStart && snapTarget == mSnapAlgorithm.getDismissStartTarget()) {
+            snapTarget = mSnapAlgorithm.getFirstSplitTarget();
+        }
+        final SnapTarget finalTarget = snapTarget;
 
         ValueAnimator anim = ValueAnimator.ofInt(position, snapTarget.position);
         anim.addUpdateListener(new AnimatorUpdateListener() {
@@ -260,13 +263,13 @@
                 resizeStack((Integer) animation.getAnimatedValue(),
                         animation.getAnimatedFraction() == 1f
                                 ? TASK_POSITION_SAME
-                                : snapTarget.position, snapTarget);
+                                : finalTarget.position, finalTarget);
             }
         });
         anim.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
-                commitSnapFlags(snapTarget);
+                commitSnapFlags(finalTarget);
                 mWindowManagerProxy.setResizing(false);
                 mDockSide = WindowManager.DOCKED_INVALID;
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java
index da3cd54..38d24ce 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java
@@ -176,26 +176,31 @@
     };
 
     @Override
-    public boolean dispatchTouchEvent(MotionEvent event) {
-        if (mDimmed && !mActivated) {
-            return handleTouchEventDimmed(event);
-        } else {
-            return super.dispatchTouchEvent(event);
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (mDimmed && !mActivated
+                && ev.getActionMasked() == MotionEvent.ACTION_DOWN && disallowSingleClick(ev)) {
+            return true;
         }
+        return super.onInterceptTouchEvent(ev);
+    }
+
+    protected boolean disallowSingleClick(MotionEvent ev) {
+        return false;
     }
 
     @Override
     public boolean onTouchEvent(MotionEvent event) {
         boolean result;
-        if (mDimmed && mActivated) {
+        if (mDimmed) {
+            boolean wasActivated = mActivated;
             result = handleTouchEventDimmed(event);
+            if (wasActivated && result && event.getAction() == MotionEvent.ACTION_UP) {
+                mFalsingManager.onNotificationDoubleTap();
+                removeCallbacks(mTapTimeoutRunnable);
+            }
         } else {
             result = super.onTouchEvent(event);
         }
-        if (mActivated && result && event.getAction() == MotionEvent.ACTION_UP) {
-            mFalsingManager.onNotificationDoubleTap();
-            removeCallbacks(mTapTimeoutRunnable);
-        }
         return result;
     }
 
@@ -608,8 +613,6 @@
                     onFinishedRunnable.run();
                 }
                 if (!mWasCancelled) {
-                    mAppearAnimationFraction = -1;
-                    setOutlineRect(null);
                     enableAppearDrawing(false);
                 }
             }
@@ -630,6 +633,7 @@
     private void cancelAppearAnimation() {
         if (mAppearAnimator != null) {
             mAppearAnimator.cancel();
+            mAppearAnimator = null;
         }
     }
 
@@ -735,6 +739,8 @@
             mDrawingAppearAnimation = enable;
             if (!enable) {
                 setContentAlpha(1.0f);
+                mAppearAnimationFraction = -1;
+                setOutlineRect(null);
             }
             invalidate();
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
index ce4eff5..2592486 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
@@ -1265,18 +1265,22 @@
     }
 
     /**
-     * Called when the size of the notification panel changes
+     * Called when the notification panel layouts
      */
-    public void onPanelHeightChanged() {
+    public void onPanelLaidOut() {
         if (mState == StatusBarState.KEYGUARD) {
             // Since the number of notifications is determined based on the height of the view, we
             // need to update them.
-            updateRowStates();
+            int maxBefore = getMaxKeyguardNotifications(false /* recompute */);
+            int maxNotifications = getMaxKeyguardNotifications(true /* recompute */);
+            if (maxBefore != maxNotifications) {
+                updateRowStates();
+            }
         }
     }
 
     @Override
-    public void onExpandClicked(View clickedView, boolean nowExpanded) {
+    public void onExpandClicked(Entry clickedEntry, boolean nowExpanded) {
     }
 
     protected class H extends Handler {
@@ -1359,6 +1363,7 @@
                     parent, false);
             row.setExpansionLogger(this, entry.notification.getKey());
             row.setGroupManager(mGroupManager);
+            row.setHeadsUpManager(mHeadsUpManager);
             row.setRemoteInputController(mRemoteInputController);
             row.setOnExpandClickListener(this);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java
new file mode 100644
index 0000000..f71f092
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java
@@ -0,0 +1,65 @@
+/*
+ * 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.statusbar;
+
+import android.view.View;
+
+import com.android.systemui.statusbar.phone.PhoneStatusBar;
+
+/**
+ * A helper to fade views in and out.
+ */
+public class CrossFadeHelper {
+    public static final long ANIMATION_DURATION_LENGTH = 210;
+
+    public static void fadeOut(final View view, final Runnable endRunnable) {
+        view.animate().cancel();
+        view.animate()
+                .alpha(0f)
+                .setDuration(ANIMATION_DURATION_LENGTH)
+                .setInterpolator(PhoneStatusBar.ALPHA_OUT)
+                .withEndAction(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (endRunnable != null) {
+                            endRunnable.run();
+                        }
+                        view.setVisibility(View.INVISIBLE);
+                    }
+                });
+        if (view.hasOverlappingRendering()) {
+            view.animate().withLayer();
+        }
+
+    }
+
+    public static void fadeIn(final View view) {
+        view.animate().cancel();
+        if (view.getVisibility() == View.INVISIBLE) {
+            view.setAlpha(0.0f);
+            view.setVisibility(View.VISIBLE);
+        }
+        view.animate()
+                .alpha(1f)
+                .setDuration(ANIMATION_DURATION_LENGTH)
+                .setInterpolator(PhoneStatusBar.ALPHA_IN)
+                .withEndAction(null);
+        if (view.hasOverlappingRendering()) {
+            view.animate().withLayer();
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
index 83853a2..52e6a9c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
@@ -25,8 +25,10 @@
 import android.os.Build;
 import android.service.notification.StatusBarNotification;
 import android.util.AttributeSet;
+import android.view.MotionEvent;
 import android.view.NotificationHeaderView;
 import android.view.View;
+import android.view.ViewGroup;
 import android.view.ViewStub;
 import android.view.accessibility.AccessibilityEvent;
 import android.widget.Chronometer;
@@ -35,7 +37,9 @@
 
 import com.android.systemui.R;
 import com.android.systemui.classifier.FalsingManager;
+import com.android.systemui.statusbar.notification.NotificationViewWrapper;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.statusbar.stack.NotificationChildrenContainer;
 import com.android.systemui.statusbar.stack.StackScrollState;
 import com.android.systemui.statusbar.stack.StackStateAnimator;
@@ -59,6 +63,11 @@
     private boolean mHasUserChangedExpansion;
     /** If {@link #mHasUserChangedExpansion}, has the user expanded this row */
     private boolean mUserExpanded;
+
+    /**
+     * Has this notification been expanded while it was pinned
+     */
+    private boolean mExpandedWhenPinned;
     /** Is the user touching this row */
     private boolean mUserLocked;
     /** Are we showing the "public" version */
@@ -103,6 +112,7 @@
     private boolean mIsSystemChildExpanded;
     private boolean mIsPinned;
     private FalsingManager mFalsingManager;
+    private HeadsUpManager mHeadsUpManager;
     private NotificationHeaderUtil mHeaderUtil = new NotificationHeaderUtil(this);
 
     private boolean mJustClicked;
@@ -115,14 +125,19 @@
         public void onClick(View v) {
             if (!mShowingPublic && mGroupManager.isSummaryOfGroup(mStatusBarNotification)) {
                 mGroupManager.toggleGroupExpansion(mStatusBarNotification);
-                mOnExpandClickListener.onExpandClicked(ExpandableNotificationRow.this,
+                mOnExpandClickListener.onExpandClicked(mEntry,
                         mGroupManager.isGroupExpanded(mStatusBarNotification));
             } else {
-                boolean nowExpanded = !isExpanded();
-                setUserExpanded(nowExpanded);
+                boolean nowExpanded;
+                if (isPinned()) {
+                    nowExpanded = !mExpandedWhenPinned;
+                    mExpandedWhenPinned = nowExpanded;
+                } else {
+                    nowExpanded = !isExpanded();
+                    setUserExpanded(nowExpanded);
+                }
                 notifyHeightChanged(true);
-                mOnExpandClickListener.onExpandClicked(ExpandableNotificationRow.this,
-                        nowExpanded);
+                mOnExpandClickListener.onExpandClicked(mEntry, nowExpanded);
             }
         }
     };
@@ -303,6 +318,16 @@
     }
 
     @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (event.getActionMasked() != MotionEvent.ACTION_DOWN
+                || !isChildInGroup() || isGroupExpanded()) {
+            return super.onTouchEvent(event);
+        } else {
+            return false;
+        }
+    }
+
+    @Override
     protected boolean shouldHideBackground() {
         return super.shouldHideBackground() || mShowNoBackground;
     }
@@ -358,9 +383,9 @@
     }
 
     public void startChildAnimation(StackScrollState finalState,
-            StackStateAnimator stateAnimator, boolean withDelays, long delay, long duration) {
+            StackStateAnimator stateAnimator, long delay, long duration) {
         if (mIsSummaryWithChildren) {
-            mChildrenContainer.startAnimationToState(finalState, stateAnimator, withDelays, delay,
+            mChildrenContainer.startAnimationToState(finalState, stateAnimator, delay,
                     duration);
         }
     }
@@ -386,6 +411,12 @@
      */
     public void setPinned(boolean pinned) {
         mIsPinned = pinned;
+        if (pinned) {
+            setIconAnimationRunning(true);
+            mExpandedWhenPinned = false;
+        } else if (mExpandedWhenPinned) {
+            setUserExpanded(true);
+        }
         setChronometerRunning(mLastChronometerRunning);
     }
 
@@ -393,11 +424,22 @@
         return mIsPinned;
     }
 
-    public int getHeadsUpHeight() {
+    /**
+     * @param atLeastMinHeight should the value returned be at least the minimum height.
+     *                         Used to avoid cyclic calls
+     * @return the height of the heads up notification when pinned
+     */
+    public int getPinnedHeadsUpHeight(boolean atLeastMinHeight) {
         if (mIsSummaryWithChildren) {
             return mChildrenContainer.getIntrinsicHeight();
         }
-        return mHeadsUpHeight;
+        if(mExpandedWhenPinned) {
+            return Math.max(getMaxExpandHeight(), mHeadsUpHeight);
+        } else if (atLeastMinHeight) {
+            return Math.max(getMinHeight(), mHeadsUpHeight);
+        } else {
+            return mHeadsUpHeight;
+        }
     }
 
     /**
@@ -461,6 +503,10 @@
         mOnExpandClickListener = onExpandClickListener;
     }
 
+    public void setHeadsUpManager(HeadsUpManager headsUpManager) {
+        mHeadsUpManager = headsUpManager;
+    }
+
     public interface ExpansionLogger {
         public void logNotificationExpansion(String key, boolean userAction, boolean expanded);
     }
@@ -517,8 +563,10 @@
     protected void onFinishInflate() {
         super.onFinishInflate();
         mPublicLayout = (NotificationContentView) findViewById(R.id.expandedPublic);
+        mPublicLayout.setContainingNotification(this);
         mPrivateLayout = (NotificationContentView) findViewById(R.id.expanded);
         mPrivateLayout.setExpandClickListener(mExpandClickListener);
+        mPrivateLayout.setContainingNotification(this);
         mPublicLayout.setExpandClickListener(mExpandClickListener);
         mGutsStub = (ViewStub) findViewById(R.id.notification_guts_stub);
         mGutsStub.setOnInflateListener(new ViewStub.OnInflateListener() {
@@ -551,13 +599,14 @@
     private void updateChildrenVisibility() {
         mPrivateLayout.setVisibility(!mShowingPublic && !mIsSummaryWithChildren ? VISIBLE
                 : INVISIBLE);
-        if (mChildrenContainer == null) {
-            return;
+        if (mChildrenContainer != null) {
+            mChildrenContainer.setVisibility(!mShowingPublic && mIsSummaryWithChildren ? VISIBLE
+                    : INVISIBLE);
         }
-        mChildrenContainer.setVisibility(!mShowingPublic && mIsSummaryWithChildren ? VISIBLE
-                : INVISIBLE);
-        mNotificationHeader.setVisibility(!mShowingPublic && mIsSummaryWithChildren ? VISIBLE
-                : INVISIBLE);
+        if (mNotificationHeader != null) {
+            mNotificationHeader.setVisibility(!mShowingPublic && mIsSummaryWithChildren ? VISIBLE
+                    : INVISIBLE);
+        }
         // The limits might have changed if the view suddenly became a group or vice versa
         updateLimits();
     }
@@ -601,6 +650,12 @@
         mPrivateLayout.updateExpandButtons(isExpandable());
     }
 
+    @Override
+    public void setClipToActualHeight(boolean clipToActualHeight) {
+        super.setClipToActualHeight(clipToActualHeight);
+        getShowingLayout().setClipToActualHeight(clipToActualHeight);
+    }
+
     /**
      * @return whether the user has changed the expansion state
      */
@@ -718,7 +773,6 @@
         if (isUserLocked()) {
             return getActualHeight();
         }
-        boolean inExpansionState = isExpanded();
         if (mGuts != null && mGuts.areGutsExposed()) {
             return mGuts.getHeight();
         } else if ((isChildInGroup() && !isGroupExpanded())) {
@@ -728,12 +782,14 @@
         } else if (mIsSummaryWithChildren && !mOnKeyguard) {
             return mChildrenContainer.getIntrinsicHeight();
         } else if (mIsHeadsUp) {
-            if (inExpansionState) {
+            if (isPinned()) {
+                return getPinnedHeadsUpHeight(true /* atLeastMinHeight */);
+            } else if (isExpanded()) {
                 return Math.max(getMaxExpandHeight(), mHeadsUpHeight);
             } else {
                 return Math.max(getMinHeight(), mHeadsUpHeight);
             }
-        } else if (inExpansionState) {
+        } else if (isExpanded()) {
             return getMaxExpandHeight();
         } else {
             return getMinHeight();
@@ -818,6 +874,12 @@
         }
     }
 
+    @Override
+    public void notifyHeightChanged(boolean needsAnimation) {
+        super.notifyHeightChanged(needsAnimation);
+        getShowingLayout().requestSelectLayout(needsAnimation || isUserLocked());
+    }
+
     public void setSensitive(boolean sensitive) {
         mSensitive = sensitive;
     }
@@ -961,8 +1023,12 @@
 
     @Override
     public int getMinHeight() {
-        if (mIsSummaryWithChildren && !isGroupExpanded() && !mShowingPublic) {
+        if (mIsHeadsUp && mHeadsUpManager.isTrackingHeadsUp()) {
+                return getPinnedHeadsUpHeight(false /* atLeastMinHeight */);
+        } else if (mIsSummaryWithChildren && !isGroupExpanded() && !mShowingPublic) {
             return mChildrenContainer.getMinHeight();
+        } else if (mIsHeadsUp) {
+            return mHeadsUpHeight;
         }
         NotificationContentView showingLayout = getShowingLayout();
         return showingLayout.getMinHeight();
@@ -1001,7 +1067,7 @@
             addView(mNotificationHeader, indexOfChild(mChildrenContainer) + 1);
         } else {
             header.reapply(getContext(), mNotificationHeader);
-            mNotificationHeaderWrapper.notifyContentUpdated();
+            mNotificationHeaderWrapper.notifyContentUpdated(mEntry.notification);
         }
         updateHeaderExpandButton();
         updateChildrenHeaderAppearance();
@@ -1059,6 +1125,17 @@
         mLoggingKey = key;
     }
 
+    @Override
+    protected boolean disallowSingleClick(MotionEvent event) {
+        float x = event.getX();
+        float y = event.getY();
+        NotificationHeaderView header = getNotificationHeader();
+        if (header != null) {
+            return header.isInTouchRect(x, y);
+        }
+        return super.disallowSingleClick(event);
+    }
+
     private void logExpansionEvent(boolean userAction, boolean wasExpanded) {
         final boolean nowExpanded = isExpanded();
         if (wasExpanded != nowExpanded && mLogger != null) {
@@ -1067,6 +1144,6 @@
     }
 
     public interface OnExpandClickListener {
-        void onExpandClicked(View clickedView, boolean nowExpanded);
+        void onExpandClicked(NotificationData.Entry clickedEntry, boolean nowExpanded);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java
index d6855a5..c190864 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java
@@ -45,6 +45,7 @@
     private static Rect mClipRect = new Rect();
     private boolean mWillBeGone;
     private int mMinClipTopAmount = 0;
+    private boolean mClipToActualHeight = true;
 
     public ExpandableView(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -326,12 +327,21 @@
     }
 
     private void updateClipping() {
-        int top = mClipTopOptimization;
-        if (top >= getActualHeight()) {
-            top = getActualHeight() - 1;
+        if (mClipToActualHeight) {
+            int top = mClipTopOptimization;
+            if (top >= getActualHeight()) {
+                top = getActualHeight() - 1;
+            }
+            mClipRect.set(0, top, getWidth(), getActualHeight());
+            setClipBounds(mClipRect);
+        } else {
+            setClipBounds(null);
         }
-        mClipRect.set(0, top, getWidth(), getActualHeight());
-        setClipBounds(mClipRect);
+    }
+
+    public void setClipToActualHeight(boolean clipToActualHeight) {
+        mClipToActualHeight = clipToActualHeight;
+        updateClipping();
     }
 
     public int getClipTopOptimization() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
index 02a39e7..36cf906 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
@@ -20,9 +20,6 @@
 import android.app.RemoteInput;
 import android.content.Context;
 import android.graphics.Outline;
-import android.graphics.Paint;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffXfermode;
 import android.graphics.Rect;
 import android.os.Build;
 import android.service.notification.StatusBarNotification;
@@ -32,13 +29,13 @@
 import android.view.ViewGroup;
 import android.view.ViewOutlineProvider;
 import android.view.ViewTreeObserver;
-import android.view.animation.Interpolator;
-import android.view.animation.LinearInterpolator;
 import android.widget.FrameLayout;
 
 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.NotificationCustomViewWrapper;
+import com.android.systemui.statusbar.notification.NotificationViewWrapper;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
 import com.android.systemui.statusbar.policy.RemoteInputView;
 
@@ -49,7 +46,6 @@
  */
 public class NotificationContentView extends FrameLayout {
 
-    private static final long ANIMATION_DURATION_LENGTH = 170;
     private static final int VISIBLE_TYPE_CONTRACTED = 0;
     private static final int VISIBLE_TYPE_EXPANDED = 1;
     private static final int VISIBLE_TYPE_HEADSUP = 2;
@@ -57,9 +53,16 @@
 
     private final Rect mClipBounds = new Rect();
     private final int mRoundRectRadius;
-    private final Interpolator mLinearInterpolator = new LinearInterpolator();
     private final boolean mRoundRectClippingEnabled;
     private final int mMinContractedHeight;
+    private final OnLayoutChangeListener mLayoutUpdater = new OnLayoutChangeListener() {
+        @Override
+        public void onLayoutChange(View v, int left, int top, int right, int bottom,
+                int oldLeft,
+                int oldTop, int oldRight, int oldBottom) {
+            selectLayout(false /* animate */, false /* force */);
+        }
+    };
 
 
     private View mContractedChild;
@@ -76,7 +79,6 @@
     private int mUnrestrictedContentHeight;
     private int mVisibleType = VISIBLE_TYPE_CONTRACTED;
     private boolean mDark;
-    private final Paint mFadePaint = new Paint();
     private boolean mAnimate;
     private boolean mIsHeadsUp;
     private boolean mShowingLegacyBackground;
@@ -108,11 +110,12 @@
     private OnClickListener mExpandClickListener;
     private boolean mBeforeN;
     private boolean mExpandable;
+    private boolean mClipToActualHeight = true;
+    private ExpandableNotificationRow mContainingNotification;
 
     public NotificationContentView(Context context, AttributeSet attrs) {
         super(context, attrs);
         mHybridViewManager = new HybridNotificationViewManager(getContext(), this);
-        mFadePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.ADD));
         mRoundRectRadius = getResources().getDimensionPixelSize(
                 R.dimen.notification_material_rounded_rect_radius);
         mRoundRectClippingEnabled = getResources().getBoolean(
@@ -241,10 +244,12 @@
     public void setContractedChild(View child) {
         if (mContractedChild != null) {
             mContractedChild.animate().cancel();
+            mContractedChild.removeOnLayoutChangeListener(mLayoutUpdater);
             removeView(mContractedChild);
         }
         addView(child);
         mContractedChild = child;
+        mContractedChild.addOnLayoutChangeListener(mLayoutUpdater);
         mContractedWrapper = NotificationViewWrapper.wrap(getContext(), child);
         selectLayout(false /* animate */, true /* force */);
         mContractedWrapper.setDark(mDark, false /* animate */, 0 /* delay */);
@@ -254,10 +259,12 @@
     public void setExpandedChild(View child) {
         if (mExpandedChild != null) {
             mExpandedChild.animate().cancel();
+            mExpandedChild.removeOnLayoutChangeListener(mLayoutUpdater);
             removeView(mExpandedChild);
         }
         addView(child);
         mExpandedChild = child;
+        mExpandedChild.addOnLayoutChangeListener(mLayoutUpdater);
         mExpandedWrapper = NotificationViewWrapper.wrap(getContext(), child);
         selectLayout(false /* animate */, true /* force */);
         updateRoundRectClipping();
@@ -266,10 +273,12 @@
     public void setHeadsUpChild(View child) {
         if (mHeadsUpChild != null) {
             mHeadsUpChild.animate().cancel();
+            mHeadsUpChild.removeOnLayoutChangeListener(mLayoutUpdater);
             removeView(mHeadsUpChild);
         }
         addView(child);
         mHeadsUpChild = child;
+        mHeadsUpChild.addOnLayoutChangeListener(mLayoutUpdater);
         mHeadsUpWrapper = NotificationViewWrapper.wrap(getContext(), child);
         selectLayout(false /* animate */, true /* force */);
         updateRoundRectClipping();
@@ -357,8 +366,17 @@
     }
 
     private void updateClipping() {
-        mClipBounds.set(0, mClipTopAmount, getWidth(), mContentHeight);
-        setClipBounds(mClipBounds);
+        if (mClipToActualHeight) {
+            mClipBounds.set(0, mClipTopAmount, getWidth(), mContentHeight);
+            setClipBounds(mClipBounds);
+        } else {
+            setClipBounds(null);
+        }
+    }
+
+    public void setClipToActualHeight(boolean clipToActualHeight) {
+        mClipToActualHeight = clipToActualHeight;
+        updateClipping();
     }
 
     private void selectLayout(boolean animate, boolean force) {
@@ -371,7 +389,7 @@
                     || (visibleType == VISIBLE_TYPE_HEADSUP && mHeadsUpChild != null)
                     || (visibleType == VISIBLE_TYPE_SINGLELINE && mSingleLineView != null)
                     || visibleType == VISIBLE_TYPE_CONTRACTED)) {
-                runSwitchAnimation(visibleType);
+                animateToVisibleType(visibleType);
             } else {
                 updateViewVisibilities(visibleType);
             }
@@ -381,59 +399,55 @@
 
     private void updateViewVisibilities(int visibleType) {
         boolean contractedVisible = visibleType == VISIBLE_TYPE_CONTRACTED;
-        mContractedChild.setVisibility(contractedVisible ? View.VISIBLE : View.INVISIBLE);
-        mContractedChild.setAlpha(contractedVisible ? 1f : 0f);
-        mContractedChild.setLayerType(LAYER_TYPE_NONE, null);
+        mContractedWrapper.setVisible(contractedVisible);
         if (mExpandedChild != null) {
             boolean expandedVisible = visibleType == VISIBLE_TYPE_EXPANDED;
-            mExpandedChild.setVisibility(expandedVisible ? View.VISIBLE : View.INVISIBLE);
-            mExpandedChild.setAlpha(expandedVisible ? 1f : 0f);
-            mExpandedChild.setLayerType(LAYER_TYPE_NONE, null);
+            mExpandedWrapper.setVisible(expandedVisible);
         }
         if (mHeadsUpChild != null) {
             boolean headsUpVisible = visibleType == VISIBLE_TYPE_HEADSUP;
-            mHeadsUpChild.setVisibility(headsUpVisible ? View.VISIBLE : View.INVISIBLE);
-            mHeadsUpChild.setAlpha(headsUpVisible ? 1f : 0f);
-            mHeadsUpChild.setLayerType(LAYER_TYPE_NONE, null);
+            mHeadsUpWrapper.setVisible(headsUpVisible);
         }
         if (mSingleLineView != null) {
             boolean singleLineVisible = visibleType == VISIBLE_TYPE_SINGLELINE;
-            mSingleLineView.setVisibility(singleLineVisible ? View.VISIBLE : View.INVISIBLE);
-            mSingleLineView.setAlpha(singleLineVisible ? 1f : 0f);
-            mSingleLineView.setLayerType(LAYER_TYPE_NONE, null);
+            mSingleLineView.setVisible(singleLineVisible);
         }
-        setLayerType(LAYER_TYPE_NONE, null);
         updateRoundRectClipping();
     }
 
-    private void runSwitchAnimation(int visibleType) {
-        View shownView = getViewForVisibleType(visibleType);
-        View hiddenView = getViewForVisibleType(mVisibleType);
-        shownView.setVisibility(View.VISIBLE);
-        hiddenView.setVisibility(View.VISIBLE);
-        shownView.setLayerType(LAYER_TYPE_HARDWARE, mFadePaint);
-        hiddenView.setLayerType(LAYER_TYPE_HARDWARE, mFadePaint);
-        setLayerType(LAYER_TYPE_HARDWARE, null);
-        hiddenView.animate()
-                .alpha(0f)
-                .setDuration(ANIMATION_DURATION_LENGTH)
-                .setInterpolator(mLinearInterpolator)
-                .withEndAction(null); // In case we have multiple changes in one frame.
-        shownView.animate()
-                .alpha(1f)
-                .setDuration(ANIMATION_DURATION_LENGTH)
-                .setInterpolator(mLinearInterpolator)
-                .withEndAction(new Runnable() {
-                    @Override
-                    public void run() {
-                        updateViewVisibilities(mVisibleType);
-                    }
-                });
+    private void animateToVisibleType(int visibleType) {
+        final TransformableView shownView = getTransformableViewForVisibleType(visibleType);
+        final TransformableView hiddenView = getTransformableViewForVisibleType(mVisibleType);
+        shownView.transformFrom(hiddenView);
+        getViewForVisibleType(visibleType).setVisibility(View.VISIBLE);
+        hiddenView.transformTo(shownView, new Runnable() {
+            @Override
+            public void run() {
+                hiddenView.setVisible(false);
+            }
+        });
         updateRoundRectClipping();
     }
 
     /**
      * @param visibleType one of the static enum types in this view
+     * @return the corresponding transformable view according to the given visible type
+     */
+    private TransformableView getTransformableViewForVisibleType(int visibleType) {
+        switch (visibleType) {
+            case VISIBLE_TYPE_EXPANDED:
+                return mExpandedWrapper;
+            case VISIBLE_TYPE_HEADSUP:
+                return mHeadsUpWrapper;
+            case VISIBLE_TYPE_SINGLELINE:
+                return mSingleLineView;
+            default:
+                return mContractedWrapper;
+        }
+    }
+
+    /**
+     * @param visibleType one of the static enum types in this view
      * @return the corresponding view according to the given visible type
      */
     private View getViewForVisibleType(int visibleType) {
@@ -455,7 +469,8 @@
     private int calculateVisibleType() {
         boolean noExpandedChild = mExpandedChild == null;
 
-        if (!noExpandedChild && mContentHeight == mExpandedChild.getHeight()) {
+        int viewHeight = Math.min(mContentHeight, mContainingNotification.getIntrinsicHeight());
+        if (!noExpandedChild && viewHeight == mExpandedChild.getHeight()) {
             return VISIBLE_TYPE_EXPANDED;
         }
         if (mIsChildInGroup && !isGroupExpanded()) {
@@ -463,13 +478,13 @@
         }
 
         if (mIsHeadsUp && mHeadsUpChild != null) {
-            if (mContentHeight <= mHeadsUpChild.getHeight() || noExpandedChild) {
+            if (viewHeight <= mHeadsUpChild.getHeight() || noExpandedChild) {
                 return VISIBLE_TYPE_HEADSUP;
             } else {
                 return VISIBLE_TYPE_EXPANDED;
             }
         } else {
-            if (mContentHeight <= mContractedChild.getHeight() || noExpandedChild) {
+            if (viewHeight <= mContractedChild.getHeight() || noExpandedChild) {
                 return VISIBLE_TYPE_CONTRACTED;
             } else {
                 return VISIBLE_TYPE_EXPANDED;
@@ -484,8 +499,17 @@
     public void setDark(boolean dark, boolean fade, long delay) {
         if (mDark == dark || mContractedChild == null) return;
         mDark = dark;
-        mContractedWrapper.setDark(dark && !mShowingLegacyBackground, fade, delay);
-        if (mSingleLineView != null) {
+        dark = dark && !mShowingLegacyBackground;
+        if (mVisibleType == VISIBLE_TYPE_CONTRACTED) {
+            mContractedWrapper.setDark(dark, fade, delay);
+        }
+        if (mVisibleType == VISIBLE_TYPE_EXPANDED) {
+            mExpandedWrapper.setDark(dark, fade, delay);
+        }
+        if (mVisibleType == VISIBLE_TYPE_HEADSUP) {
+            mHeadsUpWrapper.setDark(dark, fade, delay);
+        }
+        if (mSingleLineView != null && mVisibleType == VISIBLE_TYPE_SINGLELINE) {
             mSingleLineView.setDark(dark, fade, delay);
         }
     }
@@ -520,14 +544,14 @@
         applyRemoteInput(entry);
         selectLayout(false /* animate */, true /* force */);
         if (mContractedChild != null) {
-            mContractedWrapper.notifyContentUpdated();
+            mContractedWrapper.notifyContentUpdated(entry.notification);
             mContractedWrapper.setDark(mDark, false /* animate */, 0 /* delay */);
         }
         if (mExpandedChild != null) {
-            mExpandedWrapper.notifyContentUpdated();
+            mExpandedWrapper.notifyContentUpdated(entry.notification);
         }
         if (mHeadsUpChild != null) {
-            mHeadsUpWrapper.notifyContentUpdated();
+            mHeadsUpWrapper.notifyContentUpdated(entry.notification);
         }
         updateRoundRectClipping();
     }
@@ -644,4 +668,12 @@
         }
         return header;
     }
+
+    public void setContainingNotification(ExpandableNotificationRow containingNotification) {
+        mContainingNotification = containingNotification;
+    }
+
+    public void requestSelectLayout(boolean needsAnimation) {
+        selectLayout(needsAnimation, false);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationHeaderUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationHeaderUtil.java
index 859a330..98a37f9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationHeaderUtil.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationHeaderUtil.java
@@ -110,6 +110,25 @@
                 sIconExtractor,
                 sGreyComparator,
                 mGreyApplicator));
+        mComparators.add(new HeaderProcessor(mRow,
+                com.android.internal.R.id.profile_badge,
+                null /* Extractor */,
+                new ViewComparator() {
+                    @Override
+                    public boolean compare(View parent, View child, Object parentData,
+                            Object childData) {
+                        return parent.getVisibility() == View.VISIBLE;
+                    }
+
+                    @Override
+                    public boolean isEmpty(View view) {
+                        if (view instanceof ImageView) {
+                            return ((ImageView) view).getDrawable() == null;
+                        }
+                        return false;
+                    }
+                },
+                sVisibilityApplicator));
         mComparators.add(HeaderProcessor.forTextView(mRow,
                 com.android.internal.R.id.app_name_text));
         mComparators.add(HeaderProcessor.forTextView(mRow,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationTemplateViewWrapper.java
deleted file mode 100644
index 77e8c55..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationTemplateViewWrapper.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.statusbar;
-
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.graphics.Color;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.ProgressBar;
-
-/**
- * Wraps a notification view inflated from a template.
- */
-public class NotificationTemplateViewWrapper extends NotificationHeaderViewWrapper {
-
-    private static final int mDarkProgressTint = 0xffffffff;
-
-    protected ImageView mPicture;
-    private ProgressBar mProgressBar;
-
-    protected NotificationTemplateViewWrapper(Context ctx, View view) {
-        super(ctx, view);
-        resolveTemplateViews();
-    }
-
-    private void resolveTemplateViews() {
-        View mainColumn = mView.findViewById(com.android.internal.R.id.notification_main_column);
-        mPicture = (ImageView) mView.findViewById(com.android.internal.R.id.right_icon);
-        final View progress = mView.findViewById(com.android.internal.R.id.progress);
-        if (progress instanceof ProgressBar) {
-            mProgressBar = (ProgressBar) progress;
-        } else {
-            // It's still a viewstub
-            mProgressBar = null;
-        }
-        if (mainColumn != null) {
-            mInvertHelper.addTarget(mainColumn);
-        }
-    }
-
-    @Override
-    public void notifyContentUpdated() {
-        super.notifyContentUpdated();
-
-        // Reinspect the notification.
-        resolveTemplateViews();
-    }
-
-    @Override
-    public void setDark(boolean dark, boolean fade, long delay) {
-        super.setDark(dark, fade, delay);
-        setPictureGrayscale(dark, fade, delay);
-        setProgressBarDark(dark, fade, delay);
-    }
-
-    private void setProgressBarDark(boolean dark, boolean fade, long delay) {
-        if (mProgressBar != null) {
-            if (fade) {
-                fadeProgressDark(mProgressBar, dark, delay);
-            } else {
-                updateProgressDark(mProgressBar, dark);
-            }
-        }
-    }
-
-    private void fadeProgressDark(final ProgressBar target, final boolean dark, long delay) {
-        startIntensityAnimation(new ValueAnimator.AnimatorUpdateListener() {
-            @Override
-            public void onAnimationUpdate(ValueAnimator animation) {
-                float t = (float) animation.getAnimatedValue();
-                updateProgressDark(target, t);
-            }
-        }, dark, delay, null /* listener */);
-    }
-
-    private void updateProgressDark(ProgressBar target, float intensity) {
-        int color = interpolateColor(mColor, mDarkProgressTint, intensity);
-        target.getIndeterminateDrawable().mutate().setTint(color);
-        target.getProgressDrawable().mutate().setTint(color);
-    }
-
-    private void updateProgressDark(ProgressBar target, boolean dark) {
-        updateProgressDark(target, dark ? 1f : 0f);
-    }
-
-    protected void setPictureGrayscale(boolean grayscale, boolean fade, long delay) {
-        if (mPicture != null) {
-            if (fade) {
-                fadeGrayscale(mPicture, grayscale, delay);
-            } else {
-                updateGrayscale(mPicture, grayscale);
-            }
-        }
-    }
-
-    private static int interpolateColor(int source, int target, float t) {
-        int aSource = Color.alpha(source);
-        int rSource = Color.red(source);
-        int gSource = Color.green(source);
-        int bSource = Color.blue(source);
-        int aTarget = Color.alpha(target);
-        int rTarget = Color.red(target);
-        int gTarget = Color.green(target);
-        int bTarget = Color.blue(target);
-        return Color.argb(
-                (int) (aSource * (1f - t) + aTarget * t),
-                (int) (rSource * (1f - t) + rTarget * t),
-                (int) (gSource * (1f - t) + gTarget * t),
-                (int) (bSource * (1f - t) + bTarget * t));
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ScalingDrawableWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/ScalingDrawableWrapper.java
new file mode 100644
index 0000000..24277e6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ScalingDrawableWrapper.java
@@ -0,0 +1,43 @@
+/*
+ * 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.statusbar;
+
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.DrawableWrapper;
+
+/**
+ * An extension of {@link DrawableWrapper} that will take a given Drawable and scale it by
+ * the given factor.
+ */
+class ScalingDrawableWrapper extends DrawableWrapper {
+    private float mScaleFactor;
+
+    public ScalingDrawableWrapper(Drawable drawable, float scaleFactor) {
+        super(drawable);
+        mScaleFactor = scaleFactor;
+    }
+
+    @Override
+    public int getIntrinsicWidth() {
+        return (int) (super.getIntrinsicWidth() * mScaleFactor);
+    }
+
+    @Override
+    public int getIntrinsicHeight() {
+        return (int) (super.getIntrinsicHeight() * mScaleFactor);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java
index 68e483c..6801e5f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java
@@ -16,16 +16,18 @@
 
 package com.android.systemui.statusbar;
 
+import android.annotation.DrawableRes;
 import android.content.Context;
 import android.content.res.ColorStateList;
+import android.content.res.Resources;
 import android.graphics.Color;
-import android.graphics.PorterDuff;
 import android.graphics.drawable.Animatable;
 import android.graphics.drawable.Drawable;
 import android.telephony.SubscriptionInfo;
 import android.util.ArraySet;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.util.TypedValue;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -86,10 +88,11 @@
     View mWifiSignalSpacer;
     LinearLayout mMobileSignalGroup;
 
-    private int mWideTypeIconStartPadding;
-    private int mSecondaryTelephonyPadding;
-    private int mEndPadding;
-    private int mEndPaddingNothingVisible;
+    private final int mWideTypeIconStartPadding;
+    private final int mSecondaryTelephonyPadding;
+    private final int mEndPadding;
+    private final int mEndPaddingNothingVisible;
+    private final float mIconScaleFactor;
 
     private boolean mBlockAirplane;
     private boolean mBlockMobile;
@@ -106,6 +109,17 @@
 
     public SignalClusterView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
+
+        Resources res = getResources();
+        mWideTypeIconStartPadding = res.getDimensionPixelSize(R.dimen.wide_type_icon_start_padding);
+        mSecondaryTelephonyPadding = res.getDimensionPixelSize(R.dimen.secondary_telephony_padding);
+        mEndPadding = res.getDimensionPixelSize(R.dimen.signal_cluster_battery_padding);
+        mEndPaddingNothingVisible = res.getDimensionPixelSize(
+                R.dimen.no_signal_cluster_battery_padding);
+
+        TypedValue typedValue = new TypedValue();
+        res.getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true);
+        mIconScaleFactor = typedValue.getFloat();
     }
 
     @Override
@@ -146,19 +160,6 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
-        mWideTypeIconStartPadding = getContext().getResources().getDimensionPixelSize(
-                R.dimen.wide_type_icon_start_padding);
-        mSecondaryTelephonyPadding = getContext().getResources().getDimensionPixelSize(
-                R.dimen.secondary_telephony_padding);
-        mEndPadding = getContext().getResources().getDimensionPixelSize(
-                R.dimen.signal_cluster_battery_padding);
-        mEndPaddingNothingVisible = getContext().getResources().getDimensionPixelSize(
-                R.dimen.no_signal_cluster_battery_padding);
-    }
-
-    @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
 
         mVpn            = (ImageView) findViewById(R.id.vpn);
         mEthernetGroup  = (ViewGroup) findViewById(R.id.ethernet_combo);
@@ -174,6 +175,32 @@
         mWifiAirplaneSpacer =         findViewById(R.id.wifi_airplane_spacer);
         mWifiSignalSpacer =           findViewById(R.id.wifi_signal_spacer);
         mMobileSignalGroup = (LinearLayout) findViewById(R.id.mobile_signal_group);
+
+        maybeScaleVpnAndNoSimsIcons();
+    }
+
+    /**
+     * Extracts the icon off of the VPN and no sims views and maybe scale them by
+     * {@link #mIconScaleFactor}. Note that the other icons are not scaled here because they are
+     * dynamic. As such, they need to be scaled each time the icon changes in {@link #apply()}.
+     */
+    private void maybeScaleVpnAndNoSimsIcons() {
+        if (mIconScaleFactor == 1.f) {
+            return;
+        }
+
+        mVpn.setImageDrawable(new ScalingDrawableWrapper(mVpn.getDrawable(), mIconScaleFactor));
+
+        mNoSims.setImageDrawable(
+                new ScalingDrawableWrapper(mNoSims.getDrawable(), mIconScaleFactor));
+        mNoSimsDark.setImageDrawable(
+                new ScalingDrawableWrapper(mNoSimsDark.getDrawable(), mIconScaleFactor));
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
         for (PhoneState state : mPhoneStates) {
             mMobileSignalGroup.addView(state.mMobileGroup);
         }
@@ -185,14 +212,7 @@
 
     @Override
     protected void onDetachedFromWindow() {
-        mVpn            = null;
-        mEthernetGroup  = null;
-        mEthernet       = null;
-        mWifiGroup      = null;
-        mWifi           = null;
-        mAirplane       = null;
         mMobileSignalGroup.removeAllViews();
-        mMobileSignalGroup = null;
         TunerService.get(mContext).removeTunable(this);
 
         super.onDetachedFromWindow();
@@ -383,8 +403,8 @@
 
         if (mEthernetVisible) {
             if (mLastEthernetIconId != mEthernetIconId) {
-                mEthernet.setImageResource(mEthernetIconId);
-                mEthernetDark.setImageResource(mEthernetIconId);
+                setIconForView(mEthernet, mEthernetIconId);
+                setIconForView(mEthernetDark, mEthernetIconId);
                 mLastEthernetIconId = mEthernetIconId;
             }
             mEthernetGroup.setContentDescription(mEthernetDescription);
@@ -397,11 +417,10 @@
                 String.format("ethernet: %s",
                     (mEthernetVisible ? "VISIBLE" : "GONE")));
 
-
         if (mWifiVisible) {
             if (mWifiStrengthId != mLastWifiStrengthId) {
-                mWifi.setImageResource(mWifiStrengthId);
-                mWifiDark.setImageResource(mWifiStrengthId);
+                setIconForView(mWifi, mWifiStrengthId);
+                setIconForView(mWifiDark, mWifiStrengthId);
                 mLastWifiStrengthId = mWifiStrengthId;
             }
             mWifiGroup.setContentDescription(mWifiDescription);
@@ -428,7 +447,7 @@
 
         if (mIsAirplaneMode) {
             if (mLastAirplaneIconId != mAirplaneIconId) {
-                mAirplane.setImageResource(mAirplaneIconId);
+                setIconForView(mAirplane, mAirplaneIconId);
                 mLastAirplaneIconId = mAirplaneIconId;
             }
             mAirplane.setContentDescription(mAirplaneContentDescription);
@@ -456,6 +475,21 @@
         setPaddingRelative(0, 0, anythingVisible ? mEndPadding : mEndPaddingNothingVisible, 0);
     }
 
+    /**
+     * Sets the given drawable id on the view. This method will also scale the icon by
+     * {@link #mIconScaleFactor} if appropriate.
+     */
+    private void setIconForView(ImageView imageView, @DrawableRes int iconId) {
+        // Using the imageView's context to retrieve the Drawable so that theme is preserved.
+        Drawable icon = imageView.getContext().getDrawable(iconId);
+
+        if (mIconScaleFactor == 1.f) {
+            imageView.setImageDrawable(icon);
+        } else {
+            imageView.setImageDrawable(new ScalingDrawableWrapper(icon, mIconScaleFactor));
+        }
+    }
+
     public void setIconTint(int tint, float darkIntensity) {
         boolean changed = tint != mIconTint || darkIntensity != mDarkIntensity;
         mIconTint = tint;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
index de7a8db..5a7cf86 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
@@ -24,10 +24,13 @@
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
+import android.graphics.drawable.ScaleDrawable;
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.util.TypedValue;
+import android.view.Gravity;
 import android.view.ViewDebug;
 import android.view.accessibility.AccessibilityEvent;
 import com.android.internal.statusbar.StatusBarIcon;
@@ -189,12 +192,24 @@
      * @return Drawable for this item, or null if the package or item could not
      *         be found
      */
-    public static Drawable getIcon(Context context, StatusBarIcon icon) {
-        int userId = icon.user.getIdentifier();
+    public static Drawable getIcon(Context context, StatusBarIcon statusBarIcon) {
+        int userId = statusBarIcon.user.getIdentifier();
         if (userId == UserHandle.USER_ALL) {
             userId = UserHandle.USER_SYSTEM;
         }
-        return icon.icon.loadDrawableAsUser(context, userId);
+
+        Drawable icon = statusBarIcon.icon.loadDrawableAsUser(context, userId);
+
+        TypedValue typedValue = new TypedValue();
+        context.getResources().getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true);
+        float scaleFactor = typedValue.getFloat();
+
+        // No need to scale the icon, so return it as is.
+        if (scaleFactor == 1.f) {
+            return icon;
+        }
+
+        return new ScalingDrawableWrapper(icon, scaleFactor);
     }
 
     public StatusBarIcon getStatusBarIcon() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/TransformableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/TransformableView.java
new file mode 100644
index 0000000..38b6497
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/TransformableView.java
@@ -0,0 +1,55 @@
+/*
+ * 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.statusbar;
+
+import com.android.systemui.statusbar.notification.TransformState;
+
+/**
+ * A view that can be transformed to and from.
+ */
+public interface TransformableView {
+    int TRANSFORMING_VIEW_HEADER = 0;
+    int TRANSFORMING_VIEW_TITLE = 1;
+    int TRANSFORMING_VIEW_TEXT = 2;
+    int TRANSFORMING_VIEW_IMAGE = 3;
+    int TRANSFORMING_VIEW_PROGRESS = 4;
+
+    /**
+     * Get the current state of a view in a transform animation
+     * @param fadingView which view we are interested in
+     * @return the current transform state of this viewtype
+     */
+    TransformState getCurrentState(int fadingView);
+
+    /**
+     * Transform to the given view
+     * @param notification the view to transform to
+     */
+    void transformTo(TransformableView notification, Runnable endRunnable);
+
+    /**
+     * Transform to this view from the given view
+     * @param notification the view to transform from
+     */
+    void transformFrom(TransformableView notification);
+
+    /**
+     * Set this view to be fully visible or gone
+     * @param visible
+     */
+    void setVisible(boolean visible);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ViewTransformationHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/ViewTransformationHelper.java
new file mode 100644
index 0000000..63ff5aa
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ViewTransformationHelper.java
@@ -0,0 +1,194 @@
+/*
+ * 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.statusbar;
+
+import android.os.Handler;
+import android.util.ArrayMap;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.notification.TransformState;
+
+import java.util.Stack;
+
+/**
+ * A view that can be transformed to and from.
+ */
+public class ViewTransformationHelper implements TransformableView {
+
+    private static final int TAG_CONTAINS_TRANSFORMED_VIEW = R.id.contains_transformed_view;
+
+    private final Handler mHandler = new Handler();
+    private ArrayMap<Integer, View> mTransformedViews = new ArrayMap<>();
+    private ArrayMap<Integer, CustomTransformation> mCustomTransformations = new ArrayMap<>();
+
+    public void addTransformedView(int key, View transformedView) {
+        mTransformedViews.put(key, transformedView);
+    }
+
+    public void reset() {
+        mTransformedViews.clear();
+    }
+
+    public void setCustomTransformation(CustomTransformation transformation, int viewType) {
+        mCustomTransformations.put(viewType, transformation);
+    }
+
+    @Override
+    public TransformState getCurrentState(int fadingView) {
+        View view = mTransformedViews.get(fadingView);
+        if (view != null && view.getVisibility() != View.GONE) {
+            return TransformState.createFrom(view);
+        }
+        return null;
+    }
+
+    @Override
+    public void transformTo(TransformableView notification, Runnable endRunnable) {
+        Runnable runnable = endRunnable;
+        for (Integer viewType : mTransformedViews.keySet()) {
+            TransformState ownState = getCurrentState(viewType);
+            if (ownState != null) {
+                CustomTransformation customTransformation = mCustomTransformations.get(viewType);
+                if (customTransformation != null && customTransformation.transformTo(
+                        ownState, notification, runnable)) {
+                    ownState.recycle();
+                    runnable = null;
+                    continue;
+                }
+                TransformState otherState = notification.getCurrentState(viewType);
+                if (otherState != null) {
+                    boolean run = ownState.transformViewTo(otherState, runnable);
+                    otherState.recycle();
+                    if (run) {
+                        runnable = null;
+                    }
+                } else {
+                    // there's no other view available
+                    CrossFadeHelper.fadeOut(mTransformedViews.get(viewType), runnable);
+                    runnable = null;
+                }
+                ownState.recycle();
+            }
+        }
+        if (runnable != null) {
+            // We need to post, since the visible type is only set after the transformation is
+            // started
+            mHandler.post(runnable);
+        }
+    }
+
+    @Override
+    public void transformFrom(TransformableView notification) {
+        for (Integer viewType : mTransformedViews.keySet()) {
+            TransformState ownState = getCurrentState(viewType);
+            if (ownState != null) {
+                CustomTransformation customTransformation = mCustomTransformations.get(viewType);
+                if (customTransformation != null && customTransformation.transformFrom(
+                        ownState, notification)) {
+                    ownState.recycle();
+                    continue;
+                }
+                TransformState otherState = notification.getCurrentState(viewType);
+                if (otherState != null) {
+                    ownState.transformViewFrom(otherState);
+                    otherState.recycle();
+                } else {
+                    // There's no other view, lets fade us in
+                    // Certain views need to prepare the fade in and make sure its children are
+                    // completely visible. An example is the notification header.
+                    ownState.prepareFadeIn();
+                    CrossFadeHelper.fadeIn(mTransformedViews.get(viewType));
+                }
+                ownState.recycle();
+            }
+        }
+    }
+
+    @Override
+    public void setVisible(boolean visible) {
+        for (Integer viewType : mTransformedViews.keySet()) {
+            TransformState ownState = getCurrentState(viewType);
+            if (ownState != null) {
+                ownState.setVisible(visible);
+                ownState.recycle();
+            }
+        }
+    }
+
+    /**
+     * Add the remaining transformation views such that all views are being transformed correctly
+     * @param viewRoot the root below which all elements need to be transformed
+     */
+    public void addRemainingTransformTypes(View viewRoot) {
+        // lets now tag the right views
+        int numValues = mTransformedViews.size();
+        for (int i = 0; i < numValues; i++) {
+            View view = mTransformedViews.valueAt(i);
+            while (view != viewRoot.getParent()) {
+                view.setTag(TAG_CONTAINS_TRANSFORMED_VIEW, true);
+                view = (View) view.getParent();
+            }
+        }
+        Stack<View> stack = new Stack<>();
+        // Add the right views now
+        stack.push(viewRoot);
+        while (!stack.isEmpty()) {
+            View child = stack.pop();
+            if (child.getVisibility() == View.GONE) {
+                continue;
+            }
+            Boolean containsView = (Boolean) child.getTag(TAG_CONTAINS_TRANSFORMED_VIEW);
+            if (containsView == null) {
+                // This one is unhandled, let's add it to our list.
+                int id = child.getId();
+                if (id != View.NO_ID) {
+                    // We only fade views with an id
+                    addTransformedView(id, child);
+                    continue;
+                }
+            }
+            child.setTag(TAG_CONTAINS_TRANSFORMED_VIEW, null);
+            if (child instanceof ViewGroup && !mTransformedViews.containsValue(child)){
+                ViewGroup group = (ViewGroup) child;
+                for (int i = 0; i < group.getChildCount(); i++) {
+                    stack.push(group.getChildAt(i));
+                }
+            }
+        }
+    }
+
+    public interface CustomTransformation {
+        /**
+         * Transform a state to the given view
+         * @param ownState the state to transform
+         * @param notification the view to transform to
+         * @return whether a custom transformation is performed
+         */
+        boolean transformTo(TransformState ownState, TransformableView notification,
+                Runnable endRunnable);
+
+        /**
+         * Transform to this state from the given view
+         * @param ownState the state to transform to
+         * @param notification the view to transform from
+         * @return whether a custom transformation is performed
+         */
+        boolean transformFrom(TransformState ownState, TransformableView notification);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeaderTransformState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeaderTransformState.java
new file mode 100644
index 0000000..bf291d3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeaderTransformState.java
@@ -0,0 +1,149 @@
+/*
+ * 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.statusbar.notification;
+
+import android.util.Pools;
+import android.view.NotificationHeaderView;
+import android.view.View;
+
+import com.android.systemui.statusbar.CrossFadeHelper;
+
+/**
+ * A transform state of a text view.
+*/
+public class HeaderTransformState extends TransformState {
+
+    private static Pools.SimplePool<HeaderTransformState> sInstancePool
+            = new Pools.SimplePool<>(40);
+    private View mExpandButton;
+
+    @Override
+    public void initFrom(View view) {
+        super.initFrom(view);
+        if (view instanceof NotificationHeaderView) {
+            NotificationHeaderView header = (NotificationHeaderView) view;
+            mExpandButton = header.getExpandButton();
+        }
+    }
+
+    @Override
+    public boolean transformViewTo(TransformState otherState, Runnable endRunnable) {
+        // if the transforming notification has a header, we have ensured that it looks the same
+        // but the expand button, so lets fade just that one.
+        if (!(mTransformedView instanceof NotificationHeaderView)) {
+            return false;
+        }
+        NotificationHeaderView header = (NotificationHeaderView) mTransformedView;
+        int childCount = header.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View headerChild = header.getChildAt(i);
+            if (headerChild.getVisibility() == View.GONE) {
+                continue;
+            }
+            if (headerChild != mExpandButton) {
+                headerChild.setVisibility(View.INVISIBLE);
+            } else {
+                CrossFadeHelper.fadeOut(mExpandButton, endRunnable);
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public void transformViewFrom(TransformState otherState) {
+        // if the transforming notification has a header, we have ensured that it looks the same
+        // but the expand button, so lets fade just that one.
+        if (!(mTransformedView instanceof NotificationHeaderView)) {
+            return;
+        }
+        NotificationHeaderView header = (NotificationHeaderView) mTransformedView;
+        header.setVisibility(View.VISIBLE);
+        header.setAlpha(1.0f);
+        int childCount = header.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View headerChild = header.getChildAt(i);
+            if (headerChild.getVisibility() == View.GONE) {
+                continue;
+            }
+            if (headerChild != mExpandButton) {
+                headerChild.setVisibility(View.VISIBLE);
+            } else {
+                CrossFadeHelper.fadeIn(mExpandButton);
+            }
+        }
+        return;
+    }
+
+    public static HeaderTransformState obtain() {
+        HeaderTransformState instance = sInstancePool.acquire();
+        if (instance != null) {
+            return instance;
+        }
+        return new HeaderTransformState();
+    }
+
+    @Override
+    public void recycle() {
+        super.recycle();
+        sInstancePool.release(this);
+    }
+
+    @Override
+    protected void reset() {
+        super.reset();
+        mExpandButton = null;
+    }
+
+    public void setVisible(boolean visible) {
+        super.setVisible(visible);
+        if (!(mTransformedView instanceof NotificationHeaderView)) {
+            return;
+        }
+        NotificationHeaderView header = (NotificationHeaderView) mTransformedView;
+        int childCount = header.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View headerChild = header.getChildAt(i);
+            if (headerChild.getVisibility() == View.GONE) {
+                continue;
+            }
+            headerChild.animate().cancel();
+            headerChild.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+            if (headerChild == mExpandButton) {
+                headerChild.setAlpha(visible ? 1.0f : 0.0f);
+            }
+        }
+    }
+
+    @Override
+    public void prepareFadeIn() {
+        super.prepareFadeIn();
+        if (!(mTransformedView instanceof NotificationHeaderView)) {
+            return;
+        }
+        NotificationHeaderView header = (NotificationHeaderView) mTransformedView;
+        int childCount = header.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View headerChild = header.getChildAt(i);
+            if (headerChild.getVisibility() == View.GONE) {
+                continue;
+            }
+            headerChild.animate().cancel();
+            headerChild.setVisibility(View.VISIBLE);
+            headerChild.setAlpha(1.0f);
+        }
+    }
+}
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 5fb6fec..5eed5ed 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HybridNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HybridNotificationView.java
@@ -18,18 +18,29 @@
 
 import android.annotation.Nullable;
 import android.content.Context;
+import android.text.TextUtils;
+import android.util.ArrayMap;
 import android.util.AttributeSet;
+import android.view.View;
 import android.widget.TextView;
 
 import com.android.keyguard.AlphaOptimizedLinearLayout;
 import com.android.systemui.R;
 import com.android.systemui.ViewInvertHelper;
+import com.android.systemui.statusbar.CrossFadeHelper;
+import com.android.systemui.statusbar.TransformableView;
+import com.android.systemui.statusbar.ViewTransformationHelper;
 import com.android.systemui.statusbar.phone.NotificationPanelView;
 
+import java.util.ArrayList;
+
 /**
  * A hybrid view which may contain information about one ore more notifications.
  */
-public class HybridNotificationView extends AlphaOptimizedLinearLayout {
+public class HybridNotificationView extends AlphaOptimizedLinearLayout
+        implements TransformableView {
+
+    private ViewTransformationHelper mTransformationHelper;
 
     protected TextView mTitleView;
     protected TextView mTextView;
@@ -58,6 +69,39 @@
         mTitleView = (TextView) findViewById(R.id.notification_title);
         mTextView = (TextView) findViewById(R.id.notification_text);
         mInvertHelper = new ViewInvertHelper(this, NotificationPanelView.DOZE_ANIMATION_DURATION);
+        mTransformationHelper = new ViewTransformationHelper();
+        mTransformationHelper.setCustomTransformation(
+                new ViewTransformationHelper.CustomTransformation() {
+                    @Override
+                    public boolean transformTo(TransformState ownState, TransformableView notification,
+                            Runnable endRunnable) {
+                        // We want to transform to the same y location as the title
+                        TransformState otherState = notification.getCurrentState(
+                                TRANSFORMING_VIEW_TITLE);
+                        CrossFadeHelper.fadeOut(mTextView, endRunnable);
+                        if (otherState != null) {
+                            ownState.animateViewVerticalTo(otherState, endRunnable);
+                            otherState.recycle();
+                        }
+                        return true;
+                    }
+
+                    @Override
+                    public boolean transformFrom(TransformState ownState,
+                            TransformableView notification) {
+                        // We want to transform from the same y location as the title
+                        TransformState otherState = notification.getCurrentState(
+                                TRANSFORMING_VIEW_TITLE);
+                        CrossFadeHelper.fadeIn(mTextView);
+                        if (otherState != null) {
+                            ownState.animateViewVerticalFrom(otherState);
+                            otherState.recycle();
+                        }
+                        return true;
+                    }
+                }, TRANSFORMING_VIEW_TEXT);
+        mTransformationHelper.addTransformedView(TRANSFORMING_VIEW_TITLE, mTitleView);
+        mTransformationHelper.addTransformedView(TRANSFORMING_VIEW_TEXT, mTextView);
     }
 
     public void bind(CharSequence title) {
@@ -66,11 +110,38 @@
 
     public void bind(CharSequence title, CharSequence text) {
         mTitleView.setText(title);
+        if (TextUtils.isEmpty(title)) {
+            mTitleView.setVisibility(GONE);
+        }
         mTextView.setText(text);
+        if (TextUtils.isEmpty(text)) {
+            mTextView.setVisibility(GONE);
+        }
         requestLayout();
     }
 
     public void setDark(boolean dark, boolean fade, long delay) {
         mInvertHelper.setInverted(dark, fade, delay);
     }
+
+    @Override
+    public TransformState getCurrentState(int fadingView) {
+        return mTransformationHelper.getCurrentState(fadingView);
+    }
+
+    @Override
+    public void transformTo(TransformableView notification, Runnable endRunnable) {
+        mTransformationHelper.transformTo(notification, endRunnable);
+    }
+
+    @Override
+    public void transformFrom(TransformableView notification) {
+        mTransformationHelper.transformFrom(notification);
+    }
+
+    @Override
+    public void setVisible(boolean visible) {
+        setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+        mTransformationHelper.setVisible(visible);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HybridNotificationViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HybridNotificationViewManager.java
index b8adf5b..285d53f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HybridNotificationViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HybridNotificationViewManager.java
@@ -52,16 +52,10 @@
 
     public HybridNotificationView bindFromNotification(HybridNotificationView reusableView,
             Notification notification) {
-        CharSequence titleText = resolveTitle(notification);
-        if (titleText == null) {
-            if (reusableView != null) {
-                mParent.removeView(reusableView);
-            }
-            return null;
-        }
         if (reusableView == null) {
             reusableView = inflateHybridView();
         }
+        CharSequence titleText = resolveTitle(notification);
         CharSequence contentText = resolveText(notification);
         reusableView.bind(titleText, contentText);
         return reusableView;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ImageTransformState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ImageTransformState.java
new file mode 100644
index 0000000..e891a97
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ImageTransformState.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.statusbar.notification;
+
+import android.graphics.drawable.Icon;
+import android.util.Pools;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.android.systemui.R;
+
+/**
+ * A transform state of a image view.
+*/
+public class ImageTransformState extends TransformState {
+
+    public static final int ICON_TAG = R.id.image_icon_tag;
+    private static Pools.SimplePool<ImageTransformState> sInstancePool
+            = new Pools.SimplePool<>(40);
+    private Icon mIcon;
+
+    @Override
+    public void initFrom(View view) {
+        super.initFrom(view);
+        if (view instanceof ImageView) {
+            mIcon = (Icon) view.getTag(ICON_TAG);
+        }
+    }
+
+    @Override
+    protected boolean sameAs(TransformState otherState) {
+        if (otherState instanceof ImageTransformState) {
+            return mIcon != null && mIcon.sameAs(((ImageTransformState) otherState).getIcon());
+        }
+        return super.sameAs(otherState);
+    }
+
+    public Icon getIcon() {
+        return mIcon;
+    }
+
+    public static ImageTransformState obtain() {
+        ImageTransformState instance = sInstancePool.acquire();
+        if (instance != null) {
+            return instance;
+        }
+        return new ImageTransformState();
+    }
+
+    @Override
+    protected boolean animateScale() {
+        return true;
+    }
+
+    @Override
+    public void recycle() {
+        super.recycle();
+        sInstancePool.release(this);
+    }
+
+    @Override
+    protected void reset() {
+        super.reset();
+        mIcon = null;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationBigPictureTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationBigPictureTemplateViewWrapper.java
new file mode 100644
index 0000000..ce9540b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationBigPictureTemplateViewWrapper.java
@@ -0,0 +1,49 @@
+/*
+ * 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.statusbar.notification;
+
+import android.app.Notification;
+import android.content.Context;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.service.notification.StatusBarNotification;
+import android.view.View;
+
+/**
+ * Wraps a notification containing a big picture template
+ */
+public class NotificationBigPictureTemplateViewWrapper extends NotificationTemplateViewWrapper {
+
+    protected NotificationBigPictureTemplateViewWrapper(Context ctx, View view) {
+        super(ctx, view);
+    }
+
+    @Override
+    public void notifyContentUpdated(StatusBarNotification notification) {
+        super.notifyContentUpdated(notification);
+        updateImageTag(notification);
+    }
+
+    private void updateImageTag(StatusBarNotification notification) {
+        final Bundle extras = notification.getNotification().extras;
+        Icon overRiddenIcon = extras.getParcelable(Notification.EXTRA_LARGE_ICON_BIG);
+        if (overRiddenIcon != null) {
+            mPicture.setTag(ImageTransformState.ICON_TAG, overRiddenIcon);
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationCustomViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationCustomViewWrapper.java
similarity index 96%
rename from packages/SystemUI/src/com/android/systemui/statusbar/NotificationCustomViewWrapper.java
rename to packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationCustomViewWrapper.java
index 6fd341b..fd65aac 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationCustomViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationCustomViewWrapper.java
@@ -14,7 +14,7 @@
  * limitations under the License
  */
 
-package com.android.systemui.statusbar;
+package com.android.systemui.statusbar.notification;
 
 import android.view.View;
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationHeaderViewWrapper.java
similarity index 76%
rename from packages/SystemUI/src/com/android/systemui/statusbar/NotificationHeaderViewWrapper.java
rename to packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationHeaderViewWrapper.java
index ddad2e0..4fd4cab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationHeaderViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationHeaderViewWrapper.java
@@ -14,7 +14,7 @@
  * limitations under the License
  */
 
-package com.android.systemui.statusbar;
+package com.android.systemui.statusbar.notification;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -27,17 +27,23 @@
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffColorFilter;
 import android.graphics.drawable.Drawable;
+import android.service.notification.StatusBarNotification;
+import android.util.ArrayMap;
 import android.view.NotificationHeaderView;
 import android.view.View;
+import android.view.ViewGroup;
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
 import android.widget.ImageView;
 
 import com.android.systemui.R;
 import com.android.systemui.ViewInvertHelper;
+import com.android.systemui.statusbar.TransformableView;
+import com.android.systemui.statusbar.ViewTransformationHelper;
 import com.android.systemui.statusbar.phone.NotificationPanelView;
 
-import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Stack;
 
 /**
  * Wraps a notification header view.
@@ -52,6 +58,8 @@
     protected final Interpolator mLinearOutSlowInInterpolator;
     protected final ViewInvertHelper mInvertHelper;
 
+    protected final ViewTransformationHelper mTransformationHelper;
+
     protected int mColor;
     private ImageView mIcon;
 
@@ -64,7 +72,9 @@
         mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(ctx,
                 android.R.interpolator.linear_out_slow_in);
         mInvertHelper = new ViewInvertHelper(ctx, NotificationPanelView.DOZE_ANIMATION_DURATION);
+        mTransformationHelper = new ViewTransformationHelper();
         resolveHeaderViews();
+        updateInvertHelper();
     }
 
     protected void resolveHeaderViews() {
@@ -73,12 +83,6 @@
         mColor = resolveColor(mExpandButton);
         mNotificationHeader = (NotificationHeaderView) mView.findViewById(
                 com.android.internal.R.id.notification_header);
-        for (int i = 0; i < mNotificationHeader.getChildCount(); i++) {
-            View child = mNotificationHeader.getChildAt(i);
-            if (child != mIcon) {
-                mInvertHelper.addTarget(child);
-            }
-        }
     }
 
     private int resolveColor(ImageView icon) {
@@ -92,10 +96,58 @@
     }
 
     @Override
-    public void notifyContentUpdated() {
-        mInvertHelper.clearTargets();
+    public void notifyContentUpdated(StatusBarNotification notification) {
         // Reinspect the notification.
         resolveHeaderViews();
+        updateInvertHelper();
+        updateTransformedTypes();
+        addRemainingTransformTypes();
+        updateCropToPaddingForImageViews();
+    }
+
+    /**
+     * Adds the remaining TransformTypes to the TransformHelper. This is done to make sure that each
+     * child is faded automatically and doesn't have to be manually added.
+     * The keys used for the views are the ids.
+     */
+    private void addRemainingTransformTypes() {
+        mTransformationHelper.addRemainingTransformTypes(mView);
+    }
+
+    /**
+     * Since we are deactivating the clipping when transforming the ImageViews don't get clipped
+     * anymore during these transitions. We can avoid that by using
+     * {@link ImageView#setCropToPadding(boolean)} on all ImageViews.
+     */
+    private void updateCropToPaddingForImageViews() {
+        Stack<View> stack = new Stack<>();
+        stack.push(mView);
+        while (!stack.isEmpty()) {
+            View child = stack.pop();
+            if (child instanceof ImageView) {
+                ((ImageView) child).setCropToPadding(true);
+            } else if (child instanceof ViewGroup){
+                ViewGroup group = (ViewGroup) child;
+                for (int i = 0; i < group.getChildCount(); i++) {
+                    stack.push(group.getChildAt(i));
+                }
+            }
+        }
+    }
+
+    protected void updateInvertHelper() {
+        mInvertHelper.clearTargets();
+        for (int i = 0; i < mNotificationHeader.getChildCount(); i++) {
+            View child = mNotificationHeader.getChildAt(i);
+            if (child != mIcon) {
+                mInvertHelper.addTarget(child);
+            }
+        }
+    }
+
+    protected void updateTransformedTypes() {
+        mTransformationHelper.reset();
+        mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_HEADER, mNotificationHeader);
     }
 
     @Override
@@ -236,4 +288,25 @@
     public NotificationHeaderView getNotificationHeader() {
         return mNotificationHeader;
     }
+
+    @Override
+    public TransformState getCurrentState(int fadingView) {
+        return mTransformationHelper.getCurrentState(fadingView);
+    }
+
+    @Override
+    public void transformTo(TransformableView notification, Runnable endRunnable) {
+        mTransformationHelper.transformTo(notification, endRunnable);
+    }
+
+    @Override
+    public void transformFrom(TransformableView notification) {
+        mTransformationHelper.transformFrom(notification);
+    }
+
+    @Override
+    public void setVisible(boolean visible) {
+        super.setVisible(visible);
+        mTransformationHelper.setVisible(visible);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java
new file mode 100644
index 0000000..a959e07
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.statusbar.notification;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Color;
+import android.service.notification.StatusBarNotification;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.android.systemui.statusbar.CrossFadeHelper;
+import com.android.systemui.statusbar.TransformableView;
+import com.android.systemui.statusbar.ViewTransformationHelper;
+import com.android.systemui.statusbar.stack.StackStateAnimator;
+
+/**
+ * Wraps a notification view inflated from a template.
+ */
+public class NotificationTemplateViewWrapper extends NotificationHeaderViewWrapper {
+
+    private static final int mDarkProgressTint = 0xffffffff;
+
+    protected ImageView mPicture;
+    private ProgressBar mProgressBar;
+    private TextView mTitle;
+    private TextView mText;
+
+    protected NotificationTemplateViewWrapper(Context ctx, View view) {
+        super(ctx, view);
+        mTransformationHelper.setCustomTransformation(
+                new ViewTransformationHelper.CustomTransformation() {
+                    @Override
+                    public boolean transformTo(TransformState ownState,
+                            TransformableView notification, final Runnable endRunnable) {
+                        if (!(notification instanceof HybridNotificationView)) {
+                            return false;
+                        }
+                        TransformState otherState = notification.getCurrentState(
+                                TRANSFORMING_VIEW_TITLE);
+                        CrossFadeHelper.fadeOut(mText, endRunnable);
+                        if (otherState != null) {
+                            int[] otherStablePosition = otherState.getLaidOutLocationOnScreen();
+                            int[] ownPosition = ownState.getLaidOutLocationOnScreen();
+                            mText.animate()
+                                    .translationY((otherStablePosition[1]
+                                            + otherState.getTransformedView().getHeight()
+                                            - ownPosition[1]) * 0.33f)
+                                    .setDuration(
+                                            StackStateAnimator.ANIMATION_DURATION_STANDARD)
+                                    .setInterpolator(TransformState.FAST_OUT_SLOW_IN)
+                                    .withEndAction(new Runnable() {
+                                        @Override
+                                        public void run() {
+                                            if (endRunnable != null) {
+                                                endRunnable.run();
+                                            }
+                                            TransformState.setClippingDeactivated(mText,
+                                                    false);
+                                        }
+                                    });
+                            TransformState.setClippingDeactivated(mText, true);
+                            otherState.recycle();
+                        }
+                        return true;
+                    }
+
+                    @Override
+                    public boolean transformFrom(TransformState ownState,
+                            TransformableView notification) {
+                        if (!(notification instanceof HybridNotificationView)) {
+                            return false;
+                        }
+                        TransformState otherState = notification.getCurrentState(
+                                TRANSFORMING_VIEW_TITLE);
+                        boolean isVisible = mText.getVisibility() == View.VISIBLE;
+                        CrossFadeHelper.fadeIn(mText);
+                        if (otherState != null) {
+                            int[] otherStablePosition = otherState.getLaidOutLocationOnScreen();
+                            int[] ownStablePosition = ownState.getLaidOutLocationOnScreen();
+                            if (!isVisible) {
+                                mText.setTranslationY((otherStablePosition[1]
+                                        + otherState.getTransformedView().getHeight()
+                                        - ownStablePosition[1]) * 0.33f);
+                            }
+                            mText.animate()
+                                    .translationY(0)
+                                    .setDuration(
+                                            StackStateAnimator.ANIMATION_DURATION_STANDARD)
+                                    .setInterpolator(TransformState.FAST_OUT_SLOW_IN)
+                                    .withEndAction(new Runnable() {
+                                        @Override
+                                        public void run() {
+                                            TransformState.setClippingDeactivated(mText,
+                                                    false);
+                                        }
+                                    });
+                            TransformState.setClippingDeactivated(mText, true);
+                            otherState.recycle();
+                        }
+                        return true;
+                    }
+                }, TRANSFORMING_VIEW_TEXT);
+    }
+
+    private void resolveTemplateViews(StatusBarNotification notification) {
+        mPicture = (ImageView) mView.findViewById(com.android.internal.R.id.right_icon);
+        mPicture.setTag(ImageTransformState.ICON_TAG,
+                notification.getNotification().getLargeIcon());
+        mTitle = (TextView) mView.findViewById(com.android.internal.R.id.title);
+        mText = (TextView) mView.findViewById(com.android.internal.R.id.text);
+        final View progress = mView.findViewById(com.android.internal.R.id.progress);
+        if (progress instanceof ProgressBar) {
+            mProgressBar = (ProgressBar) progress;
+        } else {
+            // It's still a viewstub
+            mProgressBar = null;
+        }
+    }
+
+    @Override
+    public void notifyContentUpdated(StatusBarNotification notification) {
+        // Reinspect the notification. Before the super call, because the super call also updates
+        // the transformation types and we need to have our values set by then.
+        resolveTemplateViews(notification);
+        super.notifyContentUpdated(notification);
+    }
+
+    @Override
+    protected void updateInvertHelper() {
+        super.updateInvertHelper();
+        View mainColumn = mView.findViewById(com.android.internal.R.id.notification_main_column);
+        if (mainColumn != null) {
+            mInvertHelper.addTarget(mainColumn);
+        }
+    }
+
+    @Override
+    protected void updateTransformedTypes() {
+        // This also clears the existing types
+        super.updateTransformedTypes();
+        if (mTitle != null) {
+            mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_TITLE, mTitle);
+        }
+        if (mText != null) {
+            mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_TEXT, mText);
+        }
+        if (mPicture != null) {
+            mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_IMAGE, mPicture);
+        }
+        if (mProgressBar != null) {
+            mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_PROGRESS, mProgressBar);
+        }
+    }
+
+    @Override
+    public void setDark(boolean dark, boolean fade, long delay) {
+        super.setDark(dark, fade, delay);
+        setPictureGrayscale(dark, fade, delay);
+        setProgressBarDark(dark, fade, delay);
+    }
+
+    private void setProgressBarDark(boolean dark, boolean fade, long delay) {
+        if (mProgressBar != null) {
+            if (fade) {
+                fadeProgressDark(mProgressBar, dark, delay);
+            } else {
+                updateProgressDark(mProgressBar, dark);
+            }
+        }
+    }
+
+    private void fadeProgressDark(final ProgressBar target, final boolean dark, long delay) {
+        startIntensityAnimation(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                float t = (float) animation.getAnimatedValue();
+                updateProgressDark(target, t);
+            }
+        }, dark, delay, null /* listener */);
+    }
+
+    private void updateProgressDark(ProgressBar target, float intensity) {
+        int color = interpolateColor(mColor, mDarkProgressTint, intensity);
+        target.getIndeterminateDrawable().mutate().setTint(color);
+        target.getProgressDrawable().mutate().setTint(color);
+    }
+
+    private void updateProgressDark(ProgressBar target, boolean dark) {
+        updateProgressDark(target, dark ? 1f : 0f);
+    }
+
+    protected void setPictureGrayscale(boolean grayscale, boolean fade, long delay) {
+        if (mPicture != null) {
+            if (fade) {
+                fadeGrayscale(mPicture, grayscale, delay);
+            } else {
+                updateGrayscale(mPicture, grayscale);
+            }
+        }
+    }
+
+    private static int interpolateColor(int source, int target, float t) {
+        int aSource = Color.alpha(source);
+        int rSource = Color.red(source);
+        int gSource = Color.green(source);
+        int bSource = Color.blue(source);
+        int aTarget = Color.alpha(target);
+        int rTarget = Color.red(target);
+        int gTarget = Color.green(target);
+        int bTarget = Color.blue(target);
+        return Color.argb(
+                (int) (aSource * (1f - t) + aTarget * t),
+                (int) (rSource * (1f - t) + rTarget * t),
+                (int) (gSource * (1f - t) + gTarget * t),
+                (int) (bSource * (1f - t) + bTarget * t));
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationViewWrapper.java
similarity index 68%
rename from packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewWrapper.java
rename to packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationViewWrapper.java
index 61499de..0ceba78 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationViewWrapper.java
@@ -14,22 +14,29 @@
  * limitations under the License
  */
 
-package com.android.systemui.statusbar;
+package com.android.systemui.statusbar.notification;
 
 import android.content.Context;
+import android.service.notification.StatusBarNotification;
 import android.view.NotificationHeaderView;
 import android.view.View;
 
+import com.android.systemui.statusbar.CrossFadeHelper;
+import com.android.systemui.statusbar.TransformableView;
+
 /**
  * Wraps the actual notification content view; used to implement behaviors which are different for
  * the individual templates and custom views.
  */
-public abstract class NotificationViewWrapper {
+public abstract class NotificationViewWrapper implements TransformableView {
 
     protected final View mView;
 
     public static NotificationViewWrapper wrap(Context ctx, View v) {
         if (v.getId() == com.android.internal.R.id.status_bar_latest_event_content) {
+            if ("bigPicture".equals(v.getTag())) {
+                return new NotificationBigPictureTemplateViewWrapper(ctx, v);
+            }
             return new NotificationTemplateViewWrapper(ctx, v);
         } else if (v instanceof NotificationHeaderView) {
             return new NotificationHeaderViewWrapper(ctx, v);
@@ -53,8 +60,9 @@
 
     /**
      * Notifies this wrapper that the content of the view might have changed.
+     * @param notification
      */
-    public void notifyContentUpdated() {};
+    public void notifyContentUpdated(StatusBarNotification notification) {};
 
     /**
      * @return true if this template might need to be clipped with a round rect to make it look
@@ -78,4 +86,26 @@
     public NotificationHeaderView getNotificationHeader() {
         return null;
     }
+
+    @Override
+    public TransformState getCurrentState(int fadingView) {
+        return null;
+    }
+
+    @Override
+    public void transformTo(TransformableView notification, Runnable endRunnable) {
+        // By default we are fading out completely
+        CrossFadeHelper.fadeOut(mView, endRunnable);
+    }
+
+    @Override
+    public void transformFrom(TransformableView notification) {
+        // By default we are fading in completely
+        CrossFadeHelper.fadeIn(mView);
+    }
+
+    @Override
+    public void setVisible(boolean visible) {
+        mView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ProgressTransformState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ProgressTransformState.java
new file mode 100644
index 0000000..bf78194
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ProgressTransformState.java
@@ -0,0 +1,50 @@
+/*
+ * 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.statusbar.notification;
+
+import android.util.Pools;
+
+/**
+ * A transform state of a progress view.
+*/
+public class ProgressTransformState extends TransformState {
+
+    private static Pools.SimplePool<ProgressTransformState> sInstancePool
+            = new Pools.SimplePool<>(40);
+
+    @Override
+    protected boolean sameAs(TransformState otherState) {
+        if (otherState instanceof ProgressTransformState) {
+            return true;
+        }
+        return super.sameAs(otherState);
+    }
+
+    public static ProgressTransformState obtain() {
+        ProgressTransformState instance = sInstancePool.acquire();
+        if (instance != null) {
+            return instance;
+        }
+        return new ProgressTransformState();
+    }
+
+    @Override
+    public void recycle() {
+        super.recycle();
+        sInstancePool.release(this);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/TextViewTransformState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/TextViewTransformState.java
new file mode 100644
index 0000000..5ab441d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/TextViewTransformState.java
@@ -0,0 +1,70 @@
+/*
+ * 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.statusbar.notification;
+
+import android.text.TextUtils;
+import android.util.Pools;
+import android.view.View;
+import android.widget.TextView;
+
+/**
+ * A transform state of a mText view.
+*/
+public class TextViewTransformState extends TransformState {
+
+    private static Pools.SimplePool<TextViewTransformState> sInstancePool
+            = new Pools.SimplePool<>(40);
+    private CharSequence mText;
+
+    @Override
+    public void initFrom(View view) {
+        super.initFrom(view);
+        if (view instanceof TextView) {
+            TextView txt = (TextView) view;
+            mText = txt.getText();
+        }
+    }
+
+    @Override
+    protected boolean sameAs(TransformState otherState) {
+        if (otherState instanceof TextViewTransformState) {
+            TextViewTransformState otherTvs = (TextViewTransformState) otherState;
+            return TextUtils.equals(otherTvs.mText, mText);
+        }
+        return super.sameAs(otherState);
+    }
+
+    public static TextViewTransformState obtain() {
+        TextViewTransformState instance = sInstancePool.acquire();
+        if (instance != null) {
+            return instance;
+        }
+        return new TextViewTransformState();
+    }
+
+    @Override
+    public void recycle() {
+        super.recycle();
+        sInstancePool.release(this);
+    }
+
+    @Override
+    protected void reset() {
+        super.reset();
+        mText = null;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java
new file mode 100644
index 0000000..388ba0e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java
@@ -0,0 +1,316 @@
+/*
+ * 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.statusbar.notification;
+
+import android.util.ArraySet;
+import android.util.Pools;
+import android.view.NotificationHeaderView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.CrossFadeHelper;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.stack.StackStateAnimator;
+
+/**
+ * A transform state of a view.
+*/
+public class TransformState {
+
+    private static final int ANIMATE_X = 0x1;
+    private static final int ANIMATE_Y = 0x10;
+    private static final int ANIMATE_ALL = ANIMATE_X | ANIMATE_Y;
+    private static final int CLIP_CLIPPING_SET = R.id.clip_children_set_tag;
+    private static final int CLIP_CHILDREN_TAG = R.id.clip_children_tag;
+    private static final int CLIP_TO_PADDING = R.id.clip_to_padding_tag;
+    public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
+    private static Pools.SimplePool<TransformState> sInstancePool = new Pools.SimplePool<>(40);
+
+    protected View mTransformedView;
+    private int[] mOwnPosition = new int[2];
+
+    public void initFrom(View view) {
+        mTransformedView = view;
+    }
+
+    /**
+     * Transforms the {@link #mTransformedView} from the given transformviewstate
+     * @param otherState the state to transform from
+     */
+    public void transformViewFrom(TransformState otherState) {
+        mTransformedView.animate().cancel();
+        if (sameAs(otherState)) {
+            // We have the same content, lets show ourselves
+            mTransformedView.setAlpha(1.0f);
+            mTransformedView.setVisibility(View.VISIBLE);
+        } else {
+            CrossFadeHelper.fadeIn(mTransformedView);
+        }
+        animateViewFrom(otherState);
+    }
+
+    public void animateViewFrom(TransformState otherState) {
+        animateViewFrom(otherState, ANIMATE_ALL);
+    }
+
+    public void animateViewVerticalFrom(TransformState otherState) {
+        animateViewFrom(otherState, ANIMATE_Y);
+    }
+
+    private void animateViewFrom(TransformState otherState, int animationFlags) {
+        final View transformedView = mTransformedView;
+        // lets animate the positions correctly
+        int[] otherPosition = otherState.getLocationOnScreen();
+        int[] ownStablePosition = getLaidOutLocationOnScreen();
+        if ((animationFlags & ANIMATE_X) != 0) {
+            transformedView.setTranslationX(otherPosition[0] - ownStablePosition[0]);
+            transformedView.animate().translationX(0);
+        }
+        if ((animationFlags & ANIMATE_Y) != 0) {
+            transformedView.setTranslationY(otherPosition[1] - ownStablePosition[1]);
+            transformedView.animate().translationY(0);
+        }
+        if (animateScale()) {
+            // we also want to animate the scale if we're the same
+            View otherView = otherState.getTransformedView();
+            if (otherView.getWidth() != transformedView.getWidth()) {
+                float scaleX = (otherView.getWidth() * otherView.getScaleX()
+                        / (float) transformedView.getWidth());
+                transformedView.setScaleX(scaleX);
+                transformedView.setPivotX(0);
+                transformedView.animate().scaleX(1.0f);
+            }
+            if (otherView.getHeight() != transformedView.getHeight()) {
+                float scaleY = (otherView.getHeight() * otherView.getScaleY()
+                        / (float) transformedView.getHeight());
+                transformedView.setScaleY(scaleY);
+                transformedView.setPivotY(0);
+                transformedView.animate().scaleY(1.0f);
+            }
+        }
+        transformedView.animate()
+                .setInterpolator(TransformState.FAST_OUT_SLOW_IN)
+                .setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD)
+                .withEndAction(new Runnable() {
+                    @Override
+                    public void run() {
+                        setClippingDeactivated(transformedView, false);
+                    }
+                });
+        setClippingDeactivated(transformedView, true);
+    }
+
+    protected boolean animateScale() {
+        return false;
+    }
+
+    /**
+     * Transforms the {@link #mTransformedView} to the given transformviewstate
+     * @param otherState the state to transform from
+     * @param endRunnable a runnable to run at the end of the animation
+     * @return whether an animation was started
+     */
+    public boolean transformViewTo(TransformState otherState, final Runnable endRunnable) {
+        mTransformedView.animate().cancel();
+        if (sameAs(otherState)) {
+            // We have the same text, lets show ourselfs
+            mTransformedView.setAlpha(0.0f);
+            mTransformedView.setVisibility(View.INVISIBLE);
+            return false;
+        } else {
+            CrossFadeHelper.fadeOut(mTransformedView, endRunnable);
+        }
+        animateViewTo(otherState, endRunnable);
+        return true;
+    }
+
+    public void animateViewTo(TransformState otherState, Runnable endRunnable) {
+        animateViewTo(otherState, endRunnable, ANIMATE_ALL);
+    }
+
+    public void animateViewVerticalTo(TransformState otherState, Runnable endRunnable) {
+        animateViewTo(otherState, endRunnable, ANIMATE_Y);
+    }
+
+    private void animateViewTo(TransformState otherState, final Runnable endRunnable,
+            int animationFlags) {
+        // lets animate the positions correctly
+        int[] otherStablePosition = otherState.getLaidOutLocationOnScreen();
+        int[] ownPosition = getLaidOutLocationOnScreen();
+        final View transformedView = mTransformedView;
+        if ((animationFlags & ANIMATE_X) != 0) {
+            transformedView.animate()
+                    .translationX(otherStablePosition[0] - ownPosition[0]);
+        }
+        if ((animationFlags & ANIMATE_Y) != 0) {
+            transformedView.animate()
+                    .translationY(otherStablePosition[1] - ownPosition[1]);
+        }
+        transformedView.animate()
+                .setInterpolator(TransformState.FAST_OUT_SLOW_IN)
+                .setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD)
+                .withEndAction(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (endRunnable != null) {
+                            endRunnable.run();
+                        }
+                        setClippingDeactivated(transformedView, false);
+                    }
+                });
+        setClippingDeactivated(transformedView, true);
+    }
+
+    public static void setClippingDeactivated(final View transformedView, boolean deactivated) {
+        ViewGroup view = (ViewGroup) transformedView.getParent();
+        while (true) {
+            ArraySet<View> clipSet = (ArraySet<View>) view.getTag(CLIP_CLIPPING_SET);
+            if (clipSet == null) {
+                clipSet = new ArraySet<>();
+                view.setTag(CLIP_CLIPPING_SET, clipSet);
+            }
+            Boolean clipChildren = (Boolean) view.getTag(CLIP_CHILDREN_TAG);
+            if (clipChildren == null) {
+                clipChildren = view.getClipChildren();
+                view.setTag(CLIP_CHILDREN_TAG, clipChildren);
+            }
+            Boolean clipToPadding = (Boolean) view.getTag(CLIP_TO_PADDING);
+            if (clipToPadding == null) {
+                clipToPadding = view.getClipToPadding();
+                view.setTag(CLIP_TO_PADDING, clipToPadding);
+            }
+            ExpandableNotificationRow row = view instanceof ExpandableNotificationRow
+                    ? (ExpandableNotificationRow) view
+                    : null;
+            if (!deactivated) {
+                clipSet.remove(transformedView);
+                if (clipSet.isEmpty()) {
+                    view.setClipChildren(clipChildren);
+                    view.setClipToPadding(clipToPadding);
+                    view.setTag(CLIP_CLIPPING_SET, null);
+                    if (row != null) {
+                        row.setClipToActualHeight(true);
+                    }
+                }
+            } else {
+                clipSet.add(transformedView);
+                view.setClipChildren(false);
+                view.setClipToPadding(false);
+                if (row != null && row.isChildInGroup()) {
+                    // We still want to clip to the parent's height
+                    row.setClipToActualHeight(false);
+                }
+            }
+            if (row != null && !row.isChildInGroup()) {
+                return;
+            }
+            final ViewParent parent = view.getParent();
+            if (parent instanceof ViewGroup) {
+                view = (ViewGroup) parent;
+            } else {
+                return;
+            }
+        }
+    }
+
+    public int[] getLaidOutLocationOnScreen() {
+        int[] location = getLocationOnScreen();
+        location[0] -= mTransformedView.getTranslationX();
+        location[1] -= mTransformedView.getTranslationY();
+        return location;
+    }
+
+    public int[] getLocationOnScreen() {
+        mTransformedView.getLocationOnScreen(mOwnPosition);
+        return mOwnPosition;
+    }
+
+    protected boolean sameAs(TransformState otherState) {
+        return false;
+    }
+
+    public static TransformState createFrom(View view) {
+        if (view instanceof TextView) {
+            TextViewTransformState result = TextViewTransformState.obtain();
+            result.initFrom(view);
+            return result;
+        }
+        if (view instanceof NotificationHeaderView) {
+            HeaderTransformState result = HeaderTransformState.obtain();
+            result.initFrom(view);
+            return result;
+        }
+        if (view instanceof ImageView) {
+            ImageTransformState result = ImageTransformState.obtain();
+            result.initFrom(view);
+            return result;
+        }
+        if (view instanceof ProgressBar) {
+            ProgressTransformState result = ProgressTransformState.obtain();
+            result.initFrom(view);
+            return result;
+        }
+        TransformState result = obtain();
+        result.initFrom(view);
+        return result;
+    }
+
+    public void recycle() {
+        reset();
+        if (getClass() == TransformState.class) {
+            sInstancePool.release(this);
+        }
+    }
+
+    protected void reset() {
+        mTransformedView = null;
+    }
+
+    public void setVisible(boolean visible) {
+        mTransformedView.animate().cancel();
+        mTransformedView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+        mTransformedView.setAlpha(visible ? 1.0f : 0.0f);
+        if (visible) {
+            mTransformedView.setTranslationX(0);
+            mTransformedView.setTranslationY(0);
+            mTransformedView.setScaleX(1.0f);
+            mTransformedView.setScaleY(1.0f);
+        }
+    }
+
+    public void prepareFadeIn() {
+    }
+
+    public static TransformState obtain() {
+        TransformState instance = sInstancePool.acquire();
+        if (instance != null) {
+            return instance;
+        }
+        return new TransformState();
+    }
+
+    public View getTransformedView() {
+        return mTransformedView;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java
index df8c7fa..26abc48 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java
@@ -110,12 +110,12 @@
                     mInitialTouchX = x;
                     mInitialTouchY = y;
                     int expandedHeight = mPickedChild.getActualHeight();
+                    mHeadsUpManager.unpinAll();
                     mPanel.setPanelScrimMinFraction((float) expandedHeight
                             / mPanel.getMaxPanelHeight());
                     mPanel.startExpandMotion(x, y, true /* startTracking */, expandedHeight
                             + mNotificationsTopPadding);
                     mPanel.clearNotificattonEffects();
-                    mHeadsUpManager.unpinAll();
                     return true;
                 }
                 break;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarGestureHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarGestureHelper.java
index 2db0804..ae6a11f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarGestureHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarGestureHelper.java
@@ -162,11 +162,16 @@
         mDockWindowTouchSlopExceeded = false;
         mTouchDownX = (int) event.getX();
         mTouchDownY = (int) event.getY();
-        View recentsButton = mNavigationBarView.getRecentsButton();
-        mDownOnRecents = mTouchDownX >= recentsButton.getLeft()
-                && mTouchDownX <= recentsButton.getRight()
-                && mTouchDownY >= recentsButton.getTop()
-                && mTouchDownY <= recentsButton.getBottom();
+
+        if (mNavigationBarView != null) {
+            View recentsButton = mNavigationBarView.getRecentsButton();
+            if (recentsButton != null) {
+                mDownOnRecents = mTouchDownX >= recentsButton.getLeft()
+                        && mTouchDownX <= recentsButton.getRight()
+                        && mTouchDownY >= recentsButton.getTop()
+                        && mTouchDownY <= recentsButton.getBottom();
+            }
+        }
     }
 
     private boolean handleDragActionMoveEvent(MotionEvent event) {
@@ -218,7 +223,7 @@
             if (mDragMode == DRAG_MODE_DIVIDER) {
                 int position = !mIsVertical ? (int) event.getRawY() : (int) event.getRawX();
                 SnapTarget snapTarget = mDivider.getView().getSnapAlgorithm()
-                        .calculateSnapTarget(position, 0f /* velocity */);
+                        .calculateSnapTarget(position, 0f /* velocity */, false /* hardDismiss */);
                 mDivider.getView().resizeStack(position, snapTarget.position, snapTarget);
             } else if (mDragMode == DRAG_MODE_RECENTS) {
                 mRecentsComponent.onDraggingInRecents(event.getRawY());
@@ -237,7 +242,8 @@
                                 : (int) event.getRawY(),
                         mIsVertical
                                 ? mVelocityTracker.getXVelocity()
-                                : mVelocityTracker.getYVelocity());
+                                : mVelocityTracker.getYVelocity(),
+                        true /* avoidDismissStart */);
             } else if (mDragMode == DRAG_MODE_RECENTS) {
                 mRecentsComponent.onDraggingInRecentsEnded(mVelocityTracker.getYVelocity());
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
index efa8f5b..53099032 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
@@ -21,6 +21,7 @@
 import android.animation.ObjectAnimator;
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
+import android.annotation.Nullable;
 import android.app.ActivityManagerNative;
 import android.app.StatusBarManager;
 import android.content.Context;
@@ -131,12 +132,15 @@
         }
 
         public void onBackAltCleared() {
+            View backButton = getBackButton();
+            View homeButton = getHomeButton();
+
             // When dismissing ime during unlock, force the back button to run the same appearance
             // animation as home (if we catch this condition early enough).
-            if (!mBackTransitioning && getBackButton().getVisibility() == VISIBLE
-                    && mHomeAppearing && getHomeButton().getAlpha() == 0) {
+            if (backButton != null && !mBackTransitioning && backButton.getVisibility() == VISIBLE
+                    && mHomeAppearing && homeButton != null && getHomeButton().getAlpha() == 0) {
                 getBackButton().setAlpha(0);
-                ValueAnimator a = ObjectAnimator.ofFloat(getBackButton(), "alpha", 0, 1);
+                ValueAnimator a = ObjectAnimator.ofFloat(backButton, "alpha", 0, 1);
                 a.setStartDelay(mStartDelay);
                 a.setDuration(mDuration);
                 a.setInterpolator(mInterpolator);
@@ -222,7 +226,10 @@
     }
 
     public void abortCurrentGesture() {
-        getHomeButton().abortCurrentGesture();
+        View homeButton = getHomeButton();
+        if (homeButton != null) {
+            getHomeButton().abortCurrentGesture();
+        }
     }
 
     private H mHandler = new H();
@@ -235,26 +242,34 @@
         return mRotatedViews;
     }
 
+    // The following Buttons can possibly return null if NavigationBarView is extended to provide
+    // a different layout and the buttons do not exist in that new layout.
+    @Nullable
     public KeyButtonView getRecentsButton() {
         return (KeyButtonView) getCurrentView().findViewById(R.id.recent_apps);
     }
 
+    @Nullable
     public View getMenuButton() {
         return getCurrentView().findViewById(R.id.menu);
     }
 
+    @Nullable
     public View getBackButton() {
         return getCurrentView().findViewById(R.id.back);
     }
 
+    @Nullable
     public KeyButtonView getHomeButton() {
         return (KeyButtonView) getCurrentView().findViewById(R.id.home);
     }
 
+    @Nullable
     public View getImeSwitchButton() {
         return getCurrentView().findViewById(R.id.ime_switcher);
     }
 
+    @Nullable
     public View getAppShelf() {
         return getCurrentView().findViewById(R.id.app_shelf);
     }
@@ -329,19 +344,27 @@
                 ? getBackIconWithAlt(mCarMode, mVertical)
                 : getBackIcon(mCarMode, mVertical);
 
-        ((ImageView) getBackButton()).setImageDrawable(backIcon);
+        View backButton = getBackButton();
+        if (backButton != null && backButton instanceof ImageView) {
+            ((ImageView) backButton).setImageDrawable(backIcon);
+        }
 
-        ((ImageView) getRecentsButton()).setImageDrawable(
-                mVertical ? mRecentLandIcon : mRecentIcon);
+        ImageView recentsButton = getRecentsButton();
+        if (recentsButton != null) {
+            recentsButton.setImageDrawable(mVertical ? mRecentLandIcon : mRecentIcon);
+        }
 
-        if (mCarMode) {
-            ((ImageView) getHomeButton()).setImageDrawable(mHomeCarModeIcon);
-        } else {
-            ((ImageView) getHomeButton()).setImageDrawable(mHomeDefaultIcon);
+        ImageView homeButton = getHomeButton();
+        if (homeButton != null) {
+            homeButton.setImageDrawable(mCarMode ? mHomeCarModeIcon : mHomeDefaultIcon);
         }
 
         final boolean showImeButton = ((hints & StatusBarManager.NAVIGATION_HINT_IME_SHOWN) != 0);
-        getImeSwitchButton().setVisibility(showImeButton ? View.VISIBLE : View.INVISIBLE);
+        View imeSwitchButton = getImeSwitchButton();
+        if (imeSwitchButton != null) {
+            imeSwitchButton.setVisibility(showImeButton ? View.VISIBLE : View.INVISIBLE);
+        }
+
         // Update menu button in case the IME state has changed.
         setMenuVisibility(mShowMenu, true);
 
@@ -382,9 +405,20 @@
             disableRecent = false;
         }
 
-        getBackButton()   .setVisibility(disableBack       ? View.INVISIBLE : View.VISIBLE);
-        getHomeButton()   .setVisibility(disableHome       ? View.INVISIBLE : View.VISIBLE);
-        getRecentsButton().setVisibility(disableRecent     ? View.INVISIBLE : View.VISIBLE);
+        View backButton = getBackButton();
+        if (backButton != null) {
+            backButton.setVisibility(disableBack ? View.INVISIBLE : View.VISIBLE);
+        }
+
+        View homeButton = getHomeButton();
+        if (homeButton != null) {
+            homeButton.setVisibility(disableHome ? View.INVISIBLE : View.VISIBLE);
+        }
+
+        View recentsButton = getRecentsButton();
+        if (recentsButton != null) {
+            recentsButton.setVisibility(disableRecent ? View.INVISIBLE : View.VISIBLE);
+        }
 
         // The app shelf, if it exists, follows the visibility of the home button.
         View appShelf = getAppShelf();
@@ -475,7 +509,11 @@
         // Only show Menu if IME switcher not shown.
         final boolean shouldShow = mShowMenu &&
                 ((mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_IME_SHOWN) == 0);
-        getMenuButton().setVisibility(shouldShow ? View.VISIBLE : View.INVISIBLE);
+
+        View menuButton = getMenuButton();
+        if (menuButton != null) {
+            menuButton.setVisibility(shouldShow ? View.VISIBLE : View.INVISIBLE);
+        }
     }
 
     @Override
@@ -489,7 +527,10 @@
 
         mCurrentView = mRotatedViews[Surface.ROTATION_0];
 
-        getImeSwitchButton().setOnClickListener(mImeSwitcherClickListener);
+        View imeSwitchButton = getImeSwitchButton();
+        if (imeSwitchButton != null) {
+            imeSwitchButton.setOnClickListener(mImeSwitcherClickListener);
+        }
 
         updateRTLOrder();
 
@@ -515,9 +556,13 @@
     }
 
     private void updateRecentsIcon(boolean dockedStackExists) {
-        getRecentsButton().setImageResource(dockedStackExists
-                ? R.drawable.ic_sysbar_docked
-                : R.drawable.ic_sysbar_recent);
+        ImageView recentsButton = getRecentsButton();
+
+        if (recentsButton != null) {
+            recentsButton.setImageResource(dockedStackExists
+                    ? R.drawable.ic_sysbar_docked
+                    : R.drawable.ic_sysbar_recent);
+        }
     }
 
     public boolean isVertical() {
@@ -533,7 +578,10 @@
         mCurrentView.setVisibility(View.VISIBLE);
         updateLayoutTransitionsEnabled();
 
-        getImeSwitchButton().setOnClickListener(mImeSwitcherClickListener);
+        View imeSwitchButton = getImeSwitchButton();
+        if (imeSwitchButton != null) {
+            imeSwitchButton.setOnClickListener(mImeSwitcherClickListener);
+        }
 
         mDeadZone = (DeadZone) mCurrentView.findViewById(R.id.deadzone);
 
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 f6fc259..3130eb9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java
@@ -16,12 +16,13 @@
 
 package com.android.systemui.statusbar.phone;
 
-import android.app.Notification;
 import android.service.notification.StatusBarNotification;
+import android.util.ArraySet;
 
 import com.android.systemui.statusbar.ExpandableNotificationRow;
 import com.android.systemui.statusbar.NotificationData;
 import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
 
 import java.util.HashMap;
 import java.util.HashSet;
@@ -29,18 +30,19 @@
 /**
  * A class to handle notifications and their corresponding groups.
  */
-public class NotificationGroupManager {
+public class NotificationGroupManager implements HeadsUpManager.OnHeadsUpChangedListener {
 
     private final HashMap<String, NotificationGroup> mGroupMap = new HashMap<>();
     private OnGroupChangeListener mListener;
     private int mBarState = -1;
+    private ArraySet<String> mHeadsUpedEntries = new ArraySet<>();
 
     public void setOnGroupChangeListener(OnGroupChangeListener listener) {
         mListener = listener;
     }
 
     public boolean isGroupExpanded(StatusBarNotification sbn) {
-        NotificationGroup group = mGroupMap.get(sbn.getGroupKey());
+        NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
         if (group == null) {
             return false;
         }
@@ -48,7 +50,7 @@
     }
 
     public void setGroupExpanded(StatusBarNotification sbn, boolean expanded) {
-        NotificationGroup group = mGroupMap.get(sbn.getGroupKey());
+        NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
         if (group == null) {
             return;
         }
@@ -75,8 +77,7 @@
      */
     private void onEntryRemovedInternal(NotificationData.Entry removed,
             final StatusBarNotification sbn) {
-        Notification notif = sbn.getNotification();
-        String groupKey = sbn.getGroupKey();
+        String groupKey = getGroupKey(sbn);
         final NotificationGroup group = mGroupMap.get(groupKey);
         if (group == null) {
             // When an app posts 2 different notifications as summary of the same group, then a
@@ -85,7 +86,7 @@
             // the close future. See b/23676310 for reference.
             return;
         }
-        if (notif.isGroupChild()) {
+        if (isGroupChild(sbn)) {
             group.children.remove(removed);
         } else {
             group.summary = null;
@@ -97,16 +98,16 @@
         }
     }
 
-    public void onEntryAdded(NotificationData.Entry added) {
-        StatusBarNotification sbn = added.notification;
-        Notification notif = sbn.getNotification();
-        String groupKey = sbn.getGroupKey();
+    public void onEntryAdded(final NotificationData.Entry added) {
+        final StatusBarNotification sbn = added.notification;
+        boolean isGroupChild = isGroupChild(sbn);
+        String groupKey = getGroupKey(sbn);
         NotificationGroup group = mGroupMap.get(groupKey);
         if (group == null) {
             group = new NotificationGroup();
             mGroupMap.put(groupKey, group);
         }
-        if (notif.isGroupChild()) {
+        if (isGroupChild) {
             group.children.add(added);
         } else {
             group.summary = added;
@@ -119,17 +120,17 @@
 
     public void onEntryUpdated(NotificationData.Entry entry,
             StatusBarNotification oldNotification) {
-        if (mGroupMap.get(oldNotification.getGroupKey()) != null) {
+        if (mGroupMap.get(getGroupKey(oldNotification)) != null) {
             onEntryRemovedInternal(entry, oldNotification);
         }
         onEntryAdded(entry);
     }
 
     public boolean isVisible(StatusBarNotification sbn) {
-        if (!sbn.getNotification().isGroupChild()) {
+        if (!isGroupChild(sbn)) {
             return true;
         }
-        NotificationGroup group = mGroupMap.get(sbn.getGroupKey());
+        NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
         if (group != null && (group.expanded || group.summary == null)) {
             return true;
         }
@@ -137,10 +138,10 @@
     }
 
     public boolean hasGroupChildren(StatusBarNotification sbn) {
-        if (!sbn.getNotification().isGroupSummary()) {
+        if (!isGroupSummary(sbn)) {
             return false;
         }
-        NotificationGroup group = mGroupMap.get(sbn.getGroupKey());
+        NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
         if (group == null) {
             return false;
         }
@@ -165,10 +166,10 @@
      * @return whether a given notification is a child in a group which has a summary
      */
     public boolean isChildInGroupWithSummary(StatusBarNotification sbn) {
-        if (!sbn.getNotification().isGroupChild()) {
+        if (!isGroupChild(sbn)) {
             return false;
         }
-        NotificationGroup group = mGroupMap.get(sbn.getGroupKey());
+        NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
         if (group == null || group.summary == null) {
             return false;
         }
@@ -179,10 +180,10 @@
      * @return whether a given notification is a summary in a group which has children
      */
     public boolean isSummaryOfGroup(StatusBarNotification sbn) {
-        if (!sbn.getNotification().isGroupSummary()) {
+        if (!isGroupSummary(sbn)) {
             return false;
         }
-        NotificationGroup group = mGroupMap.get(sbn.getGroupKey());
+        NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
         if (group == null) {
             return false;
         }
@@ -190,24 +191,88 @@
     }
 
     public ExpandableNotificationRow getGroupSummary(StatusBarNotification sbn) {
-        NotificationGroup group = mGroupMap.get(sbn.getGroupKey());
+        NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
         return group == null ? null
                 : group.summary == null ? null
                 : group.summary.row;
     }
 
-    public void onEntryHeadsUped(NotificationData.Entry headsUp) {
-        // TODO: handle this nicely
-    }
-
     public void toggleGroupExpansion(StatusBarNotification sbn) {
-        NotificationGroup group = mGroupMap.get(sbn.getGroupKey());
+        NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
         if (group == null) {
             return;
         }
         setGroupExpanded(group, !group.expanded);
     }
 
+    private boolean isIsolated(StatusBarNotification sbn) {
+        return mHeadsUpedEntries.contains(sbn.getKey()) && sbn.getNotification().isGroupChild();
+    }
+
+    private boolean isGroupSummary(StatusBarNotification sbn) {
+        if (isIsolated(sbn)) {
+            return true;
+        }
+        return sbn.getNotification().isGroupSummary();
+    }
+    private boolean isGroupChild(StatusBarNotification sbn) {
+        if (isIsolated(sbn)) {
+            return false;
+        }
+        return sbn.getNotification().isGroupChild();
+    }
+
+    private String getGroupKey(StatusBarNotification sbn) {
+        if (isIsolated(sbn)) {
+            return sbn.getKey();
+        }
+        return sbn.getGroupKey();
+    }
+
+    @Override
+    public void onHeadsUpPinnedModeChanged(boolean inPinnedMode) {
+    }
+
+    @Override
+    public void onHeadsUpPinned(ExpandableNotificationRow headsUp) {
+    }
+
+    @Override
+    public void onHeadsUpUnPinned(ExpandableNotificationRow headsUp) {
+    }
+
+    @Override
+    public void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) {
+        final StatusBarNotification sbn = entry.notification;
+        if (entry.row.isHeadsUp()) {
+            if (!mHeadsUpedEntries.contains(sbn.getKey())) {
+                final boolean groupChild = sbn.getNotification().isGroupChild();
+                if (groupChild) {
+                    // We will be isolated now, so lets update the groups
+                    onEntryRemovedInternal(entry, entry.notification);
+                }
+                mHeadsUpedEntries.add(sbn.getKey());
+                if (groupChild) {
+                    onEntryAdded(entry);
+                    mListener.onChildIsolationChanged();
+                }
+            }
+        } else {
+            if (mHeadsUpedEntries.contains(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.onChildIsolationChanged();
+                }
+            }
+        }
+    }
+
     public static class NotificationGroup {
         public final HashSet<NotificationData.Entry> children = new HashSet<>();
         public NotificationData.Entry summary;
@@ -230,5 +295,10 @@
          * @param group the group created
          */
         void onGroupCreatedFromChildren(NotificationGroup group);
+
+        /**
+         * The isolation of a child has changed i.e it's group changes.
+         */
+        void onChildIsolationChanged();
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java
index aa01bf2..f0b7894 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java
@@ -81,7 +81,6 @@
     private boolean mMotionAborted;
     private boolean mUpwardsWhenTresholdReached;
     private boolean mAnimatingOnDown;
-    private int mLayoutHeight = 0;
 
     private ValueAnimator mHeightAnimator;
     private ObjectAnimator mPeekAnimator;
@@ -715,10 +714,7 @@
     @Override
     protected void onLayout (boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
-        if (mLayoutHeight != getHeight()) {
-            mLayoutHeight = getHeight();
-            mStatusBar.onPanelHeightChanged();
-        }
+        mStatusBar.onPanelLaidOut();
         requestPanelHeightUpdate();
         mHasLayoutedSinceDown = true;
         if (mUpdateFlingOnLayout) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
index 8a5cf26..71b6713 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -707,6 +707,7 @@
         mHeadsUpManager.setBar(this);
         mHeadsUpManager.addListener(this);
         mHeadsUpManager.addListener(mNotificationPanel);
+        mHeadsUpManager.addListener(mGroupManager);
         mNotificationPanel.setHeadsUpManager(mHeadsUpManager);
         mNotificationData.setHeadsUpManager(mHeadsUpManager);
 
@@ -1184,14 +1185,26 @@
     private void prepareNavigationBarView() {
         mNavigationBarView.reorient();
 
-        mNavigationBarView.getRecentsButton().setOnClickListener(mRecentsClickListener);
-        mNavigationBarView.getRecentsButton().setOnTouchListener(mRecentsPreloadOnTouchListener);
-        mNavigationBarView.getRecentsButton().setLongClickable(true);
-        mNavigationBarView.getRecentsButton().setOnLongClickListener(mRecentsLongClickListener);
-        mNavigationBarView.getBackButton().setLongClickable(true);
-        mNavigationBarView.getBackButton().setOnLongClickListener(mLongPressBackListener);
-        mNavigationBarView.getHomeButton().setOnTouchListener(mHomeActionListener);
-        mNavigationBarView.getHomeButton().setOnLongClickListener(mLongPressHomeListener);
+        View recentsButton = mNavigationBarView.getRecentsButton();
+        if (recentsButton != null) {
+            recentsButton.setOnClickListener(mRecentsClickListener);
+            recentsButton.setOnTouchListener(mRecentsPreloadOnTouchListener);
+            recentsButton.setLongClickable(true);
+            recentsButton.setOnLongClickListener(mRecentsLongClickListener);
+        }
+
+        View backButton = mNavigationBarView.getBackButton();
+        if (backButton != null) {
+            backButton.setLongClickable(true);
+            backButton.setOnLongClickListener(mLongPressBackListener);
+        }
+
+        View homeButton = mNavigationBarView.getHomeButton();
+        if (homeButton != null) {
+            homeButton.setOnTouchListener(mHomeActionListener);
+            homeButton.setOnLongClickListener(mLongPressHomeListener);
+        }
+
         mAssistManager.onConfigurationChanged();
     }
 
@@ -1396,16 +1409,21 @@
 
         }
 
-        ArrayList<View> toRemove = new ArrayList<>();
+        ArrayList<ExpandableNotificationRow> toRemove = new ArrayList<>();
         for (int i=0; i< mStackScroller.getChildCount(); i++) {
             View child = mStackScroller.getChildAt(i);
             if (!toShow.contains(child) && child instanceof ExpandableNotificationRow) {
-                toRemove.add(child);
+                toRemove.add((ExpandableNotificationRow) child);
             }
         }
 
-        for (View remove : toRemove) {
+        for (ExpandableNotificationRow remove : toRemove) {
+            if (mGroupManager.isChildInGroupWithSummary(remove.getStatusBarNotification())) {
+                // we are only transfering this notification to its parent, don't generate an animation
+                mStackScroller.setChildTransferInProgress(true);
+            }
             mStackScroller.removeView(remove);
+            mStackScroller.setChildTransferInProgress(false);
         }
         for (int i=0; i<toShow.size(); i++) {
             View v = toShow.get(i);
@@ -1561,6 +1579,10 @@
         mIconController.updateNotificationIcons(mNotificationData);
     }
 
+    public void requestNotificationUpdate() {
+        updateNotifications();
+    }
+
     @Override
     protected void updateRowStates() {
         super.updateRowStates();
@@ -4023,9 +4045,10 @@
     }
 
     @Override
-    public void onExpandClicked(View clickedView, boolean nowExpanded) {
+    public void onExpandClicked(Entry clickedEntry, boolean nowExpanded) {
+        mHeadsUpManager.setExpanded(clickedEntry, nowExpanded);
         if (mState == StatusBarState.KEYGUARD && nowExpanded) {
-            goToLockedShade(clickedView);
+            goToLockedShade(clickedEntry.row);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
index 813a167..902fd3d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
@@ -164,6 +164,9 @@
     public void panelScrimMinFractionChanged(float minFraction) {
         if (mMinFraction != minFraction) {
             mMinFraction = minFraction;
+            if (minFraction != 0.0f) {
+                mScrimController.animateNextChange();
+            }
             updateScrimFraction();
         }
     }
@@ -176,7 +179,7 @@
     }
 
     private void updateScrimFraction() {
-        float scrimFraction = Math.max(mPanelFraction - mMinFraction / (1.0f - mMinFraction), 0);
+        float scrimFraction = Math.max(mPanelFraction, mMinFraction);
         mScrimController.setPanelExpansion(scrimFraction);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index b9e9292..403199a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -52,8 +52,8 @@
     private static final float SCRIM_IN_FRONT_ALPHA = 0.75f;
     private static final int TAG_KEY_ANIM = R.id.scrim;
     private static final int TAG_KEY_ANIM_TARGET = R.id.scrim_target;
-    private static final int TAG_HUN_START_ALPHA = R.id.hun_scrim_alpha_start;
-    private static final int TAG_HUN_END_ALPHA = R.id.hun_scrim_alpha_end;
+    private static final int TAG_START_ALPHA = R.id.scrim_alpha_start;
+    private static final int TAG_END_ALPHA = R.id.scrim_alpha_end;
 
     private final ScrimView mScrimBehind;
     private final ScrimView mScrimInFront;
@@ -164,6 +164,10 @@
         scheduleUpdate();
     }
 
+    public void animateNextChange() {
+        mAnimateChange = true;
+    }
+
     public void setDozing(boolean dozing) {
         if (mDozing != dozing) {
             mDozing = dozing;
@@ -271,21 +275,7 @@
     }
 
     private void setScrimColor(View scrim, float alpha) {
-        ValueAnimator runningAnim = (ValueAnimator) scrim.getTag(TAG_KEY_ANIM);
-        Float target = (Float) scrim.getTag(TAG_KEY_ANIM_TARGET);
-        if (runningAnim != null && target != null) {
-            if (alpha != target) {
-                runningAnim.cancel();
-            } else {
-                return;
-            }
-        }
-        if (mAnimateChange) {
-            startScrimAnimation(scrim, alpha);
-        } else {
-            setCurrentScrimAlpha(scrim, alpha);
-            updateScrimColor(scrim);
-        }
+        updateScrim(mAnimateChange, scrim, alpha, getCurrentScrimAlpha(scrim));
     }
 
     private float getDozeAlpha(View scrim) {
@@ -428,41 +418,44 @@
     }
 
     private void updateHeadsUpScrim(boolean animate) {
-        float alpha = calculateHeadsUpAlpha();
-        ValueAnimator previousAnimator = StackStateAnimator.getChildTag(mHeadsUpScrim,
+        updateScrim(animate, mHeadsUpScrim, calculateHeadsUpAlpha(), mCurrentHeadsUpAlpha);
+    }
+
+    private void updateScrim(boolean animate, View scrim, float alpha, float currentAlpha) {
+        ValueAnimator previousAnimator = StackStateAnimator.getChildTag(scrim,
                 TAG_KEY_ANIM);
         float animEndValue = -1;
         if (previousAnimator != null) {
-            if (animate || alpha == mCurrentHeadsUpAlpha) {
+            if (animate || alpha == currentAlpha) {
                 previousAnimator.cancel();
             } else {
-                animEndValue = StackStateAnimator.getChildTag(mHeadsUpScrim, TAG_HUN_END_ALPHA);
+                animEndValue = StackStateAnimator.getChildTag(scrim, TAG_END_ALPHA);
             }
         }
-        if (alpha != mCurrentHeadsUpAlpha && alpha != animEndValue) {
+        if (alpha != currentAlpha && alpha != animEndValue) {
             if (animate) {
-                startScrimAnimation(mHeadsUpScrim, alpha);
-                mHeadsUpScrim.setTag(TAG_HUN_START_ALPHA, mCurrentHeadsUpAlpha);
-                mHeadsUpScrim.setTag(TAG_HUN_END_ALPHA, alpha);
+                startScrimAnimation(scrim, alpha);
+                scrim.setTag(TAG_START_ALPHA, currentAlpha);
+                scrim.setTag(TAG_END_ALPHA, alpha);
             } else {
                 if (previousAnimator != null) {
-                    float previousStartValue = StackStateAnimator.getChildTag(mHeadsUpScrim,
-                            TAG_HUN_START_ALPHA);
-                    float previousEndValue = StackStateAnimator.getChildTag(mHeadsUpScrim,
-                           TAG_HUN_END_ALPHA);
+                    float previousStartValue = StackStateAnimator.getChildTag(scrim,
+                            TAG_START_ALPHA);
+                    float previousEndValue = StackStateAnimator.getChildTag(scrim,
+                            TAG_END_ALPHA);
                     // we need to increase all animation keyframes of the previous animator by the
                     // relative change to the end value
                     PropertyValuesHolder[] values = previousAnimator.getValues();
                     float relativeDiff = alpha - previousEndValue;
                     float newStartValue = previousStartValue + relativeDiff;
                     values[0].setFloatValues(newStartValue, alpha);
-                    mHeadsUpScrim.setTag(TAG_HUN_START_ALPHA, newStartValue);
-                    mHeadsUpScrim.setTag(TAG_HUN_END_ALPHA, alpha);
+                    scrim.setTag(TAG_START_ALPHA, newStartValue);
+                    scrim.setTag(TAG_END_ALPHA, alpha);
                     previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
                 } else {
                     // update the alpha directly
-                    setCurrentScrimAlpha(mHeadsUpScrim, alpha);
-                    updateScrimColor(mHeadsUpScrim);
+                    setCurrentScrimAlpha(scrim, alpha);
+                    updateScrimColor(scrim);
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
index b16c98e..64fb066 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
@@ -20,6 +20,7 @@
 import android.animation.ValueAnimator;
 import android.content.Context;
 import android.content.res.ColorStateList;
+import android.content.res.Resources;
 import android.graphics.Color;
 import android.graphics.drawable.Icon;
 import android.os.Bundle;
@@ -28,6 +29,7 @@
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.ArraySet;
+import android.util.TypedValue;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.animation.AnimationUtils;
@@ -122,7 +124,10 @@
         notificationIconArea.addView(mNotificationIconAreaInner);
 
         mStatusIconsKeyguard = (LinearLayout) keyguardStatusBar.findViewById(R.id.statusIcons);
+
         mBatteryMeterView = (BatteryMeterView) statusBar.findViewById(R.id.battery);
+        maybeScaleBatteryMeterView(context);
+
         mClock = (TextView) statusBar.findViewById(R.id.clock);
         mLinearOutSlowIn = AnimationUtils.loadInterpolator(mContext,
                 android.R.interpolator.linear_out_slow_in);
@@ -136,6 +141,30 @@
         TunerService.get(mContext).addTunable(this, ICON_BLACKLIST);
     }
 
+    /**
+     * Looks up the scale factor for status bar icons and scales the battery view by that amount
+     * if appropriate.
+     */
+    private void maybeScaleBatteryMeterView(Context context) {
+        Resources res = context.getResources();
+        TypedValue typedValue = new TypedValue();
+
+        res.getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true);
+        float iconScaleFactor = typedValue.getFloat();
+
+        if (iconScaleFactor == 1.f) {
+            return;
+        }
+
+        float batteryHeight = res.getDimension(R.dimen.status_bar_battery_icon_height);
+        float batteryWidth = res.getDimension(R.dimen.status_bar_battery_icon_width);
+
+        LinearLayout.LayoutParams scaledLayoutParams = new LinearLayout.LayoutParams(
+                (int) (batteryWidth * iconScaleFactor), (int) (batteryHeight * iconScaleFactor));
+
+        mBatteryMeterView.setLayoutParams(scaledLayoutParams);
+    }
+
     @Override
     public void onTuningChanged(String key, String newValue) {
         if (!ICON_BLACKLIST.equals(key)) {
@@ -174,8 +203,12 @@
         boolean blocked = mIconBlacklist.contains(slot);
         StatusBarIconView view = new StatusBarIconView(mContext, slot, null, blocked);
         view.set(icon);
-        mStatusIcons.addView(view, viewIndex, new LinearLayout.LayoutParams(
-                ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize));
+
+        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
+                ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize);
+        lp.setMargins(mIconHPadding, 0, mIconHPadding, 0);
+        mStatusIcons.addView(view, viewIndex, lp);
+
         view = new StatusBarIconView(mContext, slot, null, blocked);
         view.set(icon);
         mStatusIconsKeyguard.addView(view, viewIndex, new LinearLayout.LayoutParams(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
index b38c3fe..dd7bd56 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
@@ -190,7 +190,6 @@
         if (alert) {
             HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(headsUp.key);
             headsUpEntry.updateEntry();
-            mGroupManager.onEntryHeadsUped(headsUp);
             setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(headsUp));
         }
     }
@@ -384,10 +383,17 @@
             for (HeadsUpEntry entry : mSortedEntries) {
                 ExpandableNotificationRow row = entry.entry.row;
                 if (row.isPinned()) {
+                    if (row.isChildInGroup()) {
+                        final ExpandableNotificationRow groupSummary
+                                = mGroupManager.getGroupSummary(row.getStatusBarNotification());
+                        if (groupSummary != null) {
+                            row = groupSummary;
+                        }
+                    }
                     row.getLocationOnScreen(mTmpTwoArray);
                     minX = mTmpTwoArray[0];
                     maxX = mTmpTwoArray[0] + row.getWidth();
-                    maxY = row.getHeadsUpHeight();
+                    maxY = row.getIntrinsicHeight();
                     break;
                 }
             }
@@ -448,6 +454,8 @@
         for (String key : mHeadsUpEntries.keySet()) {
             HeadsUpEntry entry = mHeadsUpEntries.get(key);
             setEntryPinned(entry, false /* isPinned */);
+            // maybe it got un sticky
+            entry.updateEntry(false /* updatePostTime */);
         }
     }
 
@@ -470,6 +478,10 @@
         mTrackingHeadsUp = trackingHeadsUp;
     }
 
+    public boolean isTrackingHeadsUp() {
+        return mTrackingHeadsUp;
+    }
+
     public void setIsExpanded(boolean isExpanded) {
         if (isExpanded != mIsExpanded) {
             mIsExpanded = isExpanded;
@@ -482,9 +494,25 @@
         }
     }
 
-    public int getTopHeadsUpHeight() {
+    /**
+     * @return the height of the top heads up notification when pinned. This is different from the
+     *         intrinsic height, which also includes whether the notification is system expanded and
+     *         is mainly used when dragging down from a heads up notification.
+     */
+    public int getTopHeadsUpPinnedHeight() {
         HeadsUpEntry topEntry = getTopEntry();
-        return topEntry != null ? topEntry.entry.row.getHeadsUpHeight() : 0;
+        if (topEntry == null || topEntry.entry == null) {
+            return 0;
+        }
+        ExpandableNotificationRow row = topEntry.entry.row;
+        if (row.isChildInGroup()) {
+            final ExpandableNotificationRow groupSummary
+                    = mGroupManager.getGroupSummary(row.getStatusBarNotification());
+            if (groupSummary != null) {
+                row = groupSummary;
+            }
+        }
+        return row.getPinnedHeadsUpHeight(true /* atLeastMinHeight */);
     }
 
     /**
@@ -558,6 +586,22 @@
     }
 
     /**
+     * Set an entry to be expanded and therefore stick in the heads up area if it's pinned
+     * until it's collapsed again.
+     */
+    public void setExpanded(NotificationData.Entry entry, boolean expanded) {
+        HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(entry.key);
+        if (headsUpEntry != null && headsUpEntry.expanded != expanded) {
+            headsUpEntry.expanded = expanded;
+            if (expanded) {
+                headsUpEntry.removeAutoRemovalCallbacks();
+            } else {
+                headsUpEntry.updateEntry(false /* updatePostTime */);
+            }
+        }
+    }
+
+    /**
      * This represents a notification and how long it is in a heads up mode. It also manages its
      * lifecycle automatically when created.
      */
@@ -567,6 +611,7 @@
         public long earliestRemovaltime;
         private Runnable mRemoveHeadsUpRunnable;
         public boolean remoteInputActive;
+        public boolean expanded;
 
         public void setEntry(final NotificationData.Entry entry) {
             this.entry = entry;
@@ -601,7 +646,7 @@
             if (mEntriesToRemoveAfterExpand.contains(entry)) {
                 mEntriesToRemoveAfterExpand.remove(entry);
             }
-            if (!hasFullScreenIntent(entry) && !mRemoteInputActive) {
+            if (!isSticky()) {
                 long finishTime = postTime + mHeadsUpNotificationDecay;
                 long removeDelay = Math.max(finishTime - currentTime, mMinimumDisplayTime);
                 mHandler.postDelayed(mRemoveHeadsUpRunnable, removeDelay);
@@ -609,6 +654,11 @@
             mSortedEntries.add(HeadsUpEntry.this);
         }
 
+        private boolean isSticky() {
+            return (entry.row.isPinned() && expanded)
+                    || remoteInputActive || hasFullScreenIntent(entry);
+        }
+
         @Override
         public int compareTo(HeadsUpEntry o) {
             boolean selfFullscreen = hasFullScreenIntent(entry);
@@ -641,6 +691,8 @@
             removeAutoRemovalCallbacks();
             entry = null;
             mRemoveHeadsUpRunnable = null;
+            expanded = false;
+            remoteInputActive = false;
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java
index 3a97be6..a3f571e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java
@@ -155,13 +155,7 @@
     }
 
     public int getInnerHeight() {
-        return mLayoutHeight - mTopPadding - getTopHeadsUpPushIn();
-    }
-
-    private int getTopHeadsUpPushIn() {
-        ExpandableNotificationRow topHeadsUpEntry = getTopHeadsUpEntry();
-        return topHeadsUpEntry != null ? topHeadsUpEntry.getHeadsUpHeight()
-                - topHeadsUpEntry.getMinHeight(): 0;
+        return mLayoutHeight - mTopPadding;
     }
 
     public boolean isShadeExpanded() {
@@ -180,11 +174,6 @@
         return mMaxHeadsUpTranslation;
     }
 
-    public ExpandableNotificationRow getTopHeadsUpEntry() {
-        HeadsUpManager.HeadsUpEntry topEntry = mHeadsUpManager.getTopEntry();
-        return topEntry == null ? null : topEntry.entry.row;
-    }
-
     public void setDismissAllInProgress(boolean dismissAllInProgress) {
         mDismissAllInProgress = dismissAllInProgress;
     }
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 baccd2c..dc40fb0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java
@@ -24,6 +24,7 @@
 
 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.HybridNotificationView;
 import com.android.systemui.statusbar.notification.HybridNotificationViewManager;
@@ -170,8 +171,15 @@
         mChildren.remove(row);
         removeView(row);
 
-        View divider = mDividers.remove(childIndex);
+        final View divider = mDividers.remove(childIndex);
         removeView(divider);
+        getOverlay().add(divider);
+        CrossFadeHelper.fadeOut(divider, new Runnable() {
+            @Override
+            public void run() {
+                getOverlay().remove(divider);
+            }
+        });
 
         row.setSystemChildExpanded(false);
         updateGroupOverflow();
@@ -403,31 +411,20 @@
     }
 
     public void startAnimationToState(StackScrollState state, StackStateAnimator stateAnimator,
-            boolean withDelays, long baseDelay, long duration) {
+            long baseDelay, long duration) {
         int childCount = mChildren.size();
         ViewState tmpState = new ViewState();
-        int delayIndex = 0;
-        int maxAllowChildCount = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
         for (int i = childCount - 1; i >= 0; i--) {
             ExpandableNotificationRow child = mChildren.get(i);
             StackViewState viewState = state.getViewStateForView(child);
-            int difference = Math.min(StackStateAnimator.DELAY_EFFECT_MAX_INDEX_DIFFERENCE_CHILDREN,
-                    delayIndex);
-            long delay = withDelays
-                    ? difference * StackStateAnimator.ANIMATION_DELAY_PER_ELEMENT_EXPAND_CHILDREN
-                    : 0;
-            delay = (long) (delay * (mChildrenExpanded ? 1.0f : 0.5f) + baseDelay);
-            stateAnimator.startStackAnimations(child, viewState, state, -1, delay);
+            stateAnimator.startStackAnimations(child, viewState, state, -1, baseDelay);
 
             // layout the divider
             View divider = mDividers.get(i);
             tmpState.initFrom(divider);
             tmpState.yTranslation = viewState.yTranslation - mDividerHeight;
             tmpState.alpha = mChildrenExpanded && viewState.alpha != 0 ? 0.5f : 0;
-            stateAnimator.startViewAnimations(divider, tmpState, delay, duration);
-            if (i < maxAllowChildCount) {
-                delayIndex++;
-            }
+            stateAnimator.startViewAnimations(divider, tmpState, baseDelay, duration);
         }
         if (mGroupOverflowContainer != null) {
             stateAnimator.startViewAnimations(mGroupOverflowContainer, mGroupOverFlowState,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
index 0ed1527..36b2810 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
@@ -138,6 +138,7 @@
     private final StackStateAnimator mStateAnimator = new StackStateAnimator(this);
     private boolean mAnimationsEnabled;
     private boolean mChangePositionInProgress;
+    private boolean mChildTransferInProgress;
 
     /**
      * The raw amount of the overScroll on the top, which is not rubber-banded.
@@ -363,14 +364,14 @@
         updateContentHeight();
         clampScrollPosition();
         if (mRequestViewResizeAnimationOnLayout) {
-            requestAnimationOnViewResize();
+            requestAnimationOnViewResize(null);
             mRequestViewResizeAnimationOnLayout = false;
         }
         requestChildrenUpdate();
     }
 
-    private void requestAnimationOnViewResize() {
-        if (mIsExpanded && mAnimationsEnabled) {
+    private void requestAnimationOnViewResize(ExpandableNotificationRow row) {
+        if (mAnimationsEnabled && (mIsExpanded || row != null && row.isPinned())) {
             mNeedViewResizeAnimation = true;
             mNeedsAnimation = true;
         }
@@ -495,27 +496,28 @@
         int stackHeight;
         float paddingOffset;
         boolean trackingHeadsUp = mTrackingHeadsUp || mHeadsUpManager.hasPinnedHeadsUp();
-        int normalUnfoldPositionStart = trackingHeadsUp ? mHeadsUpManager.getTopHeadsUpHeight()
+        int normalUnfoldPositionStart = trackingHeadsUp
+                ? mHeadsUpManager.getTopHeadsUpPinnedHeight()
                 : minStackHeight;
         if (newStackHeight - mTopPadding - mTopPaddingOverflow >= normalUnfoldPositionStart
                 || getNotGoneChildCount() == 0) {
             paddingOffset = mTopPaddingOverflow;
             stackHeight = newStackHeight;
         } else {
-
-            // We did not reach the position yet where we actually start growing,
-            // so we translate the stack upwards.
-            int translationY = (newStackHeight - minStackHeight);
-            // A slight parallax effect is introduced in order for the stack to catch up with
-            // the top card.
-            float partiallyThere = (newStackHeight - mTopPadding - mTopPaddingOverflow)
-                    / minStackHeight;
-            partiallyThere = Math.max(0, partiallyThere);
+            int translationY;
             if (!trackingHeadsUp) {
+                // We did not reach the position yet where we actually start growing,
+                // so we translate the stack upwards.
+                translationY = (newStackHeight - minStackHeight);
+                // A slight parallax effect is introduced in order for the stack to catch up with
+                // the top card.
+                float partiallyThere = (newStackHeight - mTopPadding - mTopPaddingOverflow)
+                        / minStackHeight;
+                partiallyThere = Math.max(0, partiallyThere);
                 translationY += (1 - partiallyThere) * (mBottomStackPeekSize +
                         mCollapseSecondCardPadding);
             } else {
-                translationY = (int) (height - mHeadsUpManager.getTopHeadsUpHeight());
+                translationY = (int) (height - normalUnfoldPositionStart);
             }
             paddingOffset = translationY - mTopPadding;
             stackHeight = (int) (height - (translationY - mTopPadding));
@@ -729,7 +731,10 @@
                 if (slidingChild instanceof ExpandableNotificationRow) {
                     ExpandableNotificationRow row = (ExpandableNotificationRow) slidingChild;
                     if (!mIsExpanded && row.isHeadsUp() && row.isPinned()
-                            && mHeadsUpManager.getTopEntry().entry.row != row) {
+                            && mHeadsUpManager.getTopEntry().entry.row != row
+                            && mGroupManager.getGroupSummary(
+                                mHeadsUpManager.getTopEntry().entry.row.getStatusBarNotification())
+                                != row) {
                         continue;
                     }
                     return row.getViewAtPosition(touchY - childTop);
@@ -1506,7 +1511,7 @@
 
     public int getMinStackHeight() {
         final ExpandableView firstChild = getFirstChildNotGone();
-        final int firstChildMinHeight = firstChild != null ? (int) firstChild.getMinHeight()
+        final int firstChildMinHeight = firstChild != null ? firstChild.getMinHeight()
                 : mCollapsedSize;
         return firstChildMinHeight + mBottomStackPeekSize + mCollapseSecondCardPadding;
     }
@@ -1628,12 +1633,16 @@
         }
     }
 
+    public void setChildTransferInProgress(boolean childTransferInProgress) {
+        mChildTransferInProgress = childTransferInProgress;
+    }
+
     @Override
     public void onViewRemoved(View child) {
         super.onViewRemoved(child);
         // we only call our internal methods if this is actually a removal and not just a
         // notification which becomes a child notification
-        if (!isChildInGroup(child)) {
+        if (!mChildTransferInProgress) {
             onViewRemovedInternal(child);
         }
     }
@@ -2337,7 +2346,10 @@
         clampScrollPosition();
         notifyHeightChangeListener(view);
         if (needsAnimation) {
-            requestAnimationOnViewResize();
+            ExpandableNotificationRow row = view instanceof ExpandableNotificationRow
+                    ? (ExpandableNotificationRow) view
+                    : null;
+            requestAnimationOnViewResize(row);
         }
         requestChildrenUpdate();
     }
@@ -2801,7 +2813,7 @@
 
     @Override
     public void onGroupExpansionChanged(ExpandableNotificationRow changedRow, boolean expanded) {
-        boolean animated = mAnimationsEnabled && mIsExpanded;
+        boolean animated = mAnimationsEnabled && (mIsExpanded || changedRow.isPinned());
         if (animated) {
             mExpandedGroupView = changedRow;
             mNeedsAnimation = true;
@@ -2812,13 +2824,12 @@
 
     @Override
     public void onGroupCreatedFromChildren(NotificationGroupManager.NotificationGroup group) {
-        for (NotificationData.Entry entry : group.children) {
-            ExpandableNotificationRow row = entry.row;
-            if (indexOfChild(row) != -1) {
-                removeView(row);
-                group.summary.row.addChildNotification(row);
-            }
-        }
+        mPhoneStatusBar.requestNotificationUpdate();
+    }
+
+    @Override
+    public void onChildIsolationChanged() {
+        mPhoneStatusBar.requestNotificationUpdate();
     }
 
     public void generateChildOrderChangedEvent() {
@@ -3113,7 +3124,7 @@
                 StackStateAnimator.ANIMATION_DURATION_STANDARD,
 
                 // ANIMATION_TYPE_GROUP_EXPANSION_CHANGED
-                StackStateAnimator.ANIMATION_DURATION_EXPAND_CLICKED,
+                StackStateAnimator.ANIMATION_DURATION_STANDARD,
 
                 // ANIMATION_TYPE_HEADS_UP_APPEAR
                 StackStateAnimator.ANIMATION_DURATION_HEADS_UP_APPEAR,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
index 5496963..822012d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
@@ -410,10 +410,6 @@
         // How far in is the element currently transitioning into the bottom stack.
         float yPositionInScrollView = 0.0f;
 
-        // If we have a heads-up higher than the collapsed height we need to add the difference to
-        // the padding of all other elements, i.e push in the top stack slightly.
-        ExpandableNotificationRow topHeadsUpEntry = ambientState.getTopHeadsUpEntry();
-
         int childCount = algorithmState.visibleChildren.size();
         int numberOfElementsCompletelyIn = algorithmState.partialInTop == 1.0f
                 ? algorithmState.lastTopStackIndex
@@ -422,7 +418,7 @@
             ExpandableView child = algorithmState.visibleChildren.get(i);
             StackViewState childViewState = resultState.getViewStateForView(child);
             childViewState.location = StackViewState.LOCATION_UNKNOWN;
-            int childHeight = getMaxAllowedChildHeight(child, ambientState);
+            int childHeight = getMaxAllowedChildHeight(child);
             int minHeight = child.getMinHeight();
             float yPositionInScrollViewAfterElement = yPositionInScrollView
                     + childHeight
@@ -504,11 +500,6 @@
             currentYPosition = childViewState.yTranslation + childHeight + mPaddingBetweenElements;
             yPositionInScrollView = yPositionInScrollViewAfterElement;
 
-            if (ambientState.isShadeExpanded() && topHeadsUpEntry != null
-                    && child != topHeadsUpEntry) {
-                childViewState.yTranslation += topHeadsUpEntry.getHeadsUpHeight() -
-                        mFirstChildMinHeight;
-            }
             childViewState.yTranslation += ambientState.getTopPadding()
                     + ambientState.getStackTranslation();
         }
@@ -533,10 +524,6 @@
             StackViewState childState = resultState.getViewStateForView(row);
             boolean isTopEntry = topHeadsUpEntry == row;
             if (mIsExpanded) {
-                if (isTopEntry) {
-                    childState.height += row.getHeadsUpHeight() - mFirstChildMinHeight;
-                }
-                childState.height = Math.max(childState.height, row.getHeadsUpHeight());
                 // Ensure that the heads up is always visible even when scrolled off from the bottom
                 float bottomPosition = ambientState.getMaxHeadsUpTranslation() - childState.height;
                 childState.yTranslation = Math.min(childState.yTranslation,
@@ -545,12 +532,12 @@
             if (row.isPinned()) {
                 childState.yTranslation = Math.max(childState.yTranslation,
                         mNotificationsTopPadding);
-                childState.height = Math.max(row.getHeadsUpHeight(), childState.height);
+                childState.height = Math.max(row.getIntrinsicHeight(), childState.height);
                 if (!isTopEntry) {
                     // Ensure that a headsUp doesn't vertically extend further than the heads-up at
                     // the top most z-position
                     StackViewState topState = resultState.getViewStateForView(topHeadsUpEntry);
-                    childState.height = row.getHeadsUpHeight();
+                    childState.height = row.getIntrinsicHeight();
                     childState.yTranslation = topState.yTranslation + topState.height
                             - childState.height;
                 }
@@ -608,16 +595,8 @@
                 mFirstChildMinHeight - childHeight);
     }
 
-    private int getMaxAllowedChildHeight(View child, AmbientState ambientState) {
-        if (child instanceof ExpandableNotificationRow) {
-            ExpandableNotificationRow row = (ExpandableNotificationRow) child;
-            if (ambientState == null && row.isHeadsUp()
-                    || ambientState != null && ambientState.getTopHeadsUpEntry() == child) {
-                int extraSize = row.getIntrinsicHeight() - row.getHeadsUpHeight();
-                return mFirstChildMinHeight + extraSize;
-            }
-            return row.getIntrinsicHeight();
-        } else if (child instanceof ExpandableView) {
+    private int getMaxAllowedChildHeight(View child) {
+        if (child instanceof ExpandableView) {
             ExpandableView expandableView = (ExpandableView) child;
             return expandableView.getIntrinsicHeight();
         }
@@ -744,7 +723,7 @@
         for (int i = 0; i < childCount; i++) {
             ExpandableView child = algorithmState.visibleChildren.get(i);
             StackViewState childViewState = resultState.getViewStateForView(child);
-            int childHeight = getMaxAllowedChildHeight(child, ambientState);
+            int childHeight = getMaxAllowedChildHeight(child);
             float yPositionInScrollViewAfterElement = yPositionInScrollView
                     + childHeight
                     + mPaddingBetweenElements;
@@ -865,13 +844,6 @@
                 // current height or the end value of the animation.
                 mFirstChildMaxHeight = StackStateAnimator.getFinalActualHeight(
                         mFirstChildWhileExpanding);
-                if (mFirstChildWhileExpanding instanceof ExpandableNotificationRow) {
-                    ExpandableNotificationRow row =
-                            (ExpandableNotificationRow) mFirstChildWhileExpanding;
-                    if (row.isHeadsUp()) {
-                        mFirstChildMaxHeight += mFirstChildMinHeight - row.getHeadsUpHeight();
-                    }
-                }
             } else {
                 updateFirstChildMaxSizeToMaxHeight();
             }
@@ -893,7 +865,7 @@
                                 int oldBottom) {
                             if (mFirstChildWhileExpanding != null) {
                                 mFirstChildMaxHeight = getMaxAllowedChildHeight(
-                                        mFirstChildWhileExpanding, null);
+                                        mFirstChildWhileExpanding);
                             } else {
                                 mFirstChildMaxHeight = 0;
                             }
@@ -901,7 +873,7 @@
                         }
                     });
         } else {
-            mFirstChildMaxHeight = getMaxAllowedChildHeight(mFirstChildWhileExpanding, null);
+            mFirstChildMaxHeight = getMaxAllowedChildHeight(mFirstChildWhileExpanding);
         }
     }
 
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 b4ab48a..9d5e072 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java
@@ -43,17 +43,14 @@
     public static final int ANIMATION_DURATION_STANDARD = 360;
     public static final int ANIMATION_DURATION_GO_TO_FULL_SHADE = 448;
     public static final int ANIMATION_DURATION_APPEAR_DISAPPEAR = 464;
-    public static final int ANIMATION_DURATION_EXPAND_CLICKED = 360;
     public static final int ANIMATION_DURATION_DIMMED_ACTIVATED = 220;
     public static final int ANIMATION_DURATION_HEADS_UP_APPEAR = 650;
     public static final int ANIMATION_DURATION_HEADS_UP_DISAPPEAR = 230;
     public static final int ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING = 80;
-    public static final int ANIMATION_DELAY_PER_ELEMENT_EXPAND_CHILDREN = 54;
     public static final int ANIMATION_DELAY_PER_ELEMENT_MANUAL = 32;
     public static final int ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE = 48;
     public static final int ANIMATION_DELAY_PER_ELEMENT_DARK = 24;
     public static final int DELAY_EFFECT_MAX_INDEX_DIFFERENCE = 2;
-    public static final int DELAY_EFFECT_MAX_INDEX_DIFFERENCE_CHILDREN = 3;
     public static final int ANIMATION_DELAY_HEADS_UP = 120;
 
     private static final int TAG_ANIMATOR_TRANSLATION_Y = R.id.translation_y_animator_tag;
@@ -263,8 +260,7 @@
                     delay + duration);
         } else if (child instanceof ExpandableNotificationRow) {
             ExpandableNotificationRow row = (ExpandableNotificationRow) child;
-            row.startChildAnimation(finalState, this, child == mChildExpandingView, delay,
-                    duration);
+            row.startChildAnimation(finalState, this, delay, duration);
         }
     }
 
@@ -898,7 +894,7 @@
                     event.animationType == NotificationStackScrollLayout
                             .AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK) {
                 mHeadsUpDisappearChildren.add(changingView);
-                if (mHostLayout.indexOfChild(changingView) == -1) {
+                if (changingView.getParent() == null) {
                     // This notification was actually removed, so we need to add it to the overlay
                     mHostLayout.getOverlay().add(changingView);
                     mTmpState.initFrom(changingView);
diff --git a/packages/SystemUI/src/com/android/systemui/tv/pip/PipManager.java b/packages/SystemUI/src/com/android/systemui/tv/pip/PipManager.java
index 3d91d62..bed1e9e 100644
--- a/packages/SystemUI/src/com/android/systemui/tv/pip/PipManager.java
+++ b/packages/SystemUI/src/com/android/systemui/tv/pip/PipManager.java
@@ -84,9 +84,8 @@
                 return;
             }
             if (DEBUG) Log.d(TAG, "PINNED_STACK:" + stackInfo);
-            mState = STATE_PIP_OVERLAY;
             mPipTaskId = stackInfo.taskIds[stackInfo.taskIds.length - 1];
-            launchPipOverlayActivity();
+            showPipOverlay(false);
         }
     };
     private final Runnable mOnTaskStackChanged = new Runnable() {
@@ -208,17 +207,17 @@
      * stack to the default PIP bound {@link com.android.internal.R.string
      * .config_defaultPictureInPictureBounds}.
      */
-    public void showPipOverlay() {
+    public void showPipOverlay(boolean resizeStack) {
         if (DEBUG) Log.d(TAG, "showPipOverlay()");
-        try {
-            mActivityManager.resizeStack(PINNED_STACK_ID, mPipBound, false);
-        } catch (Exception e) {
-            Log.e(TAG, "resizeStack failed", e);
-            closePip();
-            return;
-        }
         mState = STATE_PIP_OVERLAY;
-        launchPipOverlayActivity();
+        Intent intent = new Intent(mContext, PipOverlayActivity.class);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        final ActivityOptions options = ActivityOptions.makeBasic();
+        options.setLaunchStackId(PINNED_STACK_ID);
+        if (resizeStack) {
+            options.setLaunchBounds(mPipBound);
+        }
+        mContext.startActivity(intent, options.toBundle());
     }
 
     /**
@@ -228,13 +227,6 @@
      */
     public void showPipMenu() {
         if (DEBUG) Log.d(TAG, "showPipMenu()");
-        try {
-            mActivityManager.resizeStack(PINNED_STACK_ID, mMenuModePipBound, false);
-        } catch (Exception e) {
-            Log.e(TAG, "resizeStack failed", e);
-            closePip();
-            return;
-        }
         mState = STATE_PIP_MENU;
         for (int i = mListeners.size() - 1; i >= 0; --i) {
             mListeners.get(i).onShowPipMenu();
@@ -243,6 +235,7 @@
         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         final ActivityOptions options = ActivityOptions.makeBasic();
         options.setLaunchStackId(PINNED_STACK_ID);
+        options.setLaunchBounds(mMenuModePipBound);
         mContext.startActivity(intent, options.toBundle());
     }
 
@@ -260,14 +253,6 @@
         mListeners.remove(listener);
     }
 
-    private void launchPipOverlayActivity() {
-        Intent intent = new Intent(mContext, PipOverlayActivity.class);
-        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        final ActivityOptions options = ActivityOptions.makeBasic();
-        options.setLaunchStackId(PINNED_STACK_ID);
-        mContext.startActivity(intent, options.toBundle());
-    }
-
     private boolean hasPipTasks() {
         try {
             StackInfo stackInfo = mActivityManager.getStackInfo(PINNED_STACK_ID);
diff --git a/packages/SystemUI/src/com/android/systemui/tv/pip/PipMenuActivity.java b/packages/SystemUI/src/com/android/systemui/tv/pip/PipMenuActivity.java
index 1248321..97c70ed 100644
--- a/packages/SystemUI/src/com/android/systemui/tv/pip/PipMenuActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/tv/pip/PipMenuActivity.java
@@ -55,7 +55,7 @@
         findViewById(R.id.cancel).setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
-                mPipManager.showPipOverlay();
+                mPipManager.showPipOverlay(true);
                 finish();
             }
         });
@@ -69,7 +69,7 @@
 
     @Override
     public void onBackPressed() {
-        mPipManager.showPipOverlay();
+        mPipManager.showPipOverlay(true);
         finish();
     }
 
diff --git a/services/backup/java/com/android/server/backup/BackupManagerService.java b/services/backup/java/com/android/server/backup/BackupManagerService.java
index 234c94e..d63dd0c 100644
--- a/services/backup/java/com/android/server/backup/BackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/BackupManagerService.java
@@ -676,8 +676,13 @@
         return true;
     }
 
+    // Checks if the app is in a stopped state, that means it won't receive broadcasts.
+    private static boolean appIsStopped(ApplicationInfo app) {
+        return ((app.flags & ApplicationInfo.FLAG_STOPPED) != 0);
+    }
+
     /* does *not* check overall backup eligibility policy! */
-    public static boolean appGetsFullBackup(PackageInfo pkg) {
+    private static boolean appGetsFullBackup(PackageInfo pkg) {
         if (pkg.applicationInfo.backupAgentName != null) {
             // If it has an agent, it gets full backups only if it says so
             return (pkg.applicationInfo.flags & ApplicationInfo.FLAG_FULL_BACKUP_ONLY) != 0;
@@ -2758,7 +2763,7 @@
                     return;
                 }
 
-                if ((mCurrentPackage.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0) {
+                if (appIsStopped(mCurrentPackage.applicationInfo)) {
                     // The app has been force-stopped or cleared or just installed,
                     // and not yet launched out of that state, so just as it won't
                     // receive broadcasts, we won't run it for backup.
@@ -4179,28 +4184,18 @@
                 try {
                     PackageInfo info = mPackageManager.getPackageInfo(pkg,
                             PackageManager.GET_SIGNATURES);
-                    if ((info.applicationInfo.flags & ApplicationInfo.FLAG_ALLOW_BACKUP) == 0
-                            || pkg.equals(SHARED_BACKUP_AGENT_PACKAGE)) {
+                    if (!appIsEligibleForBackup(info.applicationInfo)) {
                         // Cull any packages that have indicated that backups are not permitted,
+                        // that run as system-domain uids but do not define their own backup agents,
                         // as well as any explicit mention of the 'special' shared-storage agent
                         // package (we handle that one at the end).
                         if (MORE_DEBUG) {
-                            Slog.d(TAG, "Ignoring opted-out package " + pkg);
+                            Slog.d(TAG, "Ignoring not eligible package " + pkg);
                         }
                         sendBackupOnResult(mBackupObserver, pkg,
-                                BackupManager.ERROR_BACKUP_NOT_ALLOWED);
+                            BackupManager.ERROR_BACKUP_NOT_ALLOWED);
                         continue;
-                    } else if ((info.applicationInfo.uid < Process.FIRST_APPLICATION_UID)
-                            && (info.applicationInfo.backupAgentName == null)) {
-                        // Cull any packages that run as system-domain uids but do not define their
-                        // own backup agents
-                        if (MORE_DEBUG) {
-                            Slog.d(TAG, "Ignoring non-agent system package " + pkg);
-                        }
-                        sendBackupOnResult(mBackupObserver, pkg,
-                                BackupManager.ERROR_BACKUP_NOT_ALLOWED);
-                        continue;
-                    } else if ((info.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0) {
+                    } else if (appIsStopped(info.applicationInfo)) {
                         // Cull any packages in the 'stopped' state: they've either just been
                         // installed or have explicitly been force-stopped by the user.  In both
                         // cases we do not want to launch them for backup.
@@ -9538,6 +9533,32 @@
         }
     }
 
+    public boolean isAppEligibleForBackup(String packageName) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
+                "isAppEligibleForBackup");
+        try {
+            PackageInfo packageInfo = mPackageManager.getPackageInfo(packageName,
+                    PackageManager.GET_SIGNATURES);
+            if (!appIsEligibleForBackup(packageInfo.applicationInfo) ||
+                    appIsStopped(packageInfo.applicationInfo)) {
+                return false;
+            }
+            IBackupTransport transport = getTransport(mCurrentTransport);
+            if (transport != null) {
+                try {
+                    return transport.isAppEligibleForBackup(packageInfo,
+                        appGetsFullBackup(packageInfo));
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Unable to contact transport");
+                }
+            }
+            // If transport is not present we couldn't tell that the package is not eligible.
+            return true;
+        } catch (NameNotFoundException e) {
+            return false;
+        }
+    }
+
     // ----- Restore session -----
 
     class ActiveRestoreSession extends IRestoreSession.Stub {
diff --git a/services/backup/java/com/android/server/backup/Trampoline.java b/services/backup/java/com/android/server/backup/Trampoline.java
index 505a1a5..bbf881b 100644
--- a/services/backup/java/com/android/server/backup/Trampoline.java
+++ b/services/backup/java/com/android/server/backup/Trampoline.java
@@ -326,6 +326,12 @@
     }
 
     @Override
+    public boolean isAppEligibleForBackup(String packageName) {
+        BackupManagerService svc = mService;
+        return (svc != null) ? svc.isAppEligibleForBackup(packageName) : false;
+    }
+
+    @Override
     public int requestBackup(String[] packages, IBackupObserver observer) throws RemoteException {
         BackupManagerService svc = mService;
         return (svc != null) ? svc.requestBackup(packages, observer) : null;
diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java
index e3f4999..10b4ba7 100644
--- a/services/core/java/com/android/server/am/ActivityStack.java
+++ b/services/core/java/com/android/server/am/ActivityStack.java
@@ -4243,6 +4243,14 @@
 
     private int getTaskConfigurationChanges(ActivityRecord record, Configuration taskConfig,
             Configuration oldTaskOverride) {
+
+        // If we went from full-screen to non-full-screen, make sure to use the correct
+        // configuration task diff, so the diff stays as small as possible.
+        if (Configuration.EMPTY.equals(oldTaskOverride)
+                && !Configuration.EMPTY.equals(taskConfig)) {
+            oldTaskOverride = record.task.extractOverrideConfig(record.configuration);
+        }
+
         // Determine what has changed.  May be nothing, if this is a config
         // that has come back from the app after going idle.  In that case
         // we just want to leave the official config object now in the
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index d9dd77d..1fc674b 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -2251,7 +2251,10 @@
             // If we didn't actual do a relaunch (indicated by kept==true meaning we kept the old
             // window), we need to clear the replace window settings. Otherwise, we schedule a
             // timeout to remove the old window if the replacing window is not coming in time.
-            mWindowManager.scheduleClearReplacingWindowIfNeeded(topActivity.appToken, !kept);
+            // In case of the pinned stack we don't resize the task during the move, but we will
+            // resize the stack soon after so we want to retain the replacing window.
+            mWindowManager.scheduleClearReplacingWindowIfNeeded(topActivity.appToken,
+                    !kept || stackId == PINNED_STACK_ID);
         }
 
         // The task might have already been running and its visibility needs to be synchronized with
diff --git a/services/core/java/com/android/server/am/TaskRecord.java b/services/core/java/com/android/server/am/TaskRecord.java
index cc86a62..4ce8b2f 100644
--- a/services/core/java/com/android/server/am/TaskRecord.java
+++ b/services/core/java/com/android/server/am/TaskRecord.java
@@ -1291,24 +1291,7 @@
             if (stack == null || StackId.persistTaskBounds(stack.mStackId)) {
                 mLastNonFullscreenBounds = mBounds;
             }
-
-            final Configuration serviceConfig = mService.mConfiguration;
-            mOverrideConfig = new Configuration(Configuration.EMPTY);
-            // TODO(multidisplay): Update Dp to that of display stack is on.
-            final float density = serviceConfig.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
-            mOverrideConfig.screenWidthDp =
-                    Math.min((int)(mBounds.width() / density), serviceConfig.screenWidthDp);
-            mOverrideConfig.screenHeightDp =
-                    Math.min((int)(mBounds.height() / density), serviceConfig.screenHeightDp);
-            mOverrideConfig.smallestScreenWidthDp =
-                    Math.min(mOverrideConfig.screenWidthDp, mOverrideConfig.screenHeightDp);
-            mOverrideConfig.orientation =
-                    (mOverrideConfig.screenWidthDp <= mOverrideConfig.screenHeightDp)
-                            ? Configuration.ORIENTATION_PORTRAIT
-                            : Configuration.ORIENTATION_LANDSCAPE;
-            final int sl = Configuration.resetScreenLayout(serviceConfig.screenLayout);
-            mOverrideConfig.screenLayout = Configuration.reduceScreenLayout(
-                    sl, mOverrideConfig.screenWidthDp, mOverrideConfig.screenHeightDp);
+            mOverrideConfig = calculateOverrideConfig(mBounds);
         }
 
         if (mFullscreen != oldFullscreen) {
@@ -1318,6 +1301,41 @@
         return !mOverrideConfig.equals(oldConfig) ? mOverrideConfig : null;
     }
 
+    Configuration calculateOverrideConfig(Rect bounds) {
+        final Configuration serviceConfig = mService.mConfiguration;
+        final Configuration config = new Configuration(Configuration.EMPTY);
+        // TODO(multidisplay): Update Dp to that of display stack is on.
+        final float density = serviceConfig.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
+        config.screenWidthDp =
+                Math.min((int)(bounds.width() / density), serviceConfig.screenWidthDp);
+        config.screenHeightDp =
+                Math.min((int)(bounds.height() / density), serviceConfig.screenHeightDp);
+        config.smallestScreenWidthDp =
+                Math.min(config.screenWidthDp, config.screenHeightDp);
+        config.orientation = (config.screenWidthDp <= config.screenHeightDp)
+                ? Configuration.ORIENTATION_PORTRAIT
+                : Configuration.ORIENTATION_LANDSCAPE;
+        final int sl = Configuration.resetScreenLayout(serviceConfig.screenLayout);
+        config.screenLayout = Configuration.reduceScreenLayout(
+                sl, config.screenWidthDp, config.screenHeightDp);
+        return config;
+    }
+
+    /**
+     * Using the existing configuration {@param config}, creates a new task override config such
+     * that all the fields that are usually set in an override config are set to the ones in
+     * {@param config}.
+     */
+    Configuration extractOverrideConfig(Configuration config) {
+        final Configuration extracted = new Configuration(Configuration.EMPTY);
+        extracted.screenWidthDp = config.screenWidthDp;
+        extracted.screenHeightDp = config.screenHeightDp;
+        extracted.smallestScreenWidthDp = config.smallestScreenWidthDp;
+        extracted.orientation = config.orientation;
+        extracted.screenLayout = config.screenLayout;
+        return extracted;
+    }
+
     Rect updateOverrideConfigurationFromLaunchBounds() {
         final Rect bounds = validateBounds(getLaunchBounds());
         updateOverrideConfiguration(bounds);
diff --git a/services/core/java/com/android/server/pm/PackageDexOptimizer.java b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
index 482217c..bce7223 100644
--- a/services/core/java/com/android/server/pm/PackageDexOptimizer.java
+++ b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
@@ -82,7 +82,7 @@
      * {@link PackageManagerService#mInstallLock}.
      */
     int performDexOpt(PackageParser.Package pkg, String[] instructionSets,
-            boolean inclDependencies, String volumeUuid, boolean useProfiles, boolean extractOnly) {
+            boolean inclDependencies, boolean useProfiles, boolean extractOnly) {
         ArraySet<String> done;
         if (inclDependencies && (pkg.usesLibraries != null || pkg.usesOptionalLibraries != null)) {
             done = new ArraySet<String>();
@@ -97,7 +97,7 @@
                 mDexoptWakeLock.acquire();
             }
             try {
-                return performDexOptLI(pkg, instructionSets, done, volumeUuid, useProfiles,
+                return performDexOptLI(pkg, instructionSets, done, useProfiles,
                         extractOnly);
             } finally {
                 if (useLock) {
@@ -108,7 +108,7 @@
     }
 
     private int performDexOptLI(PackageParser.Package pkg, String[] targetInstructionSets,
-            ArraySet<String> done, String volumeUuid, boolean useProfiles, boolean extractOnly) {
+            ArraySet<String> done, boolean useProfiles, boolean extractOnly) {
         final String[] instructionSets = targetInstructionSets != null ?
                 targetInstructionSets : getAppDexInstructionSets(pkg.applicationInfo);
 
@@ -185,7 +185,7 @@
                 try {
                     mPackageManagerService.mInstaller.dexopt(path, sharedGid,
                             pkg.packageName, dexCodeInstructionSet, dexoptNeeded, oatDir,
-                            dexFlags, volumeUuid, useProfiles);
+                            dexFlags, pkg.volumeUuid, useProfiles);
                     performedDexOpt = true;
                 } catch (InstallerException e) {
                     Slog.w(TAG, "Failed to dexopt", e);
@@ -252,8 +252,7 @@
             if (libPkg != null && !done.contains(libName)) {
                 // TODO: Analyze and investigate if we (should) profile libraries.
                 // Currently this will do a full compilation of the library.
-                performDexOptLI(libPkg, instructionSets, done,
-                        StorageManager.UUID_PRIVATE_INTERNAL, /*useProfiles*/ false,
+                performDexOptLI(libPkg, instructionSets, done, /*useProfiles*/ false,
                         /* extractOnly */ false);
             }
         }
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 9d17f8f..f05e45f 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -6717,7 +6717,7 @@
             synchronized (mInstallLock) {
                 final String[] instructionSets = new String[] { targetInstructionSet };
                 int result = mPackageDexOptimizer.performDexOpt(p, instructionSets,
-                        true /* inclDependencies */, p.volumeUuid, useProfiles, extractOnly);
+                        true /* inclDependencies */, useProfiles, extractOnly);
                 return result == PackageDexOptimizer.DEX_OPT_PERFORMED;
             }
         } finally {
@@ -6762,7 +6762,7 @@
             // Whoever is calling forceDexOpt wants a fully compiled package.
             // Don't use profiles since that may cause compilation to be skipped.
             final int res = mPackageDexOptimizer.performDexOpt(pkg, instructionSets,
-                    true /* inclDependencies */, pkg.volumeUuid, false /* useProfiles */,
+                    true /* inclDependencies */, false /* useProfiles */,
                     false /* extractOnly */);
 
             Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
@@ -13024,7 +13024,7 @@
                 // Do not run PackageDexOptimizer through the local performDexOpt
                 // method because `pkg` is not in `mPackages` yet.
                 int result = mPackageDexOptimizer.performDexOpt(pkg, null /* instructionSets */,
-                        false /* inclDependencies */, volumeUuid, false /* useProfiles */,
+                        false /* inclDependencies */, false /* useProfiles */,
                         true /* extractOnly */);
                 Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
                 if (result == PackageDexOptimizer.DEX_OPT_FAILED) {
diff --git a/services/core/java/com/android/server/wm/AppWindowToken.java b/services/core/java/com/android/server/wm/AppWindowToken.java
index 785f166..2732821 100644
--- a/services/core/java/com/android/server/wm/AppWindowToken.java
+++ b/services/core/java/com/android/server/wm/AppWindowToken.java
@@ -302,6 +302,13 @@
         }
     }
 
+    void markSurfacesExiting() {
+        for (int i = allAppWindows.size() - 1; i >= 0; i--) {
+            WindowState win = allAppWindows.get(i);
+            win.mExiting = true;
+        }
+    }
+
     /**
      * Checks whether we should save surfaces for this app.
      *
@@ -329,15 +336,14 @@
         if (!hasSavedSurface()) {
             return;
         }
-
-        if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) Slog.v(TAG_WM,
-                "Restoring saved surfaces: " + this + ", allDrawn=" + allDrawn);
-
         mAnimatingWithSavedSurface = true;
         for (int i = windows.size() - 1; i >= 0; i--) {
             WindowState ws = windows.get(i);
             ws.restoreSavedSurface();
         }
+        // Mark the app allDrawn since it must be allDrawn at the time
+        // it was first saved.
+        allDrawn = true;
     }
 
     void destroySavedSurfaces() {
diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java
index 7b0a8d7..65b91f7 100644
--- a/services/core/java/com/android/server/wm/DragState.java
+++ b/services/core/java/com/android/server/wm/DragState.java
@@ -360,8 +360,6 @@
         mCurrentX = x;
         mCurrentY = y;
 
-        final int myPid = Process.myPid();
-
         // Move the surface to the given touch
         if (SHOW_LIGHT_TRANSACTIONS) Slog.i(
                 TAG_WM, ">>> OPEN TRANSACTION notifyMoveLw");
@@ -376,7 +374,10 @@
             if (SHOW_LIGHT_TRANSACTIONS) Slog.i(
                     TAG_WM, "<<< CLOSE TRANSACTION notifyMoveLw");
         }
+        notifyLocationLw(x, y);
+    }
 
+    void notifyLocationLw(float x, float y) {
         // Tell the affected window
         WindowState touchedWin = getTouchedWinAtPointLw(x, y);
         if (touchedWin == null) {
@@ -392,6 +393,8 @@
             }
         }
         try {
+            final int myPid = Process.myPid();
+
             // have we dragged over a new window?
             if ((touchedWin != mTargetWindow) && (mTargetWindow != null)) {
                 if (DEBUG_DRAG) {
@@ -399,7 +402,7 @@
                 }
                 // force DRAG_EXITED_EVENT if appropriate
                 DragEvent evt = obtainDragEvent(mTargetWindow, DragEvent.ACTION_DRAG_EXITED,
-                        x, y, null, null, null, null, false);
+                        0, 0, null, null, null, null, false);
                 mTargetWindow.mClient.dispatchDragEvent(evt);
                 if (myPid != mTargetWindow.mSession.mPid) {
                     evt.recycle();
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index 8aaf430..d8cbbab 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -357,6 +357,8 @@
                 if (SHOW_LIGHT_TRANSACTIONS) Slog.i(
                         TAG_WM, "<<< CLOSE TRANSACTION performDrag");
             }
+
+            mService.mDragState.notifyLocationLw(touchX, touchY);
         }
 
         return true;    // success!
diff --git a/services/core/java/com/android/server/wm/TaskStack.java b/services/core/java/com/android/server/wm/TaskStack.java
index a75f2c9..2833b35 100644
--- a/services/core/java/com/android/server/wm/TaskStack.java
+++ b/services/core/java/com/android/server/wm/TaskStack.java
@@ -299,8 +299,7 @@
         final int orientation = mService.mCurConfiguration.orientation;
         mService.mPolicy.getStableInsetsLw(rotation, displayWidth, displayHeight, outBounds);
         final DividerSnapAlgorithm algorithm = new DividerSnapAlgorithm(
-                mService.mContext.getResources(),
-                0 /* minFlingVelocityPxPerSecond */, displayWidth, displayHeight,
+                mService.mContext.getResources(), displayWidth, displayHeight,
                 dividerSize, orientation == Configuration.ORIENTATION_PORTRAIT, outBounds);
         final SnapTarget target = algorithm.calculateNonDismissingSnapTarget(dividerPosition);
 
@@ -552,7 +551,6 @@
             mService.mPolicy.getStableInsetsLw(di.rotation, di.logicalWidth, di.logicalHeight,
                     mTmpRect2);
             final int position = new DividerSnapAlgorithm(mService.mContext.getResources(),
-                    0 /* minFlingVelocityPxPerSecond */,
                     di.logicalWidth,
                     di.logicalHeight,
                     dockDividerWidth,
diff --git a/services/core/java/com/android/server/wm/WindowAnimator.java b/services/core/java/com/android/server/wm/WindowAnimator.java
index 62d4f36..722c3b4 100644
--- a/services/core/java/com/android/server/wm/WindowAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowAnimator.java
@@ -281,8 +281,7 @@
             final int flags = win.mAttrs.flags;
             boolean canBeForceHidden = mPolicy.canBeForceHidden(win, win.mAttrs);
             boolean shouldBeForceHidden = shouldForceHide(win);
-            if (winAnimator.mSurfaceController != null
-                    && winAnimator.mSurfaceController.hasSurface()) {
+            if (winAnimator.hasSurface()) {
                 final boolean wasAnimating = winAnimator.mWasAnimating;
                 final boolean nowAnimating = winAnimator.stepAnimationLocked(mCurrentTime);
                 winAnimator.mWasAnimating = nowAnimating;
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 4b0015e..d2e2639 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -2679,20 +2679,11 @@
             } else {
                 winAnimator.mEnterAnimationPending = false;
                 winAnimator.mEnteringAnimation = false;
-                if (winAnimator.mSurfaceController != null &&
-                        winAnimator.mSurfaceController.hasSurface()) {
+                if (winAnimator.hasSurface() && !win.mExiting) {
                     if (DEBUG_VISIBILITY) Slog.i(TAG_WM, "Relayout invis " + win
                             + ": mExiting=" + win.mExiting);
-                    // If we are using a saved surface to do enter animation, just let the
-                    // animation run and don't destroy the surface. This could happen when
-                    // the app sets visibility to invisible for the first time after resume,
-                    // or when the user exits immediately after a resume. In both cases, we
-                    // don't want to destroy the saved surface.
                     // If we are not currently running the exit animation, we
                     // need to see about starting one.
-                    final boolean notExitingOrAnimating =
-                            !win.mExiting && !win.isAnimatingWithSavedSurface();
-                    result |= notExitingOrAnimating ? RELAYOUT_RES_SURFACE_CHANGED : 0;
                     // We don't want to animate visibility of windows which are pending
                     // replacement. In the case of activity relaunch child windows
                     // could request visibility changes as they are detached from the main
@@ -2700,11 +2691,11 @@
                     // these visibility changes though, we would cause a visual glitch
                     // hiding the window before it's replacement was available.
                     // So we just do nothing on our side.
-                    if (notExitingOrAnimating && win.mWillReplaceWindow == false) {
-                        focusMayChange = tryStartingAnimation(win, winAnimator, isDefaultDisplay,
-                                focusMayChange);
-
+                    if (!win.mWillReplaceWindow) {
+                        focusMayChange = tryStartExitingAnimation(
+                                win, winAnimator, isDefaultDisplay, focusMayChange);
                     }
+                    result |= RELAYOUT_RES_SURFACE_CHANGED;
                 }
 
                 outSurface.release();
@@ -2787,7 +2778,7 @@
         return result;
     }
 
-    private boolean tryStartingAnimation(WindowState win, WindowStateAnimator winAnimator,
+    private boolean tryStartExitingAnimation(WindowState win, WindowStateAnimator winAnimator,
             boolean isDefaultDisplay, boolean focusMayChange) {
         // Try starting an animation; if there isn't one, we
         // can destroy the surface right away.
@@ -4240,6 +4231,7 @@
                         }
                     }
                 } else {
+                    wtoken.markSurfacesExiting();
                     mClosingApps.add(wtoken);
                     wtoken.mEnteringAnimation = false;
                 }
@@ -10278,10 +10270,6 @@
         return (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, displayMetrics);
     }
 
-    void scheduleSurfaceDestroy(WindowState win) {
-        mDestroySurface.add(win);
-    }
-
     @Override
     public void registerDockedStackListener(IDockedStackListener listener) {
         if (!checkCallingPermission(android.Manifest.permission.REGISTER_WINDOW_MANAGER_LISTENERS,
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index e8a02b0..dca7735 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -1768,45 +1768,69 @@
         return mAppToken != null && mAppToken.mAnimatingWithSavedSurface;
     }
 
-    // Returns true if the surface is saved.
-    boolean destroyOrSaveSurface() {
-        Task task = getTask();
+    private boolean shouldSaveSurface() {
         if (ActivityManager.isLowRamDeviceStatic()) {
             // Don't save surfaces on Svelte devices.
-            mSurfaceSaved = false;
-        } else if (task == null || task.inHomeStack()
-                || task.getTopVisibleAppToken() != mAppToken) {
+            return false;
+        }
+
+        if (isChildWindow()) {
+            return false;
+        }
+
+        Task task = getTask();
+        if (task == null || task.inHomeStack()) {
             // Don't save surfaces for home stack apps. These usually resume and draw
             // first frame very fast. Saving surfaces are mostly a waste of memory.
-            // Don't save if the window is not the topmost window.
-            mSurfaceSaved = false;
-        } else if (isChildWindow()) {
-            mSurfaceSaved = false;
-        } else {
-            mSurfaceSaved = mAppToken.shouldSaveSurface();
+            return false;
         }
-        if (mSurfaceSaved == false) {
+
+        final AppWindowToken taskTop = task.getTopVisibleAppToken();
+        if (taskTop != null && taskTop != mAppToken) {
+            // Don't save if the window is not the topmost window.
+            return false;
+        }
+
+        return mAppToken.shouldSaveSurface();
+    }
+
+    void destroyOrSaveSurface() {
+        mSurfaceSaved = shouldSaveSurface();
+        if (mSurfaceSaved) {
+            if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) {
+                Slog.v(TAG, "Saving surface: " + this);
+            }
+
+            mWinAnimator.hide("saved surface");
+            mWinAnimator.mDrawState = WindowStateAnimator.NO_SURFACE;
+            setHasSurface(false);
+        } else {
             mWinAnimator.destroySurfaceLocked();
         }
-        return mSurfaceSaved;
     }
 
     public void destroySavedSurface() {
-        if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) Slog.v(TAG, "Destroying saved surface: " + this);
         if (mSurfaceSaved) {
+            if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) {
+                Slog.v(TAG, "Destroying saved surface: " + this);
+            }
             mWinAnimator.destroySurfaceLocked();
         }
     }
 
+    public void restoreSavedSurface() {
+        mSurfaceSaved = false;
+        setHasSurface(true);
+        mWinAnimator.mDrawState = WindowStateAnimator.READY_TO_SHOW;
+        if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) {
+            Slog.v(TAG, "Restoring saved surface: " + this);
+        }
+    }
+
     public boolean hasSavedSurface() {
         return mSurfaceSaved;
     }
 
-    public void restoreSavedSurface() {
-        mSurfaceSaved = false;
-        mWinAnimator.mDrawState = WindowStateAnimator.READY_TO_SHOW;
-    }
-
     @Override
     public boolean isDefaultDisplay() {
         final DisplayContent displayContent = getDisplayContent();
diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java
index da4e191..cffcc5d 100644
--- a/services/core/java/com/android/server/wm/WindowStateAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java
@@ -468,7 +468,7 @@
         if (WindowManagerService.localLOGV) Slog.v(
                 TAG, "Exit animation finished in " + this
                 + ": remove=" + mWin.mRemoveOnExit);
-        if (mSurfaceController != null && mSurfaceController.hasSurface()) {
+        if (hasSurface()) {
             mService.mDestroySurface.add(mWin);
             mWin.mDestroying = true;
             hide("finishExit");
@@ -734,6 +734,11 @@
         mTmpSize.bottom += scale * (attrs.surfaceInsets.top + attrs.surfaceInsets.bottom);
     }
 
+    boolean hasSurface() {
+        return !mWin.mSurfaceSaved
+                && mSurfaceController != null && mSurfaceController.hasSurface();
+    }
+
     void destroySurfaceLocked() {
         final AppWindowToken wtoken = mWin.mAppToken;
         if (wtoken != null) {
@@ -1229,7 +1234,7 @@
 
     void prepareSurfaceLocked(final boolean recoveringMemory) {
         final WindowState w = mWin;
-        if (mSurfaceController == null || !mSurfaceController.hasSurface()) {
+        if (!hasSurface()) {
             if (w.mOrientationChanging) {
                 if (DEBUG_ORIENTATION) {
                     Slog.v(TAG, "Orientation change skips hidden " + w);
@@ -1311,7 +1316,7 @@
                     w.mOrientationChanging = false;
                 }
             }
-            if (mSurfaceController != null && mSurfaceController.hasSurface()) {
+            if (hasSurface()) {
                 w.mToken.hasVisible = true;
             }
         } else {
diff --git a/services/core/java/com/android/server/wm/WindowSurfacePlacer.java b/services/core/java/com/android/server/wm/WindowSurfacePlacer.java
index ab6667a..9012b98 100644
--- a/services/core/java/com/android/server/wm/WindowSurfacePlacer.java
+++ b/services/core/java/com/android/server/wm/WindowSurfacePlacer.java
@@ -1161,6 +1161,9 @@
                 appAnimator.animation = null;
             }
             wtoken.inPendingTransaction = false;
+
+            wtoken.restoreSavedSurfaces();
+
             if (!mService.setTokenVisibilityLocked(
                     wtoken, animLp, true, transit, false, voiceInteraction)){
                 // This token isn't going to be animating. Add it to the list of tokens to
@@ -1217,8 +1220,6 @@
             if (mService.mAppTransition.isNextAppTransitionThumbnailUp()) {
                 createThumbnailAppAnimator(transit, wtoken, topOpeningLayer, topClosingLayer);
             }
-
-            wtoken.restoreSavedSurfaces();
         }
         return topOpeningApp;
     }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index eb75914..f68a299 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -67,6 +67,7 @@
 import android.content.pm.UserInfo;
 import android.database.ContentObserver;
 import android.graphics.Bitmap;
+import android.graphics.Color;
 import android.media.AudioManager;
 import android.media.IAudioService;
 import android.net.ConnectivityManager;
@@ -487,6 +488,7 @@
         private static final String TAG_SHORT_SUPPORT_MESSAGE = "short-support-message";
         private static final String TAG_LONG_SUPPORT_MESSAGE = "long-support-message";
         private static final String TAG_PARENT_ADMIN = "parent-admin";
+        private static final String TAG_ORGANIZATION_COLOR = "organization-color";
 
         final DeviceAdminInfo info;
 
@@ -580,6 +582,10 @@
         String shortSupportMessage = null;
         String longSupportMessage = null;
 
+        // Background color of confirm credentials screen. Default: gray.
+        static final int DEF_ORGANIZATION_COLOR = Color.GRAY;
+        int organizationColor = DEF_ORGANIZATION_COLOR;
+
         ActiveAdmin(DeviceAdminInfo _info, boolean parent) {
             info = _info;
             isParent = parent;
@@ -787,6 +793,11 @@
                 parentAdmin.writeToXml(out);
                 out.endTag(null, TAG_PARENT_ADMIN);
             }
+            if (organizationColor != DEF_ORGANIZATION_COLOR) {
+                out.startTag(null, TAG_ORGANIZATION_COLOR);
+                out.attribute(null, ATTR_VALUE, Integer.toString(organizationColor));
+                out.endTag(null, TAG_ORGANIZATION_COLOR);
+            }
         }
 
         void writePackageListToXml(XmlSerializer out, String outerTag,
@@ -920,6 +931,9 @@
                 } else if (TAG_PARENT_ADMIN.equals(tag)) {
                     parentAdmin = new ActiveAdmin(info, /* parent */ true);
                     parentAdmin.readFromXml(parser);
+                } else if (TAG_ORGANIZATION_COLOR.equals(tag)) {
+                    organizationColor = Integer.parseInt(
+                            parser.getAttributeValue(null, ATTR_VALUE));
                 } else {
                     Slog.w(LOG_TAG, "Unknown admin tag: " + tag);
                     XmlUtils.skipCurrentTag(parser);
@@ -7685,4 +7699,46 @@
         }
         return null;
     }
+
+    @Override
+    public void setOrganizationColor(@NonNull ComponentName who, int color) {
+        final int userHandle = mInjector.userHandleGetCallingUserId();
+        if (!mHasFeature || !isManagedProfile(userHandle)) {
+            return;
+        }
+        Preconditions.checkNotNull(who, "ComponentName is null");
+        synchronized (this) {
+            ActiveAdmin admin = getActiveAdminForCallerLocked(who,
+                    DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
+            admin.organizationColor = color;
+            saveSettingsLocked(userHandle);
+        }
+    }
+
+    @Override
+    public int getOrganizationColor(@NonNull ComponentName who) {
+        final int userHandle = mInjector.userHandleGetCallingUserId();
+        if (!mHasFeature || !isManagedProfile(userHandle)) {
+            return ActiveAdmin.DEF_ORGANIZATION_COLOR;
+        }
+        synchronized (this) {
+            ActiveAdmin admin = getActiveAdminForCallerLocked(who,
+                    DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
+            return admin.organizationColor;
+        }
+    }
+
+    @Override
+    public int getOrganizationColorForUser(int userHandle) {
+        if (!mHasFeature || !isManagedProfile(userHandle)) {
+            return ActiveAdmin.DEF_ORGANIZATION_COLOR;
+        }
+        enforceCrossUsersPermission(userHandle);
+        synchronized (this) {
+            ActiveAdmin profileOwner = getProfileOwnerAdminLocked(userHandle);
+            return (profileOwner != null)
+                    ? profileOwner.organizationColor
+                    : ActiveAdmin.DEF_ORGANIZATION_COLOR;
+        }
+    }
 }
diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java
index d45b26f..3e30188 100644
--- a/telecomm/java/android/telecom/TelecomManager.java
+++ b/telecomm/java/android/telecom/TelecomManager.java
@@ -298,6 +298,16 @@
     public static final String METADATA_IN_CALL_SERVICE_UI = "android.telecom.IN_CALL_SERVICE_UI";
 
     /**
+     * A boolean meta-data value indicating whether an {@link InCallService} implements an
+     * in-call user interface to be used while the device is in car-mode (see
+     * {@link android.content.res.Configuration.UI_MODE_TYPE_CAR}).
+     *
+     * @hide
+     */
+    public static final String METADATA_IN_CALL_SERVICE_CAR_MODE_UI =
+            "android.telecom.IN_CALL_SERVICE_CAR_MODE_UI";
+
+    /**
      * The dual tone multi-frequency signaling character sent to indicate the dialing system should
      * pause for a predefined period.
      */
diff --git a/test-runner/src/android/test/mock/MockPackageManager.java b/test-runner/src/android/test/mock/MockPackageManager.java
index 81aa6c6..5296d4d 100644
--- a/test-runner/src/android/test/mock/MockPackageManager.java
+++ b/test-runner/src/android/test/mock/MockPackageManager.java
@@ -532,6 +532,12 @@
         throw new UnsupportedOperationException();
     }
 
+    /** @hide */
+    @Override
+    public Drawable getUserBadgeForDensityNoBackground(UserHandle user, int density) {
+        throw new UnsupportedOperationException();
+    }
+
     @Override
     public CharSequence getUserBadgedLabel(CharSequence label, UserHandle user) {
         throw new UnsupportedOperationException();
diff --git a/tools/aapt/AaptAssets.cpp b/tools/aapt/AaptAssets.cpp
index d346731..3b01827 100644
--- a/tools/aapt/AaptAssets.cpp
+++ b/tools/aapt/AaptAssets.cpp
@@ -235,7 +235,7 @@
          setRegion(part2.string());
      } else if (part2.length() == 4 && isAlpha(part2)) {
          setScript(part2.string());
-     } else if (part2.length() >= 5 && part2.length() <= 8) {
+     } else if (part2.length() >= 4 && part2.length() <= 8) {
          setVariant(part2.string());
      } else {
          valid = false;
@@ -250,7 +250,7 @@
      if (((part3.length() == 2 && isAlpha(part3)) ||
          (part3.length() == 3 && isNumber(part3))) && script[0]) {
          setRegion(part3.string());
-     } else if (part3.length() >= 5 && part3.length() <= 8) {
+     } else if (part3.length() >= 4 && part3.length() <= 8) {
          setVariant(part3.string());
      } else {
          valid = false;
@@ -261,7 +261,7 @@
      }
 
      const String8& part4 = parts[3];
-     if (part4.length() >= 5 && part4.length() <= 8) {
+     if (part4.length() >= 4 && part4.length() <= 8) {
          setVariant(part4.string());
      } else {
          valid = false;
@@ -280,7 +280,7 @@
 
     String8 part = parts[currentIndex];
     if (part[0] == 'b' && part[1] == '+') {
-        // This is a "modified" BCP-47 language tag. Same semantics as BCP-47 tags,
+        // This is a "modified" BCP 47 language tag. Same semantics as BCP 47 tags,
         // except that the separator is "+" and not "-".
         Vector<String8> subtags = AaptUtil::splitAndLowerCase(part, '+');
         subtags.removeItemsAt(0);
@@ -296,8 +296,11 @@
                     setRegion(subtags[1]);
                     break;
                 case 4:
-                    setScript(subtags[1]);
-                    break;
+                    if (isAlpha(subtags[1])) {
+                        setScript(subtags[1]);
+                        break;
+                    }
+                    // This is not alphabetical, so we fall through to variant
                 case 5:
                 case 6:
                 case 7:
@@ -305,7 +308,7 @@
                     setVariant(subtags[1]);
                     break;
                 default:
-                    fprintf(stderr, "ERROR: Invalid BCP-47 tag in directory name %s\n",
+                    fprintf(stderr, "ERROR: Invalid BCP 47 tag in directory name %s\n",
                             part.string());
                     return -1;
             }
@@ -322,13 +325,13 @@
                 setRegion(subtags[1]);
                 hasRegion = true;
             } else {
-                fprintf(stderr, "ERROR: Invalid BCP-47 tag in directory name %s\n", part.string());
+                fprintf(stderr, "ERROR: Invalid BCP 47 tag in directory name %s\n", part.string());
                 return -1;
             }
 
             // The third tag can either be a region code (if the second tag was
             // a script), else a variant code.
-            if (subtags[2].size() > 4) {
+            if (subtags[2].size() >= 4) {
                 setVariant(subtags[2]);
             } else {
                 setRegion(subtags[2]);
@@ -339,7 +342,7 @@
             setRegion(subtags[2]);
             setVariant(subtags[3]);
         } else {
-            fprintf(stderr, "ERROR: Invalid BCP-47 tag in directory name: %s\n", part.string());
+            fprintf(stderr, "ERROR: Invalid BCP 47 tag in directory name: %s\n", part.string());
             return -1;
         }
 
@@ -370,7 +373,7 @@
 void AaptLocaleValue::initFromResTable(const ResTable_config& config) {
     config.unpackLanguage(language);
     config.unpackRegion(region);
-    if (config.localeScript[0]) {
+    if (config.localeScriptWasProvided) {
         memcpy(script, config.localeScript, sizeof(config.localeScript));
     }
 
@@ -385,6 +388,10 @@
 
     if (script[0]) {
         memcpy(out->localeScript, script, sizeof(out->localeScript));
+        out->localeScriptWasProvided = true;
+    } else {
+        out->computeScript();
+        out->localeScriptWasProvided = false;
     }
 
     if (variant[0]) {
diff --git a/tools/aapt2/Locale.cpp b/tools/aapt2/Locale.cpp
index 20a2d0c..0369156 100644
--- a/tools/aapt2/Locale.cpp
+++ b/tools/aapt2/Locale.cpp
@@ -96,7 +96,7 @@
          setRegion(part2.c_str());
      } else if (part2.length() == 4 && isAlpha(part2)) {
          setScript(part2.c_str());
-     } else if (part2.length() >= 5 && part2.length() <= 8) {
+     } else if (part2.length() >= 4 && part2.length() <= 8) {
          setVariant(part2.c_str());
      } else {
          valid = false;
@@ -111,7 +111,7 @@
      if (((part3.length() == 2 && isAlpha(part3)) ||
          (part3.length() == 3 && isNumber(part3))) && script[0]) {
          setRegion(part3.c_str());
-     } else if (part3.length() >= 5 && part3.length() <= 8) {
+     } else if (part3.length() >= 4 && part3.length() <= 8) {
          setVariant(part3.c_str());
      } else {
          valid = false;
@@ -122,7 +122,7 @@
      }
 
      const std::string& part4 = parts[3];
-     if (part4.length() >= 5 && part4.length() <= 8) {
+     if (part4.length() >= 4 && part4.length() <= 8) {
          setVariant(part4.c_str());
      } else {
          valid = false;
@@ -141,7 +141,7 @@
 
     std::string& part = *iter;
     if (part[0] == 'b' && part[1] == '+') {
-        // This is a "modified" BCP-47 language tag. Same semantics as BCP-47 tags,
+        // This is a "modified" BCP 47 language tag. Same semantics as BCP 47 tags,
         // except that the separator is "+" and not "-".
         std::vector<std::string> subtags = util::splitAndLowercase(part, '+');
         subtags.erase(subtags.begin());
@@ -157,8 +157,12 @@
                     setRegion(subtags[1].c_str());
                     break;
                 case 4:
-                    setScript(subtags[1].c_str());
-                    break;
+                    if ('0' <= subtags[1][0] && subtags[1][0] <= '9') {
+                        // This is a variant: fall through
+                    } else {
+                        setScript(subtags[1].c_str());
+                        break;
+                    }
                 case 5:
                 case 6:
                 case 7:
@@ -184,7 +188,7 @@
 
             // The third tag can either be a region code (if the second tag was
             // a script), else a variant code.
-            if (subtags[2].size() > 4) {
+            if (subtags[2].size() >= 4) {
                 setVariant(subtags[2].c_str());
             } else {
                 setRegion(subtags[2].c_str());
@@ -249,7 +253,7 @@
 void LocaleValue::initFromResTable(const ResTable_config& config) {
     config.unpackLanguage(language);
     config.unpackRegion(region);
-    if (config.localeScript[0]) {
+    if (config.localeScriptWasProvided) {
         memcpy(script, config.localeScript, sizeof(config.localeScript));
     }
 
@@ -264,6 +268,10 @@
 
     if (script[0]) {
         memcpy(out->localeScript, script, sizeof(out->localeScript));
+        out->localeScriptWasProvided = true;
+    } else {
+        out->computeScript();
+        out->localeScriptWasProvided = false;
     }
 
     if (variant[0]) {
diff --git a/tools/aapt2/io/ZipArchive.cpp b/tools/aapt2/io/ZipArchive.cpp
index bf0f4aa..329dac9 100644
--- a/tools/aapt2/io/ZipArchive.cpp
+++ b/tools/aapt2/io/ZipArchive.cpp
@@ -75,11 +75,19 @@
 
 std::unique_ptr<ZipFileCollection> ZipFileCollection::create(const StringPiece& path,
                                                              std::string* outError) {
+    constexpr static const int32_t kEmptyArchive = -6;
+
     std::unique_ptr<ZipFileCollection> collection = std::unique_ptr<ZipFileCollection>(
             new ZipFileCollection());
 
     int32_t result = OpenArchive(path.data(), &collection->mHandle);
     if (result != 0) {
+        // If a zip is empty, result will be an error code. This is fine and we should
+        // return an empty ZipFileCollection.
+        if (result == kEmptyArchive) {
+            return collection;
+        }
+
         if (outError) *outError = ErrorCodeString(result);
         return {};
     }
diff --git a/tools/aapt2/link/Link.cpp b/tools/aapt2/link/Link.cpp
index 51b9cec..364a55b 100644
--- a/tools/aapt2/link/Link.cpp
+++ b/tools/aapt2/link/Link.cpp
@@ -551,12 +551,13 @@
             if (resourceFile) {
                 return mergeCompiledFile(file, std::move(resourceFile), override);
             }
-        } else {
-            // Ignore non .flat files. This could be classes.dex or something else that happens
-            // to be in an archive.
+
+            return false;
         }
 
-        return false;
+        // Ignore non .flat files. This could be classes.dex or something else that happens
+        // to be in an archive.
+        return true;
     }
 
     int run(const std::vector<std::string>& inputFiles) {
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgePackageManager.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgePackageManager.java
index 08258c9..037ce57 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgePackageManager.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgePackageManager.java
@@ -492,6 +492,11 @@
     }
 
     @Override
+    public Drawable getUserBadgeForDensityNoBackground(UserHandle user, int density) {
+        return null;
+    }
+
+    @Override
     public CharSequence getUserBadgedLabel(CharSequence label, UserHandle user) {
         return null;
     }
diff --git a/tools/localedata/extract_icu_data.py b/tools/localedata/extract_icu_data.py
new file mode 100755
index 0000000..b071093
--- /dev/null
+++ b/tools/localedata/extract_icu_data.py
@@ -0,0 +1,286 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 The Android Open Source Project. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+#
+
+"""Generate a C++ data table containing locale data."""
+
+import collections
+import glob
+import os.path
+import sys
+
+
+def get_locale_parts(locale):
+    """Split a locale into three parts, for langauge, script, and region."""
+    parts = locale.split('_')
+    if len(parts) == 1:
+        return (parts[0], None, None)
+    elif len(parts) == 2:
+        if len(parts[1]) == 4:  # parts[1] is a script
+            return (parts[0], parts[1], None)
+        else:
+            return (parts[0], None, parts[1])
+    else:
+        assert len(parts) == 3
+        return tuple(parts)
+
+
+def read_likely_subtags(input_file_name):
+    """Read and parse ICU's likelySubtags.txt."""
+    with open(input_file_name) as input_file:
+        likely_script_dict = {
+            # Android's additions for pseudo-locales. These internal codes make
+            # sure that the pseudo-locales would not match other English or
+            # Arabic locales. (We can't use private-use ISO 15924 codes, since
+            # they may be used by apps for other purposes.)
+            "en_XA": "~~~A",
+            "ar_XB": "~~~B",
+        }
+        representative_locales = {
+            # Android's additions
+            "en_Latn_GB", # representative for en_Latn_001
+            "es_Latn_MX", # representative for es_Latn_419
+            "es_Latn_US", # representative for es_Latn_419 (not the best idea,
+                          # but Android has been shipping with it for quite a
+                          # while. Fortunately, MX < US, so if both exist, MX
+                          # would be chosen.)
+        }
+        for line in input_file:
+            line = unicode(line, 'UTF-8').strip(u' \n\uFEFF').encode('UTF-8')
+            if line.startswith('//'):
+                continue
+            if '{' in line and '}' in line:
+                from_locale = line[:line.index('{')]
+                to_locale = line[line.index('"')+1:line.rindex('"')]
+                from_lang, from_scr, from_region = get_locale_parts(from_locale)
+                _, to_scr, to_region = get_locale_parts(to_locale)
+                if from_lang == 'und':
+                    continue  # not very useful for our purposes
+                if from_region is None and to_region != '001':
+                    representative_locales.add(to_locale)
+                if from_scr is None:
+                    likely_script_dict[from_locale] = to_scr
+        return likely_script_dict, frozenset(representative_locales)
+
+
+# From packLanguageOrRegion() in ResourceTypes.cpp
+def pack_language_or_region(inp, base):
+    """Pack langauge or region in a two-byte tuple."""
+    if inp is None:
+        return (0, 0)
+    elif len(inp) == 2:
+        return ord(inp[0]), ord(inp[1])
+    else:
+        assert len(inp) == 3
+        base = ord(base)
+        first = ord(inp[0]) - base
+        second = ord(inp[1]) - base
+        third = ord(inp[2]) - base
+
+        return (0x80 | (third << 2) | (second >>3),
+                ((second << 5) | first) & 0xFF)
+
+
+# From packLanguage() in ResourceTypes.cpp
+def pack_language(language):
+    """Pack language in a two-byte tuple."""
+    return pack_language_or_region(language, 'a')
+
+
+# From packRegion() in ResourceTypes.cpp
+def pack_region(region):
+    """Pack region in a two-byte tuple."""
+    return pack_language_or_region(region, '0')
+
+
+def pack_to_uint32(locale):
+    """Pack language+region of locale into a 32-bit unsigned integer."""
+    lang, _, region = get_locale_parts(locale)
+    plang = pack_language(lang)
+    pregion = pack_region(region)
+    return (plang[0] << 24) | (plang[1] << 16) | (pregion[0] << 8) | pregion[1]
+
+
+def dump_script_codes(all_scripts):
+    """Dump the SCRIPT_CODES table."""
+    print 'const char SCRIPT_CODES[][4] = {'
+    for index, script in enumerate(all_scripts):
+        print "    /* %-2d */ {'%c', '%c', '%c', '%c'}," % (
+            index, script[0], script[1], script[2], script[3])
+    print '};'
+    print
+
+
+def dump_script_data(likely_script_dict, all_scripts):
+    """Dump the script data."""
+    print
+    print 'const std::unordered_map<uint32_t, uint8_t> LIKELY_SCRIPTS({'
+    for locale in sorted(likely_script_dict.keys()):
+        script = likely_script_dict[locale]
+        print '    {0x%08Xu, %2du}, // %s -> %s' % (
+            pack_to_uint32(locale),
+            all_scripts.index(script),
+            locale.replace('_', '-'),
+            script)
+    print '});'
+
+
+def pack_to_uint64(locale):
+    """Pack a full locale into a 64-bit unsigned integer."""
+    _, script, _ = get_locale_parts(locale)
+    return ((pack_to_uint32(locale) << 32) |
+            (ord(script[0]) << 24) |
+            (ord(script[1]) << 16) |
+            (ord(script[2]) << 8) |
+            ord(script[3]))
+
+
+def dump_representative_locales(representative_locales):
+    """Dump the set of representative locales."""
+    print
+    print 'std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({'
+    for locale in sorted(representative_locales):
+        print '    0x%08Xllu, // %s' % (
+            pack_to_uint64(locale),
+            locale)
+    print '});'
+
+
+def read_and_dump_likely_data(icu_data_dir):
+    """Read and dump the likely-script data."""
+    likely_subtags_txt = os.path.join(icu_data_dir, 'misc', 'likelySubtags.txt')
+    likely_script_dict, representative_locales = read_likely_subtags(
+        likely_subtags_txt)
+
+    all_scripts = list(set(likely_script_dict.values()))
+    assert len(all_scripts) <= 256
+    all_scripts.sort()
+
+    dump_script_codes(all_scripts)
+    dump_script_data(likely_script_dict, all_scripts)
+    dump_representative_locales(representative_locales)
+    return likely_script_dict
+
+
+def read_parent_data(icu_data_dir):
+    """Read locale parent data from ICU data files."""
+    all_icu_data_files = glob.glob(os.path.join(icu_data_dir, '*', '*.txt'))
+    parent_dict = {}
+    for data_file in all_icu_data_files:
+        locale = os.path.splitext(os.path.basename(data_file))[0]
+        with open(data_file) as input_file:
+            for line in input_file:
+                if '%%Parent' in line:
+                    parent = line[line.index('"')+1:line.rindex('"')]
+                    if locale in parent_dict:
+                        # Different files shouldn't have different parent info
+                        assert parent_dict[locale] == parent
+                    else:
+                        parent_dict[locale] = parent
+                elif locale.startswith('ar_') and 'default{"latn"}' in line:
+                    # Arabic parent overrides for ASCII digits. Since
+                    # Unicode extensions are not supported in ResourceTypes,
+                    # we will use ar-015 (Arabic, Northern Africa) instead
+                    # of the more correct ar-u-nu-latn.
+                    parent_dict[locale] = 'ar_015'
+    return parent_dict
+
+
+def get_likely_script(locale, likely_script_dict):
+    """Find the likely script for a locale, given the likely-script dictionary.
+    """
+    if locale.count('_') == 2:
+        # it already has a script
+        return locale.split('_')[1]
+    elif locale in likely_script_dict:
+        return likely_script_dict[locale]
+    else:
+        language = locale.split('_')[0]
+        return likely_script_dict[language]
+
+
+def dump_parent_data(script_organized_dict):
+    """Dump information for parents of locales."""
+    sorted_scripts = sorted(script_organized_dict.keys())
+    print
+    for script in sorted_scripts:
+        parent_dict = script_organized_dict[script]
+        print ('const std::unordered_map<uint32_t, uint32_t> %s_PARENTS({'
+            % script.upper())
+        for locale in sorted(parent_dict.keys()):
+            parent = parent_dict[locale]
+            print '    {0x%08Xu, 0x%08Xu}, // %s -> %s' % (
+                pack_to_uint32(locale),
+                pack_to_uint32(parent),
+                locale.replace('_', '-'),
+                parent.replace('_', '-'))
+        print '});'
+        print
+
+    print 'const struct {'
+    print '    const char script[4];'
+    print '    const std::unordered_map<uint32_t, uint32_t>* map;'
+    print '} SCRIPT_PARENTS[] = {'
+    for script in sorted_scripts:
+        print "    {{'%c', '%c', '%c', '%c'}, &%s_PARENTS}," % (
+            script[0], script[1], script[2], script[3],
+            script.upper())
+    print '};'
+
+
+def dump_parent_tree_depth(parent_dict):
+    """Find and dump the depth of the parent tree."""
+    max_depth = 1
+    for locale, _ in parent_dict.items():
+        depth = 1
+        while locale in parent_dict:
+            locale = parent_dict[locale]
+            depth += 1
+        max_depth = max(max_depth, depth)
+    assert max_depth < 5 # Our algorithms assume small max_depth
+    print
+    print 'const size_t MAX_PARENT_DEPTH = %d;' % max_depth
+
+
+def read_and_dump_parent_data(icu_data_dir, likely_script_dict):
+    """Read parent data from ICU and dump it."""
+    parent_dict = read_parent_data(icu_data_dir)
+    script_organized_dict = collections.defaultdict(dict)
+    for locale in parent_dict:
+        parent = parent_dict[locale]
+        if parent == 'root':
+            continue
+        script = get_likely_script(locale, likely_script_dict)
+        script_organized_dict[script][locale] = parent_dict[locale]
+    dump_parent_data(script_organized_dict)
+    dump_parent_tree_depth(parent_dict)
+
+
+def main():
+    """Read the data files from ICU and dump the output to a C++ file."""
+    source_root = sys.argv[1]
+    icu_data_dir = os.path.join(
+        source_root,
+        'external', 'icu', 'icu4c', 'source', 'data')
+
+    print '// Auto-generated by %s' % sys.argv[0]
+    print
+    likely_script_dict = read_and_dump_likely_data(icu_data_dir)
+    read_and_dump_parent_data(icu_data_dir, likely_script_dict)
+
+
+if __name__ == '__main__':
+    main()