Merge "Use SystemConfig to control background location throttling whitelist."
diff --git a/api/current.txt b/api/current.txt
index 659c1b8..5ecef52 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -9925,6 +9925,15 @@
     method public final int compare(android.content.pm.ApplicationInfo, android.content.pm.ApplicationInfo);
   }
 
+  public final class ChangedPackages implements android.os.Parcelable {
+    ctor public ChangedPackages(int, java.util.List<java.lang.String>);
+    method public int describeContents();
+    method public java.util.List<java.lang.String> getPackageNames();
+    method public int getSequenceNumber();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.content.pm.ChangedPackages> CREATOR;
+  }
+
   public class ComponentInfo extends android.content.pm.PackageItemInfo {
     ctor public ComponentInfo();
     ctor public ComponentInfo(android.content.pm.ComponentInfo);
@@ -10271,6 +10280,7 @@
     method public abstract java.lang.CharSequence getApplicationLabel(android.content.pm.ApplicationInfo);
     method public abstract android.graphics.drawable.Drawable getApplicationLogo(android.content.pm.ApplicationInfo);
     method public abstract android.graphics.drawable.Drawable getApplicationLogo(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException;
+    method public abstract android.content.pm.ChangedPackages getChangedPackages(int);
     method public abstract int getComponentEnabledSetting(android.content.ComponentName);
     method public abstract android.graphics.drawable.Drawable getDefaultActivityIcon();
     method public abstract android.graphics.drawable.Drawable getDrawable(java.lang.String, int, android.content.pm.ApplicationInfo);
@@ -14432,6 +14442,7 @@
     field public static final java.lang.String STRING_TYPE_HEART_RATE = "android.sensor.heart_rate";
     field public static final java.lang.String STRING_TYPE_LIGHT = "android.sensor.light";
     field public static final java.lang.String STRING_TYPE_LINEAR_ACCELERATION = "android.sensor.linear_acceleration";
+    field public static final java.lang.String STRING_TYPE_LOW_LATENCY_OFFBODY_DETECT = "android.sensor.low_latency_offbody";
     field public static final java.lang.String STRING_TYPE_MAGNETIC_FIELD = "android.sensor.magnetic_field";
     field public static final java.lang.String STRING_TYPE_MAGNETIC_FIELD_UNCALIBRATED = "android.sensor.magnetic_field_uncalibrated";
     field public static final java.lang.String STRING_TYPE_MOTION_DETECT = "android.sensor.motion_detect";
@@ -14460,6 +14471,7 @@
     field public static final int TYPE_HEART_RATE = 21; // 0x15
     field public static final int TYPE_LIGHT = 5; // 0x5
     field public static final int TYPE_LINEAR_ACCELERATION = 10; // 0xa
+    field public static final int TYPE_LOW_LATENCY_OFFBODY_DETECT = 34; // 0x22
     field public static final int TYPE_MAGNETIC_FIELD = 2; // 0x2
     field public static final int TYPE_MAGNETIC_FIELD_UNCALIBRATED = 14; // 0xe
     field public static final int TYPE_MOTION_DETECT = 30; // 0x1e
@@ -20713,7 +20725,7 @@
     method public int getStreamMaxVolume(int);
     method public int getStreamVolume(int);
     method public deprecated int getVibrateSetting(int);
-    method public boolean isBluetoothA2dpOn();
+    method public deprecated boolean isBluetoothA2dpOn();
     method public boolean isBluetoothScoAvailableOffCall();
     method public boolean isBluetoothScoOn();
     method public boolean isMicrophoneMute();
@@ -22319,6 +22331,7 @@
     method public void setAuxEffectSendLevel(float);
     method public void setBufferingParams(android.media.BufferingParams);
     method public void setDataSource(android.content.Context, android.net.Uri) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
+    method public void setDataSource(android.content.Context, android.net.Uri, java.util.Map<java.lang.String, java.lang.String>, java.util.List<java.net.HttpCookie>) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
     method public void setDataSource(android.content.Context, android.net.Uri, java.util.Map<java.lang.String, java.lang.String>) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
     method public void setDataSource(java.lang.String) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
     method public void setDataSource(android.content.res.AssetFileDescriptor) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException;
@@ -24084,6 +24097,7 @@
     field public static final java.lang.String COLUMN_AUTHOR = "author";
     field public static final java.lang.String COLUMN_AVAILABILITY = "availability";
     field public static final java.lang.String COLUMN_BROADCAST_GENRE = "broadcast_genre";
+    field public static final java.lang.String COLUMN_BROWSABLE = "browsable";
     field public static final java.lang.String COLUMN_DURATION_MILLIS = "duration_millis";
     field public static final java.lang.String COLUMN_END_TIME_UTC_MILLIS = "end_time_utc_millis";
     field public static final deprecated java.lang.String COLUMN_EPISODE_NUMBER = "episode_number";
@@ -24226,11 +24240,13 @@
     field public static final java.lang.String ACTION_BLOCKED_RATINGS_CHANGED = "android.media.tv.action.BLOCKED_RATINGS_CHANGED";
     field public static final java.lang.String ACTION_MAKE_CHANNEL_BROWSABLE = "android.media.tv.action.MAKE_CHANNEL_BROWSABLE";
     field public static final java.lang.String ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED = "android.media.tv.action.PARENTAL_CONTROLS_ENABLED_CHANGED";
+    field public static final java.lang.String ACTION_PROGRAM_BROWSABLE_DISABLED = "android.media.tv.action.PROGRAM_BROWSABLE_DISABLED";
     field public static final java.lang.String ACTION_QUERY_CONTENT_RATING_SYSTEMS = "android.media.tv.action.QUERY_CONTENT_RATING_SYSTEMS";
     field public static final java.lang.String ACTION_SETUP_INPUTS = "android.media.tv.action.SETUP_INPUTS";
     field public static final java.lang.String ACTION_VIEW_RECORDING_SCHEDULES = "android.media.tv.action.VIEW_RECORDING_SCHEDULES";
     field public static final java.lang.String EXTRA_CHANNEL_ID = "android.media.tv.extra.CHANNEL_ID";
     field public static final java.lang.String EXTRA_PACKAGE_NAME = "android.media.tv.extra.PACKAGE_NAME";
+    field public static final java.lang.String EXTRA_PROGRAM_ID = "android.media.tv.extra.PROGRAM_ID";
     field public static final int INPUT_STATE_CONNECTED = 0; // 0x0
     field public static final int INPUT_STATE_CONNECTED_STANDBY = 1; // 0x1
     field public static final int INPUT_STATE_DISCONNECTED = 2; // 0x2
@@ -40063,6 +40079,7 @@
     method public java.lang.CharSequence getApplicationLabel(android.content.pm.ApplicationInfo);
     method public android.graphics.drawable.Drawable getApplicationLogo(android.content.pm.ApplicationInfo);
     method public android.graphics.drawable.Drawable getApplicationLogo(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException;
+    method public android.content.pm.ChangedPackages getChangedPackages(int);
     method public int getComponentEnabledSetting(android.content.ComponentName);
     method public android.graphics.drawable.Drawable getDefaultActivityIcon();
     method public android.graphics.drawable.Drawable getDrawable(java.lang.String, int, android.content.pm.ApplicationInfo);
@@ -47316,7 +47333,7 @@
 
   public final class TextClassificationManager {
     method public java.util.List<android.view.textclassifier.TextLanguage> detectLanguages(java.lang.CharSequence);
-    method public android.view.textclassifier.TextClassifier getDefaultTextClassifier();
+    method public synchronized android.view.textclassifier.TextClassifier getDefaultTextClassifier();
   }
 
   public final class TextClassificationResult {
diff --git a/api/system-current.txt b/api/system-current.txt
index a6de1ea..e89c25c 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -10373,6 +10373,15 @@
     method public final int compare(android.content.pm.ApplicationInfo, android.content.pm.ApplicationInfo);
   }
 
+  public final class ChangedPackages implements android.os.Parcelable {
+    ctor public ChangedPackages(int, java.util.List<java.lang.String>);
+    method public int describeContents();
+    method public java.util.List<java.lang.String> getPackageNames();
+    method public int getSequenceNumber();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.content.pm.ChangedPackages> CREATOR;
+  }
+
   public class ComponentInfo extends android.content.pm.PackageItemInfo {
     ctor public ComponentInfo();
     ctor public ComponentInfo(android.content.pm.ComponentInfo);
@@ -10770,6 +10779,7 @@
     method public abstract java.lang.CharSequence getApplicationLabel(android.content.pm.ApplicationInfo);
     method public abstract android.graphics.drawable.Drawable getApplicationLogo(android.content.pm.ApplicationInfo);
     method public abstract android.graphics.drawable.Drawable getApplicationLogo(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException;
+    method public abstract android.content.pm.ChangedPackages getChangedPackages(int);
     method public abstract int getComponentEnabledSetting(android.content.ComponentName);
     method public abstract android.graphics.drawable.Drawable getDefaultActivityIcon();
     method public abstract java.lang.String getDefaultBrowserPackageNameAsUser(int);
@@ -15026,6 +15036,7 @@
     field public static final java.lang.String STRING_TYPE_HEART_RATE = "android.sensor.heart_rate";
     field public static final java.lang.String STRING_TYPE_LIGHT = "android.sensor.light";
     field public static final java.lang.String STRING_TYPE_LINEAR_ACCELERATION = "android.sensor.linear_acceleration";
+    field public static final java.lang.String STRING_TYPE_LOW_LATENCY_OFFBODY_DETECT = "android.sensor.low_latency_offbody";
     field public static final java.lang.String STRING_TYPE_MAGNETIC_FIELD = "android.sensor.magnetic_field";
     field public static final java.lang.String STRING_TYPE_MAGNETIC_FIELD_UNCALIBRATED = "android.sensor.magnetic_field_uncalibrated";
     field public static final java.lang.String STRING_TYPE_MOTION_DETECT = "android.sensor.motion_detect";
@@ -15056,6 +15067,7 @@
     field public static final int TYPE_HEART_RATE = 21; // 0x15
     field public static final int TYPE_LIGHT = 5; // 0x5
     field public static final int TYPE_LINEAR_ACCELERATION = 10; // 0xa
+    field public static final int TYPE_LOW_LATENCY_OFFBODY_DETECT = 34; // 0x22
     field public static final int TYPE_MAGNETIC_FIELD = 2; // 0x2
     field public static final int TYPE_MAGNETIC_FIELD_UNCALIBRATED = 14; // 0xe
     field public static final int TYPE_MOTION_DETECT = 30; // 0x1e
@@ -22327,7 +22339,7 @@
     method public int getStreamMaxVolume(int);
     method public int getStreamVolume(int);
     method public deprecated int getVibrateSetting(int);
-    method public boolean isBluetoothA2dpOn();
+    method public deprecated boolean isBluetoothA2dpOn();
     method public boolean isBluetoothScoAvailableOffCall();
     method public boolean isBluetoothScoOn();
     method public boolean isHdmiSystemAudioSupported();
@@ -23962,6 +23974,7 @@
     method public void setAuxEffectSendLevel(float);
     method public void setBufferingParams(android.media.BufferingParams);
     method public void setDataSource(android.content.Context, android.net.Uri) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
+    method public void setDataSource(android.content.Context, android.net.Uri, java.util.Map<java.lang.String, java.lang.String>, java.util.List<java.net.HttpCookie>) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
     method public void setDataSource(android.content.Context, android.net.Uri, java.util.Map<java.lang.String, java.lang.String>) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
     method public void setDataSource(java.lang.String) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
     method public void setDataSource(android.content.res.AssetFileDescriptor) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException;
@@ -25873,6 +25886,7 @@
     field public static final java.lang.String COLUMN_AUTHOR = "author";
     field public static final java.lang.String COLUMN_AVAILABILITY = "availability";
     field public static final java.lang.String COLUMN_BROADCAST_GENRE = "broadcast_genre";
+    field public static final java.lang.String COLUMN_BROWSABLE = "browsable";
     field public static final java.lang.String COLUMN_DURATION_MILLIS = "duration_millis";
     field public static final java.lang.String COLUMN_END_TIME_UTC_MILLIS = "end_time_utc_millis";
     field public static final deprecated java.lang.String COLUMN_EPISODE_NUMBER = "episode_number";
@@ -26096,11 +26110,13 @@
     field public static final java.lang.String ACTION_BLOCKED_RATINGS_CHANGED = "android.media.tv.action.BLOCKED_RATINGS_CHANGED";
     field public static final java.lang.String ACTION_MAKE_CHANNEL_BROWSABLE = "android.media.tv.action.MAKE_CHANNEL_BROWSABLE";
     field public static final java.lang.String ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED = "android.media.tv.action.PARENTAL_CONTROLS_ENABLED_CHANGED";
+    field public static final java.lang.String ACTION_PROGRAM_BROWSABLE_DISABLED = "android.media.tv.action.PROGRAM_BROWSABLE_DISABLED";
     field public static final java.lang.String ACTION_QUERY_CONTENT_RATING_SYSTEMS = "android.media.tv.action.QUERY_CONTENT_RATING_SYSTEMS";
     field public static final java.lang.String ACTION_SETUP_INPUTS = "android.media.tv.action.SETUP_INPUTS";
     field public static final java.lang.String ACTION_VIEW_RECORDING_SCHEDULES = "android.media.tv.action.VIEW_RECORDING_SCHEDULES";
     field public static final java.lang.String EXTRA_CHANNEL_ID = "android.media.tv.extra.CHANNEL_ID";
     field public static final java.lang.String EXTRA_PACKAGE_NAME = "android.media.tv.extra.PACKAGE_NAME";
+    field public static final java.lang.String EXTRA_PROGRAM_ID = "android.media.tv.extra.PROGRAM_ID";
     field public static final int INPUT_STATE_CONNECTED = 0; // 0x0
     field public static final int INPUT_STATE_CONNECTED_STANDBY = 1; // 0x1
     field public static final int INPUT_STATE_DISCONNECTED = 2; // 0x2
@@ -43488,6 +43504,7 @@
     method public java.lang.CharSequence getApplicationLabel(android.content.pm.ApplicationInfo);
     method public android.graphics.drawable.Drawable getApplicationLogo(android.content.pm.ApplicationInfo);
     method public android.graphics.drawable.Drawable getApplicationLogo(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException;
+    method public android.content.pm.ChangedPackages getChangedPackages(int);
     method public int getComponentEnabledSetting(android.content.ComponentName);
     method public android.graphics.drawable.Drawable getDefaultActivityIcon();
     method public java.lang.String getDefaultBrowserPackageNameAsUser(int);
@@ -50757,7 +50774,7 @@
 
   public final class TextClassificationManager {
     method public java.util.List<android.view.textclassifier.TextLanguage> detectLanguages(java.lang.CharSequence);
-    method public android.view.textclassifier.TextClassifier getDefaultTextClassifier();
+    method public synchronized android.view.textclassifier.TextClassifier getDefaultTextClassifier();
   }
 
   public final class TextClassificationResult {
diff --git a/api/test-current.txt b/api/test-current.txt
index ebba10e..b38e2b2 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -9953,6 +9953,15 @@
     method public final int compare(android.content.pm.ApplicationInfo, android.content.pm.ApplicationInfo);
   }
 
+  public final class ChangedPackages implements android.os.Parcelable {
+    ctor public ChangedPackages(int, java.util.List<java.lang.String>);
+    method public int describeContents();
+    method public java.util.List<java.lang.String> getPackageNames();
+    method public int getSequenceNumber();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.content.pm.ChangedPackages> CREATOR;
+  }
+
   public class ComponentInfo extends android.content.pm.PackageItemInfo {
     ctor public ComponentInfo();
     ctor public ComponentInfo(android.content.pm.ComponentInfo);
@@ -10300,6 +10309,7 @@
     method public abstract java.lang.CharSequence getApplicationLabel(android.content.pm.ApplicationInfo);
     method public abstract android.graphics.drawable.Drawable getApplicationLogo(android.content.pm.ApplicationInfo);
     method public abstract android.graphics.drawable.Drawable getApplicationLogo(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException;
+    method public abstract android.content.pm.ChangedPackages getChangedPackages(int);
     method public abstract int getComponentEnabledSetting(android.content.ComponentName);
     method public abstract android.graphics.drawable.Drawable getDefaultActivityIcon();
     method public abstract java.lang.String getDefaultBrowserPackageNameAsUser(int);
@@ -14467,6 +14477,7 @@
     field public static final java.lang.String STRING_TYPE_HEART_RATE = "android.sensor.heart_rate";
     field public static final java.lang.String STRING_TYPE_LIGHT = "android.sensor.light";
     field public static final java.lang.String STRING_TYPE_LINEAR_ACCELERATION = "android.sensor.linear_acceleration";
+    field public static final java.lang.String STRING_TYPE_LOW_LATENCY_OFFBODY_DETECT = "android.sensor.low_latency_offbody";
     field public static final java.lang.String STRING_TYPE_MAGNETIC_FIELD = "android.sensor.magnetic_field";
     field public static final java.lang.String STRING_TYPE_MAGNETIC_FIELD_UNCALIBRATED = "android.sensor.magnetic_field_uncalibrated";
     field public static final java.lang.String STRING_TYPE_MOTION_DETECT = "android.sensor.motion_detect";
@@ -14495,6 +14506,7 @@
     field public static final int TYPE_HEART_RATE = 21; // 0x15
     field public static final int TYPE_LIGHT = 5; // 0x5
     field public static final int TYPE_LINEAR_ACCELERATION = 10; // 0xa
+    field public static final int TYPE_LOW_LATENCY_OFFBODY_DETECT = 34; // 0x22
     field public static final int TYPE_MAGNETIC_FIELD = 2; // 0x2
     field public static final int TYPE_MAGNETIC_FIELD_UNCALIBRATED = 14; // 0xe
     field public static final int TYPE_MOTION_DETECT = 30; // 0x1e
@@ -20806,7 +20818,7 @@
     method public int getStreamMaxVolume(int);
     method public int getStreamVolume(int);
     method public deprecated int getVibrateSetting(int);
-    method public boolean isBluetoothA2dpOn();
+    method public deprecated boolean isBluetoothA2dpOn();
     method public boolean isBluetoothScoAvailableOffCall();
     method public boolean isBluetoothScoOn();
     method public boolean isMicrophoneMute();
@@ -22412,6 +22424,7 @@
     method public void setAuxEffectSendLevel(float);
     method public void setBufferingParams(android.media.BufferingParams);
     method public void setDataSource(android.content.Context, android.net.Uri) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
+    method public void setDataSource(android.content.Context, android.net.Uri, java.util.Map<java.lang.String, java.lang.String>, java.util.List<java.net.HttpCookie>) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
     method public void setDataSource(android.content.Context, android.net.Uri, java.util.Map<java.lang.String, java.lang.String>) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
     method public void setDataSource(java.lang.String) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
     method public void setDataSource(android.content.res.AssetFileDescriptor) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException;
@@ -24177,6 +24190,7 @@
     field public static final java.lang.String COLUMN_AUTHOR = "author";
     field public static final java.lang.String COLUMN_AVAILABILITY = "availability";
     field public static final java.lang.String COLUMN_BROADCAST_GENRE = "broadcast_genre";
+    field public static final java.lang.String COLUMN_BROWSABLE = "browsable";
     field public static final java.lang.String COLUMN_DURATION_MILLIS = "duration_millis";
     field public static final java.lang.String COLUMN_END_TIME_UTC_MILLIS = "end_time_utc_millis";
     field public static final deprecated java.lang.String COLUMN_EPISODE_NUMBER = "episode_number";
@@ -24319,11 +24333,13 @@
     field public static final java.lang.String ACTION_BLOCKED_RATINGS_CHANGED = "android.media.tv.action.BLOCKED_RATINGS_CHANGED";
     field public static final java.lang.String ACTION_MAKE_CHANNEL_BROWSABLE = "android.media.tv.action.MAKE_CHANNEL_BROWSABLE";
     field public static final java.lang.String ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED = "android.media.tv.action.PARENTAL_CONTROLS_ENABLED_CHANGED";
+    field public static final java.lang.String ACTION_PROGRAM_BROWSABLE_DISABLED = "android.media.tv.action.PROGRAM_BROWSABLE_DISABLED";
     field public static final java.lang.String ACTION_QUERY_CONTENT_RATING_SYSTEMS = "android.media.tv.action.QUERY_CONTENT_RATING_SYSTEMS";
     field public static final java.lang.String ACTION_SETUP_INPUTS = "android.media.tv.action.SETUP_INPUTS";
     field public static final java.lang.String ACTION_VIEW_RECORDING_SCHEDULES = "android.media.tv.action.VIEW_RECORDING_SCHEDULES";
     field public static final java.lang.String EXTRA_CHANNEL_ID = "android.media.tv.extra.CHANNEL_ID";
     field public static final java.lang.String EXTRA_PACKAGE_NAME = "android.media.tv.extra.PACKAGE_NAME";
+    field public static final java.lang.String EXTRA_PROGRAM_ID = "android.media.tv.extra.PROGRAM_ID";
     field public static final int INPUT_STATE_CONNECTED = 0; // 0x0
     field public static final int INPUT_STATE_CONNECTED_STANDBY = 1; // 0x1
     field public static final int INPUT_STATE_DISCONNECTED = 2; // 0x2
@@ -40200,6 +40216,7 @@
     method public java.lang.CharSequence getApplicationLabel(android.content.pm.ApplicationInfo);
     method public android.graphics.drawable.Drawable getApplicationLogo(android.content.pm.ApplicationInfo);
     method public android.graphics.drawable.Drawable getApplicationLogo(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException;
+    method public android.content.pm.ChangedPackages getChangedPackages(int);
     method public int getComponentEnabledSetting(android.content.ComponentName);
     method public android.graphics.drawable.Drawable getDefaultActivityIcon();
     method public java.lang.String getDefaultBrowserPackageNameAsUser(int);
@@ -47630,7 +47647,7 @@
 
   public final class TextClassificationManager {
     method public java.util.List<android.view.textclassifier.TextLanguage> detectLanguages(java.lang.CharSequence);
-    method public android.view.textclassifier.TextClassifier getDefaultTextClassifier();
+    method public synchronized android.view.textclassifier.TextClassifier getDefaultTextClassifier();
   }
 
   public final class TextClassificationResult {
diff --git a/cmds/pm/src/com/android/commands/pm/Pm.java b/cmds/pm/src/com/android/commands/pm/Pm.java
index 7015381..b336472 100644
--- a/cmds/pm/src/com/android/commands/pm/Pm.java
+++ b/cmds/pm/src/com/android/commands/pm/Pm.java
@@ -53,6 +53,7 @@
 import android.os.HandlerThread;
 import android.os.IUserManager;
 import android.os.ParcelFileDescriptor;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.SELinux;
@@ -1004,7 +1005,8 @@
                 // In non-split user mode, userId can only be SYSTEM
                 int parentUserId = userId >= 0 ? userId : UserHandle.USER_SYSTEM;
                 info = mUm.createRestrictedProfile(name, parentUserId);
-                mAm.addSharedAccountsFromParentUser(parentUserId, userId);
+                mAm.addSharedAccountsFromParentUser(parentUserId, userId,
+                        (Process.myUid() == Process.ROOT_UID) ? "root" : "com.android.shell");
             } else if (userId < 0) {
                 info = mUm.createUser(name, flags);
             } else {
diff --git a/core/java/android/accounts/AccountManager.java b/core/java/android/accounts/AccountManager.java
index 0263681..7d039ef 100644
--- a/core/java/android/accounts/AccountManager.java
+++ b/core/java/android/accounts/AccountManager.java
@@ -1802,7 +1802,7 @@
     public void addSharedAccountsFromParentUser(UserHandle parentUser, UserHandle user) {
         try {
             mService.addSharedAccountsFromParentUser(parentUser.getIdentifier(),
-                    user.getIdentifier());
+                    user.getIdentifier(), mContext.getOpPackageName());
         } catch (RemoteException re) {
             throw re.rethrowFromSystemServer();
         }
diff --git a/core/java/android/accounts/IAccountManager.aidl b/core/java/android/accounts/IAccountManager.aidl
index e0fdac1..49cd2c6 100644
--- a/core/java/android/accounts/IAccountManager.aidl
+++ b/core/java/android/accounts/IAccountManager.aidl
@@ -80,7 +80,7 @@
     /* Shared accounts */
     Account[] getSharedAccountsAsUser(int userId);
     boolean removeSharedAccountAsUser(in Account account, int userId);
-    void addSharedAccountsFromParentUser(int parentUserId, int userId);
+    void addSharedAccountsFromParentUser(int parentUserId, int userId, String opPackageName);
 
     /* Account renaming. */
     void renameAccount(in IAccountManagerResponse response, in Account accountToRename, String newName);
diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java
index 78cd89b..fca26f8 100644
--- a/core/java/android/animation/AnimatorSet.java
+++ b/core/java/android/animation/AnimatorSet.java
@@ -1865,7 +1865,7 @@
             if (mEvent == ANIMATION_START) {
                 return mNode.mStartTime;
             } else if (mEvent == ANIMATION_DELAY_ENDED) {
-                return mNode.mStartTime = mNode.mStartTime == DURATION_INFINITE
+                return mNode.mStartTime == DURATION_INFINITE
                         ? DURATION_INFINITE : mNode.mStartTime + mNode.mAnimation.getStartDelay();
             } else {
                 return mNode.mEndTime;
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 376823e..1f8e6db 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -5726,7 +5726,7 @@
         // Preload fonts resources
         try {
             final ApplicationInfo info =
-                    sPackageManager.getApplicationInfo(
+                    getPackageManager().getApplicationInfo(
                             data.appInfo.packageName,
                             PackageManager.GET_META_DATA /*flags*/,
                             UserHandle.myUserId());
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index 0c6c4ba..333e412 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -29,6 +29,7 @@
 import android.content.IntentSender;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.ChangedPackages;
 import android.content.pm.ComponentInfo;
 import android.content.pm.InstantAppInfo;
 import android.content.pm.FeatureInfo;
@@ -506,6 +507,15 @@
     }
 
     @Override
+    public ChangedPackages getChangedPackages(int sequenceNumber) {
+        try {
+            return mPM.getChangedPackages(sequenceNumber, mContext.getUserId());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @Override
     @SuppressWarnings("unchecked")
     public FeatureInfo[] getSystemAvailableFeatures() {
         try {
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 7900fc5..812daf8 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -3684,7 +3684,8 @@
                         mContext, backgroundColor);
                 mSecondaryTextColor = NotificationColorUtil.resolveSecondaryColor(
                         mContext, backgroundColor);
-                mActionBarColor = NotificationColorUtil.resolveActionBarColor(backgroundColor);
+                mActionBarColor = NotificationColorUtil.resolveActionBarColor(mContext,
+                        backgroundColor);
             }
         }
 
@@ -4082,16 +4083,23 @@
 
         /**
          * Construct a RemoteViews for the final heads-up notification layout.
+         *
+         * @param increasedHeight true if this layout be created with an increased height. Some
+         * styles may support showing more then just that basic 1U size
+         * and the system may decide to render important notifications
+         * slightly bigger even when collapsed.
+         *
+         * @hide
          */
-        public RemoteViews createHeadsUpContentView() {
+        public RemoteViews createHeadsUpContentView(boolean increasedHeight) {
             if (mN.headsUpContentView != null
                     && (mStyle == null ||  !mStyle.displayCustomViewInline())) {
                 return mN.headsUpContentView;
             } else if (mStyle != null) {
-                    final RemoteViews styleView = mStyle.makeHeadsUpContentView();
-                    if (styleView != null) {
-                        return styleView;
-                    }
+                final RemoteViews styleView = mStyle.makeHeadsUpContentView(increasedHeight);
+                if (styleView != null) {
+                    return styleView;
+                }
             } else if (mActions.size() == 0) {
                 return null;
             }
@@ -4100,6 +4108,13 @@
         }
 
         /**
+         * Construct a RemoteViews for the final heads-up notification layout.
+         */
+        public RemoteViews createHeadsUpContentView() {
+            return createHeadsUpContentView(false /* useIncreasedHeight */);
+        }
+
+        /**
          * Construct a RemoteViews for the display in public contexts like on the lockscreen.
          *
          * @hide
@@ -4152,12 +4167,21 @@
                     mN.extras.putCharSequence(EXTRA_SUB_TEXT, newSummary);
                 }
             }
+            Boolean colorized = (Boolean) mN.extras.get(EXTRA_COLORIZED);
+            mN.extras.putBoolean(EXTRA_COLORIZED, false);
+
             RemoteViews header = makeNotificationHeader();
+
             if (summary != null) {
                 mN.extras.putCharSequence(EXTRA_SUB_TEXT, summary);
             } else {
                 mN.extras.remove(EXTRA_SUB_TEXT);
             }
+            if (colorized != null) {
+                mN.extras.putBoolean(EXTRA_COLORIZED, colorized);
+            } else {
+                mN.extras.remove(EXTRA_COLORIZED);
+            }
             mN.color = color;
             return header;
         }
@@ -4823,9 +4847,11 @@
 
         /**
          * Construct a Style-specific RemoteViews for the final HUN layout.
+         *
+         * @param increasedHeight true if this layout be created with an increased height.
          * @hide
          */
-        public RemoteViews makeHeadsUpContentView() {
+        public RemoteViews makeHeadsUpContentView(boolean increasedHeight) {
             return null;
         }
 
@@ -5171,6 +5197,17 @@
         /**
          * @hide
          */
+        @Override
+        public RemoteViews makeHeadsUpContentView(boolean increasedHeight) {
+            if (increasedHeight && mBuilder.mActions.size() > 0) {
+                return makeBigContentView();
+            }
+            return super.makeHeadsUpContentView(increasedHeight);
+        }
+
+        /**
+         * @hide
+         */
         public RemoteViews makeBigContentView() {
 
             // Nasty
@@ -5578,7 +5615,10 @@
          * @hide
          */
         @Override
-        public RemoteViews makeHeadsUpContentView() {
+        public RemoteViews makeHeadsUpContentView(boolean increasedHeight) {
+            if (increasedHeight) {
+                return makeBigContentView();
+            }
             Message m = findLatestIncomingMessage();
             CharSequence title = mConversationTitle != null
                     ? mConversationTitle
@@ -6028,7 +6068,7 @@
          * @hide
          */
         @Override
-        public RemoteViews makeHeadsUpContentView() {
+        public RemoteViews makeHeadsUpContentView(boolean increasedHeight) {
             RemoteViews expanded = makeMediaBigContentView();
             return expanded != null ? expanded : makeMediaContentView();
         }
@@ -6208,7 +6248,7 @@
          * @hide
          */
         @Override
-        public RemoteViews makeHeadsUpContentView() {
+        public RemoteViews makeHeadsUpContentView(boolean increasedHeight) {
             return makeDecoratedHeadsUpContentView();
         }
 
@@ -6344,7 +6384,7 @@
          * @hide
          */
         @Override
-        public RemoteViews makeHeadsUpContentView() {
+        public RemoteViews makeHeadsUpContentView(boolean increasedHeight) {
             RemoteViews customRemoteView = mBuilder.mN.headsUpContentView != null
                     ? mBuilder.mN.headsUpContentView
                     : mBuilder.mN.contentView;
diff --git a/core/java/android/app/QueuedWork.java b/core/java/android/app/QueuedWork.java
index 6ee4780..a38fd43 100644
--- a/core/java/android/app/QueuedWork.java
+++ b/core/java/android/app/QueuedWork.java
@@ -16,86 +16,249 @@
 
 package android.app;
 
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.LinkedList;
 
 /**
- * Internal utility class to keep track of process-global work that's
- * outstanding and hasn't been finished yet.
+ * Internal utility class to keep track of process-global work that's outstanding and hasn't been
+ * finished yet.
  *
- * This was created for writing SharedPreference edits out
- * asynchronously so we'd have a mechanism to wait for the writes in
- * Activity.onPause and similar places, but we may use this mechanism
- * for other things in the future.
+ * New work will be {@link #queue queued}.
+ *
+ * It is possible to add 'finisher'-runnables that are {@link #waitToFinish guaranteed to be run}.
+ * This is used to make sure the work has been finished.
+ *
+ * This was created for writing SharedPreference edits out asynchronously so we'd have a mechanism
+ * to wait for the writes in Activity.onPause and similar places, but we may use this mechanism for
+ * other things in the future.
+ *
+ * The queued asynchronous work is performed on a separate, dedicated thread.
  *
  * @hide
  */
 public class QueuedWork {
+    private static final String LOG_TAG = QueuedWork.class.getSimpleName();
+    private static final boolean DEBUG = true;
 
-    // The set of Runnables that will finish or wait on any async
-    // activities started by the application.
-    private static final ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers =
-            new ConcurrentLinkedQueue<Runnable>();
+    /** Delay for delayed runnables, as big as possible but low enough to be barely perceivable */
+    private static final long DELAY = 100;
 
-    private static ExecutorService sSingleThreadExecutor = null; // lazy, guarded by class
+    /** Lock for this class */
+    private static final Object sLock = new Object();
 
     /**
-     * Returns a single-thread Executor shared by the entire process,
-     * creating it if necessary.
+     * Used to make sure that only one thread is processing work items at a time. This means that
+     * they are processed in the order added.
+     *
+     * This is separate from {@link #sLock} as this is held the whole time while work is processed
+     * and we do not want to stall the whole class.
      */
-    public static ExecutorService singleThreadExecutor() {
-        synchronized (QueuedWork.class) {
-            if (sSingleThreadExecutor == null) {
-                // TODO: can we give this single thread a thread name?
-                sSingleThreadExecutor = Executors.newSingleThreadExecutor();
+    private static Object sProcessingWork = new Object();
+
+    /** Finishers {@link #addFinisher added} and not yet {@link #removeFinisher removed} */
+    @GuardedBy("sLock")
+    private static final LinkedList<Runnable> sFinishers = new LinkedList<>();
+
+    /** {@link #getHandler() Lazily} created handler */
+    @GuardedBy("sLock")
+    private static Handler sHandler = null;
+
+    /** Work queued via {@link #queue} */
+    @GuardedBy("sLock")
+    private static final LinkedList<Runnable> sWork = new LinkedList<>();
+
+    /** If new work can be delayed or not */
+    @GuardedBy("sLock")
+    private static boolean sCanDelay = true;
+
+    /**
+     * Lazily create a handler on a separate thread.
+     *
+     * @return the handler
+     */
+    private static Handler getHandler() {
+        synchronized (sLock) {
+            if (sHandler == null) {
+                HandlerThread handlerThread = new HandlerThread("queued-work-looper",
+                        Process.THREAD_PRIORITY_FOREGROUND);
+                handlerThread.start();
+
+                sHandler = new QueuedWorkHandler(handlerThread.getLooper());
             }
-            return sSingleThreadExecutor;
+            return sHandler;
         }
     }
 
     /**
-     * Add a runnable to finish (or wait for) a deferred operation
-     * started in this context earlier.  Typically finished by e.g.
-     * an Activity#onPause.  Used by SharedPreferences$Editor#startCommit().
+     * Add a finisher-runnable to wait for {@link #queue asynchronously processed work}.
      *
-     * Note that this doesn't actually start it running.  This is just
-     * a scratch set for callers doing async work to keep updated with
-     * what's in-flight.  In the common case, caller code
-     * (e.g. SharedPreferences) will pretty quickly call remove()
-     * after an add().  The only time these Runnables are run is from
-     * waitToFinish(), below.
+     * Used by SharedPreferences$Editor#startCommit().
+     *
+     * Note that this doesn't actually start it running.  This is just a scratch set for callers
+     * doing async work to keep updated with what's in-flight. In the common case, caller code
+     * (e.g. SharedPreferences) will pretty quickly call remove() after an add(). The only time
+     * these Runnables are run is from {@link #waitToFinish}.
+     *
+     * @param finisher The runnable to add as finisher
      */
-    public static void add(Runnable finisher) {
-        sPendingWorkFinishers.add(finisher);
-    }
-
-    public static void remove(Runnable finisher) {
-        sPendingWorkFinishers.remove(finisher);
+    public static void addFinisher(Runnable finisher) {
+        synchronized (sLock) {
+            sFinishers.add(finisher);
+        }
     }
 
     /**
-     * Finishes or waits for async operations to complete.
-     * (e.g. SharedPreferences$Editor#startCommit writes)
+     * Remove a previously {@link #addFinisher added} finisher-runnable.
      *
-     * Is called from the Activity base class's onPause(), after
-     * BroadcastReceiver's onReceive, after Service command handling,
-     * etc.  (so async work is never lost)
+     * @param finisher The runnable to remove.
+     */
+    public static void removeFinisher(Runnable finisher) {
+        synchronized (sLock) {
+            sFinishers.remove(finisher);
+        }
+    }
+
+    /**
+     * Trigger queued work to be processed immediately. The queued work is processed on a separate
+     * thread asynchronous. While doing that run and process all finishers on this thread. The
+     * finishers can be implemented in a way to check weather the queued work is finished.
+     *
+     * Is called from the Activity base class's onPause(), after BroadcastReceiver's onReceive,
+     * after Service command handling, etc. (so async work is never lost)
      */
     public static void waitToFinish() {
-        Runnable toFinish;
-        while ((toFinish = sPendingWorkFinishers.poll()) != null) {
-            toFinish.run();
+        long startTime = 0;
+        boolean hadMessages = false;
+
+        if (DEBUG) {
+            startTime = System.currentTimeMillis();
+        }
+
+        Handler handler = getHandler();
+
+        synchronized (sLock) {
+            if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
+                // Delayed work will be processed at processPendingWork() below
+                handler.removeMessages(QueuedWorkHandler.MSG_RUN);
+
+                if (DEBUG) {
+                    hadMessages = true;
+                    Log.d(LOG_TAG, "waiting");
+                }
+            }
+
+            // We should not delay any work as this might delay the finishers
+            sCanDelay = false;
+        }
+
+        processPendingWork();
+
+        try {
+            while (true) {
+                Runnable finisher;
+
+                synchronized (sLock) {
+                    finisher = sFinishers.poll();
+                }
+
+                if (finisher == null) {
+                    break;
+                }
+
+                finisher.run();
+            }
+        } finally {
+            sCanDelay = true;
+        }
+
+        if (DEBUG) {
+            long waitTime = System.currentTimeMillis() - startTime;
+
+            if (waitTime > 0 || hadMessages) {
+                Log.d(LOG_TAG, "waited " + waitTime + " ms");
+            }
         }
     }
-    
+
     /**
-     * Returns true if there is pending work to be done.  Note that the
-     * result is out of data as soon as you receive it, so be careful how you
-     * use it.
+     * Queue a work-runnable for processing asynchronously.
+     *
+     * @param work The new runnable to process
+     * @param shouldDelay If the message should be delayed
+     */
+    public static void queue(Runnable work, boolean shouldDelay) {
+        Handler handler = getHandler();
+
+        synchronized (sLock) {
+            sWork.add(work);
+
+            if (shouldDelay && sCanDelay) {
+                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
+            } else {
+                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
+            }
+        }
+    }
+
+    /**
+     * @return True iff there is any {@link #queue async work queued}.
      */
     public static boolean hasPendingWork() {
-        return !sPendingWorkFinishers.isEmpty();
+        synchronized (sLock) {
+            return !sWork.isEmpty();
+        }
     }
-    
+
+    private static void processPendingWork() {
+        long startTime = 0;
+
+        if (DEBUG) {
+            startTime = System.currentTimeMillis();
+        }
+
+        synchronized (sProcessingWork) {
+            LinkedList<Runnable> work;
+
+            synchronized (sLock) {
+                work = (LinkedList<Runnable>) sWork.clone();
+                sWork.clear();
+
+                // Remove all msg-s as all work will be processed now
+                getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
+            }
+
+            if (work.size() > 0) {
+                for (Runnable w : work) {
+                    w.run();
+                }
+
+                if (DEBUG) {
+                    Log.d(LOG_TAG, "processing " + work.size() + " items took " +
+                            +(System.currentTimeMillis() - startTime) + " ms");
+                }
+            }
+        }
+    }
+
+    private static class QueuedWorkHandler extends Handler {
+        static final int MSG_RUN = 1;
+
+        QueuedWorkHandler(Looper looper) {
+            super(looper);
+        }
+
+        public void handleMessage(Message msg) {
+            if (msg.what == MSG_RUN) {
+                processPendingWork();
+            }
+        }
+    }
 }
diff --git a/core/java/android/app/SharedPreferencesImpl.java b/core/java/android/app/SharedPreferencesImpl.java
index 3bb7019..11ba7ee 100644
--- a/core/java/android/app/SharedPreferencesImpl.java
+++ b/core/java/android/app/SharedPreferencesImpl.java
@@ -53,7 +53,7 @@
 
 final class SharedPreferencesImpl implements SharedPreferences {
     private static final String TAG = "SharedPreferencesImpl";
-    private static final boolean DEBUG = false;
+    private static final boolean DEBUG = true;
     private static final Object CONTENT = new Object();
 
     // Lock ordering rules:
@@ -318,6 +318,7 @@
 
         @GuardedBy("mWritingToDiskLock")
         volatile boolean writeToDiskResult = false;
+        boolean wasWritten = false;
 
         private MemoryCommitResult(long memoryStateGeneration, @Nullable List<String> keysModified,
                 @Nullable Set<OnSharedPreferenceChangeListener> listeners,
@@ -328,7 +329,8 @@
             this.mapToWriteToDisk = mapToWriteToDisk;
         }
 
-        void setDiskWriteResult(boolean result) {
+        void setDiskWriteResult(boolean wasWritten, boolean result) {
+            this.wasWritten = wasWritten;
             writeToDiskResult = result;
             writtenToDiskLatch.countDown();
         }
@@ -396,6 +398,8 @@
         }
 
         public void apply() {
+            final long startTime = System.currentTimeMillis();
+
             final MemoryCommitResult mcr = commitToMemory();
             final Runnable awaitCommit = new Runnable() {
                     public void run() {
@@ -403,15 +407,21 @@
                             mcr.writtenToDiskLatch.await();
                         } catch (InterruptedException ignored) {
                         }
+
+                        if (DEBUG && mcr.wasWritten) {
+                            Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+                                    + " applied after " + (System.currentTimeMillis() - startTime)
+                                    + " ms");
+                        }
                     }
                 };
 
-            QueuedWork.add(awaitCommit);
+            QueuedWork.addFinisher(awaitCommit);
 
             Runnable postWriteRunnable = new Runnable() {
                     public void run() {
                         awaitCommit.run();
-                        QueuedWork.remove(awaitCommit);
+                        QueuedWork.removeFinisher(awaitCommit);
                     }
                 };
 
@@ -503,13 +513,26 @@
         }
 
         public boolean commit() {
+            long startTime = 0;
+
+            if (DEBUG) {
+                startTime = System.currentTimeMillis();
+            }
+
             MemoryCommitResult mcr = commitToMemory();
+
             SharedPreferencesImpl.this.enqueueDiskWrite(
                 mcr, null /* sync write on this thread okay */);
             try {
                 mcr.writtenToDiskLatch.await();
             } catch (InterruptedException e) {
                 return false;
+            } finally {
+                if (DEBUG) {
+                    Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+                            + " committed after " + (System.currentTimeMillis() - startTime)
+                            + " ms");
+                }
             }
             notifyListeners(mcr);
             return mcr.writeToDiskResult;
@@ -587,11 +610,7 @@
             }
         }
 
-        if (DEBUG) {
-            Log.d(TAG, "added " + mcr.memoryStateGeneration + " -> " + mFile.getName());
-        }
-
-        QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
+        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
     }
 
     private static FileOutputStream createFileOutputStream(File file) {
@@ -619,8 +638,31 @@
 
     // Note: must hold mWritingToDiskLock
     private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
+        long startTime = 0;
+        long existsTime = 0;
+        long backupExistsTime = 0;
+        long outputStreamCreateTime = 0;
+        long writeTime = 0;
+        long fsyncTime = 0;
+        long setPermTime = 0;
+        long fstatTime = 0;
+        long deleteTime = 0;
+
+        if (DEBUG) {
+            startTime = System.currentTimeMillis();
+        }
+
+        boolean fileExists = mFile.exists();
+
+        if (DEBUG) {
+            existsTime = System.currentTimeMillis();
+
+            // Might not be set, hence init them to a default value
+            backupExistsTime = existsTime;
+        }
+
         // Rename the current file so it may be used as a backup during the next read
-        if (mFile.exists()) {
+        if (fileExists) {
             boolean needsWrite = false;
 
             // Only need to write if the disk state is older than this commit
@@ -639,18 +681,21 @@
             }
 
             if (!needsWrite) {
-                if (DEBUG) {
-                    Log.d(TAG, "skipped " + mcr.memoryStateGeneration + " -> " + mFile.getName());
-                }
-                mcr.setDiskWriteResult(true);
+                mcr.setDiskWriteResult(false, true);
                 return;
             }
 
-            if (!mBackupFile.exists()) {
+            boolean backupFileExists = mBackupFile.exists();
+
+            if (DEBUG) {
+                backupExistsTime = System.currentTimeMillis();
+            }
+
+            if (!backupFileExists) {
                 if (!mFile.renameTo(mBackupFile)) {
                     Log.e(TAG, "Couldn't rename file " + mFile
                           + " to backup file " + mBackupFile);
-                    mcr.setDiskWriteResult(false);
+                    mcr.setDiskWriteResult(false, false);
                     return;
                 }
             } else {
@@ -663,19 +708,34 @@
         // from the backup.
         try {
             FileOutputStream str = createFileOutputStream(mFile);
+
+            if (DEBUG) {
+                outputStreamCreateTime = System.currentTimeMillis();
+            }
+
             if (str == null) {
-                mcr.setDiskWriteResult(false);
+                mcr.setDiskWriteResult(false, false);
                 return;
             }
             XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
+
+            if (DEBUG) {
+                writeTime = System.currentTimeMillis();
+            }
+
             FileUtils.sync(str);
 
             if (DEBUG) {
-                Log.d(TAG, "wrote " + mcr.memoryStateGeneration + " -> " + mFile.getName());
+                fsyncTime = System.currentTimeMillis();
             }
 
             str.close();
             ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
+
+            if (DEBUG) {
+                setPermTime = System.currentTimeMillis();
+            }
+
             try {
                 final StructStat stat = Os.stat(mFile.getPath());
                 synchronized (mLock) {
@@ -685,12 +745,30 @@
             } catch (ErrnoException e) {
                 // Do nothing
             }
+
+            if (DEBUG) {
+                fstatTime = System.currentTimeMillis();
+            }
+
             // Writing was successful, delete the backup file if there is one.
             mBackupFile.delete();
 
+            if (DEBUG) {
+                deleteTime = System.currentTimeMillis();
+            }
+
             mDiskStateGeneration = mcr.memoryStateGeneration;
 
-            mcr.setDiskWriteResult(true);
+            mcr.setDiskWriteResult(true, true);
+
+            Log.d(TAG, "write: " + (existsTime - startTime) + "/"
+                    + (backupExistsTime - startTime) + "/"
+                    + (outputStreamCreateTime - startTime) + "/"
+                    + (writeTime - startTime) + "/"
+                    + (fsyncTime - startTime) + "/"
+                    + (setPermTime - startTime) + "/"
+                    + (fstatTime - startTime) + "/"
+                    + (deleteTime - startTime));
 
             return;
         } catch (XmlPullParserException e) {
@@ -698,12 +776,13 @@
         } catch (IOException e) {
             Log.w(TAG, "writeToFile: Got exception:", e);
         }
+
         // Clean up an unsuccessfully written file
         if (mFile.exists()) {
             if (!mFile.delete()) {
                 Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
             }
         }
-        mcr.setDiskWriteResult(false);
+        mcr.setDiskWriteResult(false, false);
     }
 }
diff --git a/core/java/android/app/assist/AssistStructure.java b/core/java/android/app/assist/AssistStructure.java
index 08aa5f2..6591fc9 100644
--- a/core/java/android/app/assist/AssistStructure.java
+++ b/core/java/android/app/assist/AssistStructure.java
@@ -420,18 +420,17 @@
             mRoot = new ViewNode();
 
             ViewNodeBuilder builder = new ViewNodeBuilder(assist, mRoot, false, 0);
-            if ((root.getWindowFlags()& WindowManager.LayoutParams.FLAG_SECURE) != 0) {
-                // This is a secure window, so it doesn't want a screenshot, and that
-                // means we should also not copy out its view hierarchy.
-
+            if ((root.getWindowFlags() & WindowManager.LayoutParams.FLAG_SECURE) != 0) {
                 if (forAutoFill) {
                     // NOTE: flags are currently not supported, hence 0
                     view.onProvideAutoFillStructure(builder, 0);
                 } else {
+                    // This is a secure window, so it doesn't want a screenshot, and that
+                    // means we should also not copy out its view hierarchy for Assist
                     view.onProvideStructure(builder);
+                    builder.setAssistBlocked(true);
+                    return;
                 }
-                builder.setAssistBlocked(true);
-                return;
             }
             if (forAutoFill) {
                 // NOTE: flags are currently not supported, hence 0
diff --git a/core/java/android/content/BroadcastReceiver.java b/core/java/android/content/BroadcastReceiver.java
index 485d078..c3d6606 100644
--- a/core/java/android/content/BroadcastReceiver.java
+++ b/core/java/android/content/BroadcastReceiver.java
@@ -211,16 +211,16 @@
                     // of the list to finish the broadcast, so we don't block this
                     // thread (which may be the main thread) to have it finished.
                     //
-                    // Note that we don't need to use QueuedWork.add() with the
+                    // Note that we don't need to use QueuedWork.addFinisher() with the
                     // runnable, since we know the AM is waiting for us until the
                     // executor gets to it.
-                    QueuedWork.singleThreadExecutor().execute( new Runnable() {
+                    QueuedWork.queue(new Runnable() {
                         @Override public void run() {
                             if (ActivityThread.DEBUG_BROADCAST) Slog.i(ActivityThread.TAG,
                                     "Finishing broadcast after work to component " + mToken);
                             sendFinished(mgr);
                         }
-                    });
+                    }, false);
                 } else {
                     if (ActivityThread.DEBUG_BROADCAST) Slog.i(ActivityThread.TAG,
                             "Finishing broadcast to component " + mToken);
diff --git a/core/java/android/content/pm/ChangedPackages.aidl b/core/java/android/content/pm/ChangedPackages.aidl
new file mode 100644
index 0000000..1a9f5a1
--- /dev/null
+++ b/core/java/android/content/pm/ChangedPackages.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2017, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm;
+
+parcelable ChangedPackages;
\ No newline at end of file
diff --git a/core/java/android/content/pm/ChangedPackages.java b/core/java/android/content/pm/ChangedPackages.java
new file mode 100644
index 0000000..94b8a5d
--- /dev/null
+++ b/core/java/android/content/pm/ChangedPackages.java
@@ -0,0 +1,82 @@
+/**
+ * Copyright (c) 2017, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.content.pm;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Intent;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.List;
+
+/**
+ * Packages that have been changed since the last time they
+ * were requested.
+ */
+public final class ChangedPackages implements Parcelable {
+    /** The last known sequence number for these changes */
+    private final int mSequenceNumber;
+    /** The names of the packages that have changed */
+    private final List<String> mPackageNames;
+
+    public ChangedPackages(int sequenceNumber, @NonNull List<String> packageNames) {
+        this.mSequenceNumber = sequenceNumber;
+        this.mPackageNames = packageNames;
+    }
+
+    /** @hide */
+    protected ChangedPackages(Parcel in) {
+        mSequenceNumber = in.readInt();
+        mPackageNames = in.createStringArrayList();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mSequenceNumber);
+        dest.writeStringList(mPackageNames);
+    }
+
+    /**
+     * Returns the last known sequence number for these changes.
+     */
+    public int getSequenceNumber() {
+        return mSequenceNumber;
+    }
+
+    /**
+     * Returns the names of the packages that have changed.
+     */
+    public @NonNull List<String> getPackageNames() {
+        return mPackageNames;
+    }
+
+    public static final Parcelable.Creator<ChangedPackages> CREATOR =
+            new Parcelable.Creator<ChangedPackages>() {
+        public ChangedPackages createFromParcel(Parcel in) {
+            return new ChangedPackages(in);
+        }
+
+        public ChangedPackages[] newArray(int size) {
+            return new ChangedPackages[size];
+        }
+    };
+}
diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl
index 3fb46cf..9d36a73 100644
--- a/core/java/android/content/pm/IPackageManager.aidl
+++ b/core/java/android/content/pm/IPackageManager.aidl
@@ -23,6 +23,7 @@
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.ContainerEncryptionParams;
+import android.content.pm.ChangedPackages;
 import android.content.pm.InstantAppInfo;
 import android.content.pm.FeatureInfo;
 import android.content.pm.IPackageInstallObserver2;
@@ -611,6 +612,8 @@
     String getServicesSystemSharedLibraryPackageName();
     String getSharedSystemSharedLibraryPackageName();
 
+    ChangedPackages getChangedPackages(int sequenceNumber, int userId);
+
     boolean isPackageDeviceAdminOnAnyUser(String packageName);
 
     List<String> getPreviousCodePaths(in String packageName);
diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java
index c8f6406..40deeae 100644
--- a/core/java/android/content/pm/LauncherApps.java
+++ b/core/java/android/content/pm/LauncherApps.java
@@ -37,6 +37,7 @@
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
+import android.graphics.drawable.MaskableIconDrawable;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
@@ -803,7 +804,15 @@
             }
             try {
                 final Bitmap bmp = BitmapFactory.decodeFileDescriptor(pfd.getFileDescriptor());
-                return (bmp == null) ? null : new BitmapDrawable(mContext.getResources(), bmp);
+                if (bmp != null) {
+                    BitmapDrawable dr = new BitmapDrawable(mContext.getResources(), bmp);
+                    if (shortcut.hasMaskableBitmap()) {
+                        return new MaskableIconDrawable(null, dr);
+                    } else {
+                        return dr;
+                    }
+                }
+                return null;
             } finally {
                 try {
                     pfd.close();
@@ -821,7 +830,8 @@
                     return loadDrawableResourceFromPackage(shortcut.getPackage(),
                             icon.getResId(), shortcut.getUserHandle(), density);
                 }
-                case Icon.TYPE_BITMAP: {
+                case Icon.TYPE_BITMAP:
+                case Icon.TYPE_BITMAP_MASKABLE: {
                     return icon.loadDrawable(mContext);
                 }
                 default:
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index b20b5e2..308153d 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -20,6 +20,7 @@
 import android.annotation.CheckResult;
 import android.annotation.DrawableRes;
 import android.annotation.IntDef;
+import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
@@ -3853,6 +3854,17 @@
     public abstract @NonNull String getSharedSystemSharedLibraryPackageName();
 
     /**
+     * Returns the names of the packages that have been changed
+     * [eg. added, removed or updated] since the given sequence
+     * number.
+     * <p>If no packages have been changed, returns <code>null</code>.
+     * <p>The sequence number starts at <code>0</code> and is
+     * reset every boot.
+     */
+    public abstract @Nullable ChangedPackages getChangedPackages(
+            @IntRange(from=0) int sequenceNumber);
+
+    /**
      * Get a list of features that are available on the
      * system.
      *
diff --git a/core/java/android/content/pm/ShortcutInfo.java b/core/java/android/content/pm/ShortcutInfo.java
index b4dcdf7..f1f2683 100644
--- a/core/java/android/content/pm/ShortcutInfo.java
+++ b/core/java/android/content/pm/ShortcutInfo.java
@@ -92,6 +92,9 @@
     public static final int FLAG_IMMUTABLE = 1 << 8;
 
     /** @hide */
+    public static final int FLAG_MASKABLE_BITMAP = 1 << 9;
+
+    /** @hide */
     @IntDef(flag = true,
             value = {
             FLAG_DYNAMIC,
@@ -103,6 +106,7 @@
             FLAG_DISABLED,
             FLAG_STRINGS_RESOLVED,
             FLAG_IMMUTABLE,
+            FLAG_MASKABLE_BITMAP,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ShortcutFlags {}
@@ -690,6 +694,7 @@
         switch (icon.getType()) {
             case Icon.TYPE_RESOURCE:
             case Icon.TYPE_BITMAP:
+            case Icon.TYPE_BITMAP_MASKABLE:
                 break; // OK
             default:
                 throw getInvalidIconException();
@@ -815,8 +820,9 @@
          * <p>Tints set with {@link Icon#setTint} or {@link Icon#setTintList} are not supported
          * and will be ignored.
          *
-         * <p>Only icons created with {@link Icon#createWithBitmap(Bitmap)} and
-         * {@link Icon#createWithResource} are supported.
+         * <p>Only icons created with {@link Icon#createWithBitmap(Bitmap)},
+         * {@link Icon#createWithMaskableBitmap(Bitmap)}
+         * and {@link Icon#createWithResource} are supported.
          * Other types, such as URI-based icons, are not supported.
          *
          * @see LauncherApps#getShortcutIconDrawable(ShortcutInfo, int)
@@ -1442,6 +1448,15 @@
     }
 
     /**
+     * Return whether a shortcut's icon is maskable.
+     *
+     * @hide internal/unit tests only
+     */
+    public boolean hasMaskableBitmap() {
+        return hasFlags(FLAG_MASKABLE_BITMAP);
+    }
+
+    /**
      * Return whether a shortcut only contains "key" information only or not.  If true, only the
      * following fields are available.
      * <ul>
diff --git a/core/java/android/hardware/Sensor.java b/core/java/android/hardware/Sensor.java
index d87c55e..8c13cc8 100644
--- a/core/java/android/hardware/Sensor.java
+++ b/core/java/android/hardware/Sensor.java
@@ -649,9 +649,22 @@
      * to be in the same order as the HAL. Skipping this sensor
      */
 
-    /* TYPE_LOW_LATENCY_OFF_BODY_SENSOR - defined as type 34 in the HAL needs to
-     * be defined in this space.
+    /**
+     * A constant describing a low latency off-body detect sensor.
+     *
+     * See {@link android.hardware.SensorEvent#values SensorEvent.values} for more details.
+     *
      */
+    public static final int TYPE_LOW_LATENCY_OFFBODY_DETECT = 34;
+
+
+    /**
+     * A constant string describing a low-latency offbody detector sensor.
+     *
+     * @see #TYPE_LOW_LATENCY_OFFBODY_DETECT
+     */
+    public static final String STRING_TYPE_LOW_LATENCY_OFFBODY_DETECT =
+            "android.sensor.low_latency_offbody";
 
     /**
      * A constant describing an uncalibrated accelerometer sensor.
@@ -669,6 +682,7 @@
      */
     public static final String STRING_TYPE_ACCELEROMETER_UNCALIBRATED =
             "android.sensor.accelerometer_uncalibrated";
+
     /**
      * A constant describing all sensor types.
      */
@@ -786,7 +800,7 @@
             1, // SENSOR_TYPE_HEART_BEAT
             2, // SENSOR_TYPE_DYNAMIC_SENSOR_META
             16,// skip over additional sensor info type
-            1, // reserving for LLOB sensor type
+            1, // SENSOR_TYPE_LOW_LATENCY_OFFBODY_DETECT
             6, // SENSOR_TYPE_ACCELEROMETER_UNCALIBRATED
     };
 
@@ -1195,6 +1209,9 @@
             case TYPE_DYNAMIC_SENSOR_META:
                 mStringType = STRING_TYPE_DYNAMIC_SENSOR_META;
                 return true;
+            case TYPE_LOW_LATENCY_OFFBODY_DETECT:
+                mStringType = STRING_TYPE_LOW_LATENCY_OFFBODY_DETECT;
+                return true;
             case TYPE_ACCELEROMETER_UNCALIBRATED:
                 mStringType = STRING_TYPE_ACCELEROMETER_UNCALIBRATED;
                 return true;
diff --git a/core/java/android/hardware/SensorEvent.java b/core/java/android/hardware/SensorEvent.java
index 9b72757a..c0bca97 100644
--- a/core/java/android/hardware/SensorEvent.java
+++ b/core/java/android/hardware/SensorEvent.java
@@ -564,6 +564,42 @@
      * completely unlikely to be anywhere else on the QRS complex.
      * </p>
      *
+     * <h4>{@link android.hardware.Sensor#TYPE_LOW_LATENCY_OFFBODY_DETECT
+     * Sensor.TYPE_LOW_LATENCY_OFFBODY_DETECT}:</h4>
+     *
+     * <p>
+     * A sensor of this type returns an event every time the device transitions
+     * from off-body to on-body and from on-body to off-body (e.g. a wearable
+     * device being removed from the wrist would trigger an event indicating an
+     * off-body transition). The event returned will contain a single value to
+     * indicate off-body state:
+     * </p>
+     *
+     * <ul>
+     *  <li> values[0]: off-body state</li>
+     * </ul>
+     *
+     * <p>
+     *     Valid values for off-body state:
+     * <ul>
+     *  <li> 1.0 (device is on-body)</li>
+     *  <li> 0.0 (device is off-body)</li>
+     * </ul>
+     * </p>
+     *
+     * <p>
+     * When a sensor of this type is activated, it must deliver the initial
+     * on-body or off-body event representing the current device state within
+     * 5 seconds of activating the sensor.
+     * </p>
+     *
+     * <p>
+     * This sensor must be able to detect and report an on-body to off-body
+     * transition within 1 second of the device being removed from the body,
+     * and must be able to detect and report an off-body to on-body transition
+     * within 5 seconds of the device being put back onto the body.
+     * </p>
+     *
      * <h4>{@link android.hardware.Sensor#TYPE_ACCELEROMETER_UNCALIBRATED
      * Sensor.TYPE_ACCELEROMETER_UNCALIBRATED}:</h4> All values are in SI
      * units (m/s^2)
diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java
index dc5750d..acb1d07 100644
--- a/core/java/android/net/ConnectivityManager.java
+++ b/core/java/android/net/ConnectivityManager.java
@@ -1453,8 +1453,9 @@
     private void sendExpireMsgForFeature(NetworkCapabilities netCap, int seqNum, int delay) {
         if (delay >= 0) {
             Log.d(TAG, "sending expire msg with seqNum " + seqNum + " and delay " + delay);
-            Message msg = sCallbackHandler.obtainMessage(EXPIRE_LEGACY_REQUEST, seqNum, 0, netCap);
-            sCallbackHandler.sendMessageDelayed(msg, delay);
+            CallbackHandler handler = getHandler();
+            Message msg = handler.obtainMessage(EXPIRE_LEGACY_REQUEST, seqNum, 0, netCap);
+            handler.sendMessageDelayed(msg, delay);
         }
     }
 
@@ -2897,19 +2898,19 @@
         }
     }
 
-    static final HashMap<NetworkRequest, NetworkCallback> sCallbacks = new HashMap<>();
-    static CallbackHandler sCallbackHandler;
+    private static final HashMap<NetworkRequest, NetworkCallback> sCallbacks = new HashMap<>();
+    private static CallbackHandler sCallbackHandler;
 
-    private final static int LISTEN  = 1;
-    private final static int REQUEST = 2;
+    private static final int LISTEN  = 1;
+    private static final int REQUEST = 2;
 
     private NetworkRequest sendRequestForNetwork(NetworkCapabilities need,
             NetworkCallback callback, int timeoutMs, int action, int legacyType) {
-        return sendRequestForNetwork(need, callback, getHandler(), timeoutMs, action, legacyType);
+        return sendRequestForNetwork(need, callback, timeoutMs, action, legacyType, getHandler());
     }
 
-    private NetworkRequest sendRequestForNetwork(NetworkCapabilities need,
-            NetworkCallback callback, Handler handler, int timeoutMs, int action, int legacyType) {
+    private NetworkRequest sendRequestForNetwork(NetworkCapabilities need, NetworkCallback callback,
+            int timeoutMs, int action, int legacyType, CallbackHandler handler) {
         if (callback == null) {
             throw new IllegalArgumentException("null NetworkCallback");
         }
diff --git a/core/java/android/net/NetworkKey.java b/core/java/android/net/NetworkKey.java
index e5f0bf0..31a74dc 100644
--- a/core/java/android/net/NetworkKey.java
+++ b/core/java/android/net/NetworkKey.java
@@ -24,6 +24,7 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.text.TextUtils;
+import android.util.Log;
 
 import java.util.Objects;
 
@@ -41,6 +42,8 @@
 // etc.) so that clients can pull out these details depending on the type of network.
 public class NetworkKey implements Parcelable {
 
+    private static final String TAG = "NetworkKey";
+
     /** A wifi network, for which {@link #wifiKey} will be populated. */
     public static final int TYPE_WIFI = 1;
 
@@ -59,13 +62,28 @@
     /**
      * Constructs a new NetworkKey for the given wifi {@link ScanResult}.
      *
-     * @throws IllegalArgumentException if the given ScanResult is malformed
+     * @return  A new {@link NetworkKey} instance or <code>null</code> if the given
+     *          {@link ScanResult} instance is malformed.
      * @hide
      */
-    public static NetworkKey createFromScanResult(ScanResult result) {
-        return new NetworkKey(
-                new WifiKey(
-                        '"' + result.wifiSsid.toString() + '"', result.BSSID));
+    @Nullable
+    public static NetworkKey createFromScanResult(@Nullable ScanResult result) {
+        if (result != null && result.wifiSsid != null) {
+            final String ssid = result.wifiSsid.toString();
+            final String bssid = result.BSSID;
+            if (!TextUtils.isEmpty(ssid) && !ssid.equals(WifiSsid.NONE)
+                    && !TextUtils.isEmpty(bssid)) {
+                WifiKey wifiKey;
+                try {
+                    wifiKey = new WifiKey(String.format("\"%s\"", ssid), bssid);
+                } catch (IllegalArgumentException e) {
+                    Log.e(TAG, "Unable to create WifiKey.", e);
+                    return null;
+                }
+                return new NetworkKey(wifiKey);
+            }
+        }
+        return null;
     }
 
     /**
@@ -83,7 +101,14 @@
             final String bssid = wifiInfo.getBSSID();
             if (!TextUtils.isEmpty(ssid) && !ssid.equals(WifiSsid.NONE)
                     && !TextUtils.isEmpty(bssid)) {
-                return new NetworkKey(new WifiKey(ssid, bssid));
+                WifiKey wifiKey;
+                try {
+                    wifiKey = new WifiKey(ssid, bssid);
+                } catch (IllegalArgumentException e) {
+                    Log.e(TAG, "Unable to create WifiKey.", e);
+                    return null;
+                }
+                return new NetworkKey(wifiKey);
             }
         }
         return null;
diff --git a/core/java/android/os/ZygoteProcess.java b/core/java/android/os/ZygoteProcess.java
index fa9f394..b3366d8 100644
--- a/core/java/android/os/ZygoteProcess.java
+++ b/core/java/android/os/ZygoteProcess.java
@@ -506,4 +506,24 @@
             state.writer.flush();
         }
     }
+
+    /**
+     * Instructs the zygote to preload the default set of classes and resources. Returns
+     * {@code true} if a preload was performed as a result of this call, and {@code false}
+     * otherwise. The latter usually means that the zygote eagerly preloaded at startup
+     * or due to a previous call to {@code preloadDefault}. Note that this call is synchronous.
+     */
+    public boolean preloadDefault(String abi) throws ZygoteStartFailedEx, IOException {
+        synchronized (mLock) {
+            ZygoteState state = openZygoteSocketIfNeeded(abi);
+            // Each query starts with the argument count (1 in this case)
+            state.writer.write("1");
+            state.writer.newLine();
+            state.writer.write("--preload-default");
+            state.writer.newLine();
+            state.writer.flush();
+
+            return (state.inputStream.readInt() == 0);
+        }
+    }
 }
diff --git a/core/java/android/provider/SettingsStringUtil.java b/core/java/android/provider/SettingsStringUtil.java
index f242d79..3dfedea 100644
--- a/core/java/android/provider/SettingsStringUtil.java
+++ b/core/java/android/provider/SettingsStringUtil.java
@@ -60,7 +60,7 @@
             StringBuilder sb = new StringBuilder();
             Iterator<T> it = iterator();
             if (it.hasNext()) {
-                sb.append(it.next());
+                sb.append(itemToString(it.next()));
                 while (it.hasNext()) {
                     sb.append(DELIMITER);
                     sb.append(itemToString(it.next()));
diff --git a/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java b/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java
index cb5f220..491eabc 100644
--- a/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java
+++ b/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java
@@ -97,6 +97,8 @@
         }
 
         mAudioTrack.setPlaybackPositionUpdateListener(this);
+        // Ensure we set the first marker if there is one.
+        updateMarker();
 
         try {
             byte[] buffer = null;
diff --git a/core/java/android/text/method/LinkMovementMethod.java b/core/java/android/text/method/LinkMovementMethod.java
index 24c119f..31ed549 100644
--- a/core/java/android/text/method/LinkMovementMethod.java
+++ b/core/java/android/text/method/LinkMovementMethod.java
@@ -29,9 +29,6 @@
 /**
  * A movement method that traverses links in the text buffer and scrolls if necessary.
  * Supports clicking on links with DPad Center or Enter.
- *
- * <p>Note: Starting from Android 8.0 (API level 25) this class no longer handles the touch
- * clicks.
  */
 public class LinkMovementMethod extends ScrollingMovementMethod {
     private static final int CLICK = 1;
@@ -198,7 +195,7 @@
                                 MotionEvent event) {
         int action = event.getAction();
 
-        if (action == MotionEvent.ACTION_DOWN) {
+        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
             int x = (int) event.getX();
             int y = (int) event.getY();
 
@@ -215,9 +212,13 @@
             ClickableSpan[] links = buffer.getSpans(off, off, ClickableSpan.class);
 
             if (links.length != 0) {
-                Selection.setSelection(buffer,
+                if (action == MotionEvent.ACTION_UP) {
+                    links[0].onClick(widget);
+                } else if (action == MotionEvent.ACTION_DOWN) {
+                    Selection.setSelection(buffer,
                         buffer.getSpanStart(links[0]),
                         buffer.getSpanEnd(links[0]));
+                }
                 return true;
             } else {
                 Selection.removeSelection(buffer);
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 17cd446..5572cbb 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -121,7 +121,6 @@
 import android.view.Choreographer;
 import android.view.ContextMenu;
 import android.view.DragEvent;
-import android.view.GestureDetector;
 import android.view.Gravity;
 import android.view.HapticFeedbackConstants;
 import android.view.KeyCharacterMap;
@@ -682,8 +681,6 @@
      */
     private Editor mEditor;
 
-    private GestureDetector mClickableSpanOnClickGestureDetector;
-
     private static final int DEVICE_PROVISIONED_UNKNOWN = 0;
     private static final int DEVICE_PROVISIONED_NO = 1;
     private static final int DEVICE_PROVISIONED_YES = 2;
@@ -9319,24 +9316,21 @@
                 handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);
             }
 
-            // Lazily create the clickable span gesture detector only if it looks like it
-            // might be useful.
-            if (action == MotionEvent.ACTION_DOWN && mClickableSpanOnClickGestureDetector == null
-                    && shouldUseClickableSpanOnClickGestureDetector()) {
-                ClickableSpan[] links = ((Spannable) mText).getSpans(
-                        getSelectionStart(), getSelectionEnd(),
-                        ClickableSpan.class);
+            final boolean textIsSelectable = isTextSelectable();
+            if (touchIsFinished && mLinksClickable && mAutoLinkMask != 0 && textIsSelectable) {
+                // The LinkMovementMethod which should handle taps on links has not been installed
+                // on non editable text that support text selection.
+                // We reproduce its behavior here to open links for these.
+                ClickableSpan[] links = ((Spannable) mText).getSpans(getSelectionStart(),
+                    getSelectionEnd(), ClickableSpan.class);
+
                 if (links.length > 0) {
-                    mClickableSpanOnClickGestureDetector =
-                            createClickableSpanOnClickGestureDetector();
+                    links[0].onClick(this);
+                    handled = true;
                 }
             }
 
-            if (mClickableSpanOnClickGestureDetector != null) {
-                handled |= mClickableSpanOnClickGestureDetector.onTouchEvent(event);
-            }
-
-            if (touchIsFinished && (isTextEditable() || isTextSelectable())) {
+            if (touchIsFinished && (isTextEditable() || textIsSelectable)) {
                 // Show the IME, except when selecting in read-only text.
                 final InputMethodManager imm = InputMethodManager.peekInstance();
                 viewClicked(imm);
@@ -9754,31 +9748,6 @@
         mEditor.onLocaleChanged();
     }
 
-    private GestureDetector createClickableSpanOnClickGestureDetector() {
-        return new GestureDetector(mContext,
-                new GestureDetector.SimpleOnGestureListener() {
-                    @Override
-                    public boolean onSingleTapUp(MotionEvent e) {
-                        if (shouldUseClickableSpanOnClickGestureDetector()) {
-                            ClickableSpan[] links = ((Spannable) mText).getSpans(
-                                    getSelectionStart(), getSelectionEnd(),
-                                    ClickableSpan.class);
-                            if (links.length > 0) {
-                                links[0].onClick(TextView.this);
-                                return true;
-                            }
-                        }
-                        return false;
-                    }
-                });
-    }
-
-    private boolean shouldUseClickableSpanOnClickGestureDetector() {
-        return mLinksClickable && (mMovement != null) &&
-                (mMovement instanceof LinkMovementMethod
-                        || (mAutoLinkMask != 0 && isTextSelectable()));
-    }
-
     /**
      * This method is used by the ArrowKeyMovementMethod to jump from one word to the other.
      * Made available to achieve a consistent behavior.
diff --git a/core/java/com/android/internal/os/WebViewZygoteInit.java b/core/java/com/android/internal/os/WebViewZygoteInit.java
index d82a211..f27c0d4 100644
--- a/core/java/com/android/internal/os/WebViewZygoteInit.java
+++ b/core/java/com/android/internal/os/WebViewZygoteInit.java
@@ -55,8 +55,15 @@
         }
 
         @Override
-        protected void maybePreload() {
-            // Do nothing, we don't need to call ZygoteInit.maybePreload() for the WebView zygote.
+        protected void preload() {
+            // Nothing to preload by default.
+        }
+
+        @Override
+        protected boolean isPreloadComplete() {
+            // Webview zygotes don't preload any classes or resources or defaults, all of their
+            // preloading is package specific.
+            return true;
         }
 
         @Override
diff --git a/core/java/com/android/internal/os/Zygote.java b/core/java/com/android/internal/os/Zygote.java
index 59416dd..fa71a62 100644
--- a/core/java/com/android/internal/os/Zygote.java
+++ b/core/java/com/android/internal/os/Zygote.java
@@ -173,6 +173,10 @@
         VM_HOOKS.postForkChild(debugFlags, isSystemServer, instructionSet);
     }
 
+    /**
+     * Resets this process' priority to the default value (0).
+     */
+    native static void nativeResetNicePriority();
 
     /**
      * Executes "/system/bin/sh -c &lt;command&gt;" using the exec() system call.
diff --git a/core/java/com/android/internal/os/ZygoteConnection.java b/core/java/com/android/internal/os/ZygoteConnection.java
index a7f311b..e2485e9 100644
--- a/core/java/com/android/internal/os/ZygoteConnection.java
+++ b/core/java/com/android/internal/os/ZygoteConnection.java
@@ -171,7 +171,9 @@
                 return handleAbiListQuery();
             }
 
-            maybePreload();
+            if (parsedArgs.preloadDefault) {
+                return handlePreload();
+            }
 
             if (parsedArgs.preloadPackage != null) {
                 return handlePreloadPackage(parsedArgs.preloadPackage,
@@ -282,8 +284,34 @@
         }
     }
 
-    protected void maybePreload() {
-        ZygoteInit.maybePreload();
+    /**
+     * Preloads resources if the zygote is in lazily preload mode. Writes the result of the
+     * preload operation; {@code 0} when a preload was initiated due to this request and {@code 1}
+     * if no preload was initiated. The latter implies that the zygote is not configured to load
+     * resources lazy or that the zygote has already handled a previous request to handlePreload.
+     */
+    private boolean handlePreload() {
+        try {
+            if (isPreloadComplete()) {
+                mSocketOutStream.writeInt(1);
+            } else {
+                preload();
+                mSocketOutStream.writeInt(0);
+            }
+
+            return false;
+        } catch (IOException ioe) {
+            Log.e(TAG, "Error writing to command socket", ioe);
+            return true;
+        }
+    }
+
+    protected void preload() {
+        ZygoteInit.lazyPreload();
+    }
+
+    protected boolean isPreloadComplete() {
+        return ZygoteInit.isPreloadComplete();
     }
 
     protected boolean handlePreloadPackage(String packagePath, String libsPath) {
@@ -402,6 +430,13 @@
         String preloadPackageLibs;
 
         /**
+         * Whether this is a request to start preloading the default resources and classes.
+         * This argument only makes sense when the zygote is in lazy preload mode (i.e, when
+         * it's started with --enable-lazy-preload).
+         */
+        boolean preloadDefault;
+
+        /**
          * Constructs instance and parses args
          * @param args zygote command-line args
          * @throws IllegalArgumentException
@@ -564,6 +599,8 @@
                 } else if (arg.equals("--preload-package")) {
                     preloadPackage = args[++curArg];
                     preloadPackageLibs = args[++curArg];
+                } else if (arg.equals("--preload-default")) {
+                    preloadDefault = true;
                 } else {
                     break;
                 }
@@ -578,7 +615,7 @@
                     throw new IllegalArgumentException(
                             "Unexpected arguments after --preload-package.");
                 }
-            } else {
+            } else if (!preloadDefault) {
                 if (!seenRuntimeArgs) {
                     throw new IllegalArgumentException("Unexpected argument : " + args[curArg]);
                 }
diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java
index a72b66a..0b5a1b7 100644
--- a/core/java/com/android/internal/os/ZygoteInit.java
+++ b/core/java/com/android/internal/os/ZygoteInit.java
@@ -51,6 +51,7 @@
 
 import com.android.internal.logging.MetricsLogger;
 
+import com.android.internal.util.Preconditions;
 import dalvik.system.DexFile;
 import dalvik.system.PathClassLoader;
 import dalvik.system.VMRuntime;
@@ -146,11 +147,11 @@
         sPreloadComplete = true;
     }
 
-    public static void maybePreload() {
-        if (!sPreloadComplete) {
-            Log.i(TAG, "Lazily preloading resources.");
-            preload(new BootTimingsTraceLog("ZygoteInitTiming_lazy", Trace.TRACE_TAG_DALVIK));
-        }
+    public static void lazyPreload() {
+        Preconditions.checkState(!sPreloadComplete);
+        Log.i(TAG, "Lazily preloading resources.");
+
+        preload(new BootTimingsTraceLog("ZygoteInitTiming_lazy", Trace.TRACE_TAG_DALVIK));
     }
 
     private static void beginIcuCachePinning() {
@@ -712,6 +713,8 @@
                 EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_END,
                     SystemClock.uptimeMillis());
                 bootTimingsTraceLog.traceEnd(); // ZygotePreload
+            } else {
+                Zygote.nativeResetNicePriority();
             }
 
             // Finish profiling the zygote initialization.
@@ -783,6 +786,10 @@
         }
     }
 
+    static boolean isPreloadComplete() {
+        return sPreloadComplete;
+    }
+
     /**
      * Class not instantiable.
      */
diff --git a/core/java/com/android/internal/util/NotificationColorUtil.java b/core/java/com/android/internal/util/NotificationColorUtil.java
index b4890b7..44b21b4 100644
--- a/core/java/com/android/internal/util/NotificationColorUtil.java
+++ b/core/java/com/android/internal/util/NotificationColorUtil.java
@@ -456,7 +456,10 @@
         }
     }
 
-    public static int resolveActionBarColor(int backgroundColor) {
+    public static int resolveActionBarColor(Context context, int backgroundColor) {
+        if (backgroundColor == Notification.COLOR_DEFAULT) {
+            return context.getColor(com.android.internal.R.color.notification_action_list);
+        }
         boolean useDark = shouldUseDark(backgroundColor);
         final double[] result = ColorUtilsFromCompat.getTempDouble3Array();
         ColorUtilsFromCompat.colorToLAB(backgroundColor, result);
diff --git a/core/java/com/android/internal/widget/ILockSettings.aidl b/core/java/com/android/internal/widget/ILockSettings.aidl
index b380b13..b8c062e 100644
--- a/core/java/com/android/internal/widget/ILockSettings.aidl
+++ b/core/java/com/android/internal/widget/ILockSettings.aidl
@@ -45,4 +45,10 @@
     void systemReady();
     void userPresent(int userId);
     int getStrongAuthForUser(int userId);
+
+    long addEscrowToken(in byte[] token, int userId);
+    boolean removeEscrowToken(long handle, int userId);
+    boolean isEscrowTokenActive(long handle, int userId);
+    boolean setLockCredentialWithToken(String credential, int type, long tokenHandle, in byte[] token, int userId);
+    void unlockUserWithToken(long tokenHandle, in byte[] token, int userId);
 }
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index ef6e6c2..0aba9c2 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -147,6 +147,10 @@
 
     public static final String PROFILE_KEY_NAME_ENCRYPT = "profile_key_name_encrypt_";
     public static final String PROFILE_KEY_NAME_DECRYPT = "profile_key_name_decrypt_";
+    public static final String SYNTHETIC_PASSWORD_KEY_PREFIX = "synthetic_password_";
+
+    public static final String SYNTHETIC_PASSWORD_HANDLE_KEY = "sp-handle";
+    public static final String SYNTHETIC_PASSWORD_ENABLED_KEY = "enable-sp";
 
     private final Context mContext;
     private final ContentResolver mContentResolver;
@@ -769,7 +773,7 @@
             getLockSettings().setLockCredential(password, CREDENTIAL_TYPE_PASSWORD, savedPassword,
                     userHandle);
 
-            addEncryptionPassword(password, computedQuality, userHandle);
+            updateEncryptionPasswordIfNeeded(password, computedQuality, userHandle);
             updatePasswordHistory(password, userHandle);
         } catch (RemoteException re) {
             // Cant do much
@@ -777,7 +781,11 @@
         }
     }
 
-    private void addEncryptionPassword(String password, int quality, int userHandle) {
+    /**
+     * Update device encryption password if calling user is USER_SYSTEM and device supports
+     * encryption.
+     */
+    private void updateEncryptionPasswordIfNeeded(String password, int quality, int userHandle) {
         // Update the device encryption password.
         if (userHandle == UserHandle.USER_SYSTEM
                 && LockPatternUtils.isDeviceEncryptionEnabled()) {
@@ -1398,6 +1406,104 @@
     }
 
     /**
+     * Create an escrow token for the current user, which can later be used to unlock FBE
+     * or change user password.
+     *
+     * After adding, if the user currently has lockscreen password, he will need to perform a
+     * confirm credential operation in order to activate the token for future use. If the user
+     * has no secure lockscreen, then the token is activated immediately.
+     *
+     * @return a unique 64-bit token handle which is needed to refer to this token later.
+     */
+    public long addEscrowToken(byte[] token, int userId) {
+        try {
+            return getLockSettings().addEscrowToken(token, userId);
+        } catch (RemoteException re) {
+            return 0L;
+        }
+    }
+
+    /**
+     * Remove an escrow token.
+     * @return true if the given handle refers to a valid token previously returned from
+     * {@link #addEscrowToken}, whether it's active or not. return false otherwise.
+     */
+    public boolean removeEscrowToken(long handle, int userId) {
+        try {
+            return getLockSettings().removeEscrowToken(handle, userId);
+        } catch (RemoteException re) {
+            return false;
+        }
+    }
+
+    /**
+     * Check if the given escrow token is active or not. Only active token can be used to call
+     * {@link #setLockCredentialWithToken} and {@link #unlockUserWithToken}
+     */
+    public boolean isEscrowTokenActive(long handle, int userId) {
+        try {
+            return getLockSettings().isEscrowTokenActive(handle, userId);
+        } catch (RemoteException re) {
+            return false;
+        }
+    }
+
+    public boolean setLockCredentialWithToken(String credential, int type, long tokenHandle,
+            byte[] token, int userId) {
+        try {
+            if (type != CREDENTIAL_TYPE_NONE) {
+                if (TextUtils.isEmpty(credential) || credential.length() < MIN_LOCK_PASSWORD_SIZE) {
+                    throw new IllegalArgumentException("password must not be null and at least "
+                            + "of length " + MIN_LOCK_PASSWORD_SIZE);
+                }
+
+                final int computedQuality = PasswordMetrics.computeForPassword(credential).quality;
+                if (!getLockSettings().setLockCredentialWithToken(credential, type, tokenHandle,
+                        token, userId)) {
+                    return false;
+                }
+                setLong(PASSWORD_TYPE_KEY, Math.max(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC,
+                        computedQuality), userId);
+
+                updateEncryptionPasswordIfNeeded(credential, computedQuality, userId);
+                updatePasswordHistory(credential, userId);
+            } else {
+                if (!TextUtils.isEmpty(credential)) {
+                    throw new IllegalArgumentException("password must be emtpy for NONE type");
+                }
+                if (!getLockSettings().setLockCredentialWithToken(null, CREDENTIAL_TYPE_NONE,
+                        tokenHandle, token, userId)) {
+                    return false;
+                }
+                setLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED,
+                        userId);
+
+                if (userId == UserHandle.USER_SYSTEM) {
+                    // Set the encryption password to default.
+                    updateEncryptionPassword(StorageManager.CRYPT_TYPE_DEFAULT, null);
+                    setCredentialRequiredToDecrypt(false);
+                }
+            }
+            onAfterChangingPassword(userId);
+            return true;
+        } catch (RemoteException re) {
+            Log.e(TAG, "Unable to save lock password ", re);
+            re.rethrowFromSystemServer();
+        }
+        return false;
+    }
+
+    public void unlockUserWithToken(long tokenHandle, byte[] token, int userId) {
+        try {
+            getLockSettings().unlockUserWithToken(tokenHandle, token, userId);
+        } catch (RemoteException re) {
+            Log.e(TAG, "Unable to unlock user with token", re);
+            re.rethrowFromSystemServer();
+        }
+    }
+
+
+    /**
      * Callback to be notified about progress when checking credentials.
      */
     public interface CheckCredentialProgressCallback {
@@ -1559,6 +1665,14 @@
                         break;
                 }
             }
-        };
+        }
+    }
+
+    public void enableSyntheticPassword() {
+        setLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 1L, UserHandle.USER_SYSTEM);
+    }
+
+    public boolean isSyntheticPasswordEnabled() {
+        return getLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 0, UserHandle.USER_SYSTEM) != 0;
     }
 }
diff --git a/core/java/com/android/internal/widget/SwipeDismissLayout.java b/core/java/com/android/internal/widget/SwipeDismissLayout.java
index 261fa43..6d814bf 100644
--- a/core/java/com/android/internal/widget/SwipeDismissLayout.java
+++ b/core/java/com/android/internal/widget/SwipeDismissLayout.java
@@ -79,7 +79,6 @@
     private boolean mDismissed;
     private boolean mDiscardIntercept;
     private VelocityTracker mVelocityTracker;
-    private float mTranslationX;
     private boolean mBlockGesture = false;
     private boolean mActivityTranslucencyConverted = false;
 
@@ -166,8 +165,10 @@
             return super.onInterceptTouchEvent(ev);
         }
 
-        // offset because the view is translated during swipe
-        ev.offsetLocation(mTranslationX, 0);
+        // Offset because the view is translated during swipe, match X with raw X. Active touch
+        // coordinates are mostly used by the velocity tracker, so offset it to match the raw
+        // coordinates which is what is primarily used elsewhere.
+        ev.offsetLocation(ev.getRawX() - ev.getX(), 0);
 
         switch (ev.getActionMasked()) {
             case MotionEvent.ACTION_DOWN:
@@ -232,8 +233,12 @@
         if (mVelocityTracker == null || !mDismissable) {
             return super.onTouchEvent(ev);
         }
-        // offset because the view is translated during swipe
-        ev.offsetLocation(mTranslationX, 0);
+
+        // Offset because the view is translated during swipe, match X with raw X. Active touch
+        // coordinates are mostly used by the velocity tracker, so offset it to match the raw
+        // coordinates which is what is primarily used elsewhere.
+        ev.offsetLocation(ev.getRawX() - ev.getX(), 0);
+
         switch (ev.getActionMasked()) {
             case MotionEvent.ACTION_UP:
                 updateDismiss(ev);
@@ -266,7 +271,6 @@
     }
 
     private void setProgress(float deltaX) {
-        mTranslationX = deltaX;
         if (mProgressListener != null && deltaX >= 0)  {
             mProgressListener.onSwipeProgressChanged(
                     this, progressToAlpha(deltaX / getWidth()), deltaX);
@@ -300,7 +304,6 @@
             mVelocityTracker.recycle();
         }
         mVelocityTracker = null;
-        mTranslationX = 0;
         mDownX = 0;
         mLastX = Integer.MIN_VALUE;
         mDownY = 0;
diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp
index e2fc444..c3f0e9d 100644
--- a/core/jni/com_android_internal_os_Zygote.cpp
+++ b/core/jni/com_android_internal_os_Zygote.cpp
@@ -806,6 +806,10 @@
     UnmountTree("/storage");
 }
 
+static void com_android_internal_os_Zygote_nativeResetNicePriority(JNIEnv* env, jclass) {
+    ResetNicePriority(env);
+}
+
 static const JNINativeMethod gMethods[] = {
     { "nativeForkAndSpecialize",
       "(II[II[[IILjava/lang/String;Ljava/lang/String;[I[ILjava/lang/String;Ljava/lang/String;)I",
@@ -815,7 +819,9 @@
     { "nativeAllowFileAcrossFork", "(Ljava/lang/String;)V",
       (void *) com_android_internal_os_Zygote_nativeAllowFileAcrossFork },
     { "nativeUnmountStorageOnInit", "()V",
-      (void *) com_android_internal_os_Zygote_nativeUnmountStorageOnInit }
+      (void *) com_android_internal_os_Zygote_nativeUnmountStorageOnInit },
+    { "nativeResetNicePriority", "()V",
+      (void *) com_android_internal_os_Zygote_nativeResetNicePriority }
 };
 
 int register_com_android_internal_os_Zygote(JNIEnv* env) {
diff --git a/core/proto/android/service/notification.proto b/core/proto/android/service/notification.proto
index bc257e0..819460e 100644
--- a/core/proto/android/service/notification.proto
+++ b/core/proto/android/service/notification.proto
@@ -23,6 +23,8 @@
 
 message NotificationServiceDumpProto {
     repeated NotificationRecordProto records = 1;
+
+    ZenModeProto zen = 2;
 }
 
 message NotificationRecordProto {
@@ -42,4 +44,21 @@
     ENQUEUED = 0;
 
     POSTED = 1;
+
+    SNOOZED = 2;
+}
+
+message ZenModeProto {
+    ZenMode zen_mode = 1;
+    repeated string enabled_active_conditions = 2;
+    int32 suppressed_effects = 3;
+    repeated string suppressors = 4;
+    string policy = 5;
+}
+
+enum ZenMode {
+    ZEN_MODE_OFF = 0;
+    ZEN_MODE_IMPORTANT_INTERRUPTIONS = 1;
+    ZEN_MODE_NO_INTERRUPTIONS = 2;
+    ZEN_MODE_ALARMS = 3;
 }
\ No newline at end of file
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 36bb821..4d5e45b 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -5846,8 +5846,6 @@
 
     <!-- Drawable used to draw masked icons with foreground and background layers. -->
     <declare-styleable name="MaskableIconDrawableLayer">
-        <!-- The color to use for the layer, only if drawable is not defined. -->
-        <attr name="color" />
         <!-- The drawable to use for the layer. -->
         <attr name="drawable" />
      </declare-styleable>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 54c392f..72a2e43 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2541,7 +2541,7 @@
 
     <!-- Default insets [LEFT/RIGHTxTOP/BOTTOM] from the screen edge for picture-in-picture windows.
          These values are in DPs and will be converted to pixel sizes internally. -->
-    <string translatable="false" name="config_defaultPictureInPictureScreenEdgeInsets">8x8</string>
+    <string translatable="false" name="config_defaultPictureInPictureScreenEdgeInsets">16x16</string>
 
     <!-- Max default size [WIDTHxHEIGHT] on screen for picture-in-picture windows to fit inside.
          These values are in DPs and will be converted to pixel sizes internally. -->
diff --git a/core/tests/coretests/src/android/net/NetworkKeyTest.java b/core/tests/coretests/src/android/net/NetworkKeyTest.java
index 1afe9da..fff23a0 100644
--- a/core/tests/coretests/src/android/net/NetworkKeyTest.java
+++ b/core/tests/coretests/src/android/net/NetworkKeyTest.java
@@ -4,6 +4,7 @@
 import static org.junit.Assert.assertNull;
 import static org.mockito.Mockito.when;
 
+import android.net.wifi.ScanResult;
 import android.net.wifi.WifiInfo;
 import android.net.wifi.WifiSsid;
 import android.support.test.runner.AndroidJUnit4;
@@ -17,7 +18,9 @@
 @RunWith(AndroidJUnit4.class)
 public class NetworkKeyTest {
     private static final String VALID_SSID = "\"ssid1\"";
+    private static final String VALID_UNQUOTED_SSID = "ssid1";
     private static final String VALID_BSSID = "00:00:00:00:00:00";
+    private static final String INVALID_BSSID = "invalid_bssid";
     @Mock private WifiInfo mWifiInfo;
 
     @Before
@@ -64,6 +67,13 @@
     }
 
     @Test
+    public void createFromWifi_invalidBssid() throws Exception {
+        when(mWifiInfo.getSSID()).thenReturn(VALID_SSID);
+        when(mWifiInfo.getBSSID()).thenReturn(INVALID_BSSID);
+        assertNull(NetworkKey.createFromWifiInfo(mWifiInfo));
+    }
+
+    @Test
     public void createFromWifi_validWifiInfo() throws Exception {
         when(mWifiInfo.getSSID()).thenReturn(VALID_SSID);
         when(mWifiInfo.getBSSID()).thenReturn(VALID_BSSID);
@@ -72,4 +82,72 @@
         final NetworkKey actual = NetworkKey.createFromWifiInfo(mWifiInfo);
         assertEquals(expected, actual);
     }
+
+    @Test
+    public void createFromScanResult_nullInput() {
+        assertNull(NetworkKey.createFromScanResult(null));
+    }
+
+    @Test
+    public void createFromScanResult_nullWifiSsid() {
+        ScanResult scanResult = new ScanResult();
+        scanResult.BSSID = VALID_BSSID;
+
+        assertNull(NetworkKey.createFromScanResult(scanResult));
+    }
+
+    @Test
+    public void createFromScanResult_emptyWifiSsid() {
+        ScanResult scanResult = new ScanResult();
+        scanResult.wifiSsid = WifiSsid.createFromAsciiEncoded("");
+        scanResult.BSSID = VALID_BSSID;
+
+        assertNull(NetworkKey.createFromScanResult(scanResult));
+    }
+
+    @Test
+    public void createFromScanResult_noneWifiSsid() {
+        ScanResult scanResult = new ScanResult();
+        scanResult.wifiSsid = WifiSsid.createFromAsciiEncoded(WifiSsid.NONE);
+        scanResult.BSSID = VALID_BSSID;
+
+        assertNull(NetworkKey.createFromScanResult(scanResult));
+    }
+
+    @Test
+    public void createFromScanResult_nullBssid() {
+        ScanResult scanResult = new ScanResult();
+        scanResult.wifiSsid = WifiSsid.createFromAsciiEncoded(VALID_UNQUOTED_SSID);
+
+        assertNull(NetworkKey.createFromScanResult(scanResult));
+    }
+
+    @Test
+    public void createFromScanResult_emptyBssid() {
+        ScanResult scanResult = new ScanResult();
+        scanResult.wifiSsid = WifiSsid.createFromAsciiEncoded(VALID_UNQUOTED_SSID);
+        scanResult.BSSID = "";
+
+        assertNull(NetworkKey.createFromScanResult(scanResult));
+    }
+
+    @Test
+    public void createFromScanResult_invalidBssid() {
+        ScanResult scanResult = new ScanResult();
+        scanResult.wifiSsid = WifiSsid.createFromAsciiEncoded(VALID_UNQUOTED_SSID);
+        scanResult.BSSID = INVALID_BSSID;
+
+        assertNull(NetworkKey.createFromScanResult(scanResult));
+    }
+
+    @Test
+    public void createFromScanResult_validWifiSsid() {
+        ScanResult scanResult = new ScanResult();
+        scanResult.wifiSsid = WifiSsid.createFromAsciiEncoded(VALID_UNQUOTED_SSID);
+        scanResult.BSSID = VALID_BSSID;
+
+        NetworkKey expected = new NetworkKey(new WifiKey(VALID_SSID, VALID_BSSID));
+        NetworkKey actual = NetworkKey.createFromScanResult(scanResult);
+        assertEquals(expected, actual);
+    }
 }
diff --git a/core/tests/packagemanagertests/Android.mk b/core/tests/packagemanagertests/Android.mk
new file mode 100644
index 0000000..c1e8c98
--- /dev/null
+++ b/core/tests/packagemanagertests/Android.mk
@@ -0,0 +1,20 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+# We only want this apk build for tests.
+LOCAL_MODULE_TAGS := tests
+
+# Include all test java files.
+LOCAL_SRC_FILES := \
+    $(call all-java-files-under, src)
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    android-support-test \
+    frameworks-base-testutils
+
+LOCAL_JAVA_LIBRARIES := android.test.runner
+LOCAL_PACKAGE_NAME := FrameworksCorePackageManagerTests
+
+LOCAL_CERTIFICATE := platform
+
+include $(BUILD_PACKAGE)
diff --git a/core/tests/packagemanagertests/AndroidManifest.xml b/core/tests/packagemanagertests/AndroidManifest.xml
new file mode 100644
index 0000000..8f49008
--- /dev/null
+++ b/core/tests/packagemanagertests/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          android:installLocation="internalOnly"
+          package="com.android.frameworks.coretests.packagemanager"
+          android:sharedUserId="android.uid.system">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+            android:name="android.support.test.runner.AndroidJUnitRunner"
+            android:targetPackage="com.android.frameworks.coretests.packagemanager"
+            android:label="Frameworks PackageManager Core Tests" />
+
+</manifest>
diff --git a/core/tests/packagemanagertests/src/android/content/pm/KernelPackageMappingTests.java b/core/tests/packagemanagertests/src/android/content/pm/KernelPackageMappingTests.java
new file mode 100644
index 0000000..1097bc7
--- /dev/null
+++ b/core/tests/packagemanagertests/src/android/content/pm/KernelPackageMappingTests.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.os.FileUtils;
+import android.os.Process;
+import android.os.ServiceManager;
+import android.os.UserManager;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * This test needs to be run without any secondary users on the device,
+ * and selinux needs to be disabled with "adb shell setenforce 0".
+ */
+@RunWith(AndroidJUnit4.class)
+public class KernelPackageMappingTests {
+
+    private static final String TAG = "KernelPackageMapping";
+    private static final String SDCARDFS_PATH = "/config/sdcardfs";
+
+    private UserInfo mSecondaryUser;
+
+    private static File getKernelPackageDir(String packageName) {
+        return new File(new File(SDCARDFS_PATH), packageName);
+    }
+
+    private static File getKernelPackageFile(String packageName, String filename) {
+        return new File(getKernelPackageDir(packageName), filename);
+    }
+
+    private UserManager getUserManager() {
+        UserManager um = (UserManager) InstrumentationRegistry.getContext().getSystemService(
+                Context.USER_SERVICE);
+        return um;
+    }
+
+    private IPackageManager getIPackageManager() {
+        IPackageManager ipm = IPackageManager.Stub.asInterface(
+                ServiceManager.getService("package"));
+        return ipm;
+    }
+
+    private static String getContent(File file) {
+        try {
+            return FileUtils.readTextFile(file, 0, null).trim();
+        } catch (IOException ioe) {
+            Log.w(TAG, "Couldn't read file " + file.getAbsolutePath() + "\n" + ioe);
+            return "<error>";
+        }
+    }
+
+    @Test
+    public void testInstalledPrimary() throws Exception {
+        assertEquals("1000", getContent(getKernelPackageFile("com.android.settings", "appid")));
+    }
+
+    @Test
+    public void testInstalledAll() throws Exception {
+        assertEquals("", getContent(getKernelPackageFile("com.android.settings",
+                "excluded_userids")));
+    }
+
+    @Test
+    public void testNotInstalledSecondary() throws Exception {
+        mSecondaryUser = getUserManager().createUser("Secondary", 0);
+        assertEquals(Integer.toString(mSecondaryUser.id),
+                getContent(
+                        getKernelPackageFile("com.android.frameworks.coretests.packagemanager",
+                                "excluded_userids")));
+    }
+
+    @After
+    public void shutDown() throws Exception {
+        if (mSecondaryUser != null) {
+            getUserManager().removeUser(mSecondaryUser.id);
+        }
+    }
+}
diff --git a/graphics/java/android/graphics/drawable/MaskableIconDrawable.java b/graphics/java/android/graphics/drawable/MaskableIconDrawable.java
index e4f1788a..472b229 100644
--- a/graphics/java/android/graphics/drawable/MaskableIconDrawable.java
+++ b/graphics/java/android/graphics/drawable/MaskableIconDrawable.java
@@ -427,7 +427,7 @@
                 }
                 if (type != XmlPullParser.START_TAG) {
                     throw new XmlPullParserException(parser.getPositionDescription()
-                            + ": <foreground> or <background> tag requires a 'color' or 'drawable'"
+                            + ": <foreground> or <background> tag requires a 'drawable'"
                             + "attribute or child tag defining a drawable");
                 }
 
@@ -451,12 +451,6 @@
         layer.mThemeAttrs = a.extractThemeAttrs();
 
         Drawable dr = a.getDrawable(R.styleable.MaskableIconDrawableLayer_drawable);
-        if (dr == null) {
-             int color = a.getColor(R.styleable.MaskableIconDrawableLayer_color, Color.TRANSPARENT);
-             if (color != Color.TRANSPARENT) {
-                 dr = new ColorDrawable(color);
-             }
-        }
         if (dr != null) {
             if (layer.mDrawable != null) {
                 // It's possible that a drawable was already set, in which case
diff --git a/libs/hwui/DeferredLayerUpdater.cpp b/libs/hwui/DeferredLayerUpdater.cpp
index 00e8c05..ff90160 100644
--- a/libs/hwui/DeferredLayerUpdater.cpp
+++ b/libs/hwui/DeferredLayerUpdater.cpp
@@ -98,6 +98,8 @@
                 mUpdateTexImage = false;
                 doUpdateTexImage();
             }
+            GLenum renderTarget = mSurfaceTexture->getCurrentTextureTarget();
+            static_cast<GlLayer*>(mLayer)->setRenderTarget(renderTarget);
         }
         if (mTransform) {
             mLayer->getTransform().load(*mTransform);
@@ -140,12 +142,8 @@
         }
         #endif
         mSurfaceTexture->getTransformMatrix(transform);
-        GLenum renderTarget = mSurfaceTexture->getCurrentTextureTarget();
 
-        LOG_ALWAYS_FATAL_IF(renderTarget != GL_TEXTURE_2D && renderTarget != GL_TEXTURE_EXTERNAL_OES,
-                "doUpdateTexImage target %x, 2d %x, EXT %x",
-                renderTarget, GL_TEXTURE_2D, GL_TEXTURE_EXTERNAL_OES);
-        updateLayer(forceFilter, renderTarget, transform);
+        updateLayer(forceFilter, transform);
     }
 }
 
@@ -155,28 +153,17 @@
                         mLayer->getApi(), Layer::Api::OpenGL, Layer::Api::Vulkan);
 
     static const mat4 identityMatrix;
-    updateLayer(false, GL_NONE, identityMatrix.data);
+    updateLayer(false, identityMatrix.data);
 
     VkLayer* vkLayer = static_cast<VkLayer*>(mLayer);
     vkLayer->updateTexture();
 }
 
-void DeferredLayerUpdater::updateLayer(bool forceFilter, GLenum renderTarget,
-        const float* textureTransform) {
+void DeferredLayerUpdater::updateLayer(bool forceFilter, const float* textureTransform) {
     mLayer->setBlend(mBlend);
     mLayer->setForceFilter(forceFilter);
     mLayer->setSize(mWidth, mHeight);
     mLayer->getTexTransform().load(textureTransform);
-
-    if (mLayer->getApi() == Layer::Api::OpenGL) {
-        GlLayer* glLayer = static_cast<GlLayer*>(mLayer);
-        if (renderTarget != glLayer->getRenderTarget()) {
-            glLayer->setRenderTarget(renderTarget);
-            glLayer->bindTexture();
-            glLayer->setFilter(GL_NEAREST, false, true);
-            glLayer->setWrap(GL_CLAMP_TO_EDGE, false, true);
-        }
-    }
 }
 
 void DeferredLayerUpdater::detachSurfaceTexture() {
diff --git a/libs/hwui/DeferredLayerUpdater.h b/libs/hwui/DeferredLayerUpdater.h
index 6717361..6164e47 100644
--- a/libs/hwui/DeferredLayerUpdater.h
+++ b/libs/hwui/DeferredLayerUpdater.h
@@ -101,7 +101,7 @@
 
     void detachSurfaceTexture();
 
-    void updateLayer(bool forceFilter, GLenum renderTarget, const float* textureTransform);
+    void updateLayer(bool forceFilter, const float* textureTransform);
 
     void destroyLayer();
 
diff --git a/libs/hwui/GlLayer.cpp b/libs/hwui/GlLayer.cpp
index aacad54..070e954 100644
--- a/libs/hwui/GlLayer.cpp
+++ b/libs/hwui/GlLayer.cpp
@@ -55,9 +55,15 @@
     texture.deleteTexture();
 }
 
-void GlLayer::bindTexture() const {
-    if (texture.mId) {
-        caches.textureState().bindTexture(texture.target(), texture.mId);
+void GlLayer::setRenderTarget(GLenum renderTarget) {
+    if (renderTarget != getRenderTarget()) {
+        // new render target: bind with new target, and update filter/wrap
+        texture.mTarget = renderTarget;
+        if (texture.mId) {
+            caches.textureState().bindTexture(texture.target(), texture.mId);
+        }
+        texture.setFilter(GL_NEAREST, false, true);
+        texture.setWrap(GL_CLAMP_TO_EDGE, false, true);
     }
 }
 
diff --git a/libs/hwui/GlLayer.h b/libs/hwui/GlLayer.h
index 85ddaff..20aaf4a 100644
--- a/libs/hwui/GlLayer.h
+++ b/libs/hwui/GlLayer.h
@@ -68,23 +68,12 @@
         return texture.target();
     }
 
-    inline void setRenderTarget(GLenum renderTarget) {
-        texture.mTarget = renderTarget;
-    }
-
     inline bool isRenderable() const {
         return texture.target() != GL_NONE;
     }
 
-    void setWrap(GLenum wrap, bool bindTexture = false, bool force = false) {
-        texture.setWrap(wrap, bindTexture, force);
-    }
+    void setRenderTarget(GLenum renderTarget);
 
-    void setFilter(GLenum filter, bool bindTexture = false, bool force = false) {
-        texture.setFilter(filter, bindTexture, force);
-    }
-
-    void bindTexture() const;
     void generateTexture();
 
     /**
diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp
index de80ee3..f2b0eb3 100644
--- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp
+++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp
@@ -128,6 +128,8 @@
         return false;
     }
 
+    // acquire most recent buffer for drawing
+    deferredLayer->updateTexImage();
     deferredLayer->apply();
 
     SkCanvas canvas(*bitmap);
diff --git a/libs/hwui/renderthread/OpenGLPipeline.cpp b/libs/hwui/renderthread/OpenGLPipeline.cpp
index 8a5d9cc..acd6110 100644
--- a/libs/hwui/renderthread/OpenGLPipeline.cpp
+++ b/libs/hwui/renderthread/OpenGLPipeline.cpp
@@ -120,6 +120,8 @@
 
 bool OpenGLPipeline::copyLayerInto(DeferredLayerUpdater* layer, SkBitmap* bitmap) {
     ATRACE_CALL();
+    // acquire most recent buffer for drawing
+    layer->updateTexImage();
     layer->apply();
     return OpenGLReadbackImpl::copyLayerInto(mRenderThread,
             static_cast<GlLayer&>(*layer->backingLayer()), bitmap);
diff --git a/libs/hwui/tests/common/TestUtils.cpp b/libs/hwui/tests/common/TestUtils.cpp
index 3e52c39..64ec58d 100644
--- a/libs/hwui/tests/common/TestUtils.cpp
+++ b/libs/hwui/tests/common/TestUtils.cpp
@@ -74,7 +74,11 @@
     layerUpdater->setTransform(&transform);
 
     // updateLayer so it's ready to draw
-    layerUpdater->updateLayer(true, GL_TEXTURE_EXTERNAL_OES, Matrix4::identity().data);
+    layerUpdater->updateLayer(true, Matrix4::identity().data);
+    if (layerUpdater->backingLayer()->getApi() == Layer::Api::OpenGL) {
+        static_cast<GlLayer*>(layerUpdater->backingLayer())->setRenderTarget(
+                GL_TEXTURE_EXTERNAL_OES);
+    }
     return layerUpdater;
 }
 
diff --git a/libs/hwui/tests/unit/DeferredLayerUpdaterTests.cpp b/libs/hwui/tests/unit/DeferredLayerUpdaterTests.cpp
index 1ef9dba..87d897e 100644
--- a/libs/hwui/tests/unit/DeferredLayerUpdaterTests.cpp
+++ b/libs/hwui/tests/unit/DeferredLayerUpdaterTests.cpp
@@ -44,7 +44,12 @@
     // push the deferred updates to the layer
     Matrix4 scaledMatrix;
     scaledMatrix.loadScale(0.5, 0.5, 0.0);
-    layerUpdater->updateLayer(true, GL_TEXTURE_EXTERNAL_OES, scaledMatrix.data);
+    layerUpdater->updateLayer(true, scaledMatrix.data);
+    if (layerUpdater->backingLayer()->getApi() == Layer::Api::OpenGL) {
+        GlLayer* glLayer = static_cast<GlLayer*>(layerUpdater->backingLayer());
+        glLayer->setRenderTarget(GL_TEXTURE_EXTERNAL_OES);
+    }
+
 
     // the backing layer should now have all the properties applied.
     if (layerUpdater->backingLayer()->getApi() == Layer::Api::OpenGL) {
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index fb3f5b3..a4f2a7e 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -1456,10 +1456,11 @@
     }
 
     /**
-     * Checks whether A2DP audio routing to the Bluetooth headset is on or off.
+     * Checks whether a Bluetooth A2DP audio peripheral is connected or not.
      *
-     * @return true if A2DP audio is being routed to/from Bluetooth headset;
+     * @return true if a Bluetooth A2DP peripheral is connected
      *         false if otherwise
+     * @deprecated Use {@link AudioManager#getDevices(int)} instead to list available audio devices.
      */
     public boolean isBluetoothA2dpOn() {
         if (AudioSystem.getDeviceConnectionState(DEVICE_OUT_BLUETOOTH_A2DP,"")
@@ -1492,7 +1493,7 @@
      *
      * @return true if a wired headset is connected.
      *         false if otherwise
-     * @deprecated Use only to check is a headset is connected or not.
+     * @deprecated Use {@link AudioManager#getDevices(int)} instead to list available audio devices.
      */
     public boolean isWiredHeadsetOn() {
         if (AudioSystem.getDeviceConnectionState(DEVICE_OUT_WIRED_HEADSET,"")
diff --git a/media/java/android/media/MediaHTTPConnection.java b/media/java/android/media/MediaHTTPConnection.java
index d6bf421..228a6de 100644
--- a/media/java/android/media/MediaHTTPConnection.java
+++ b/media/java/android/media/MediaHTTPConnection.java
@@ -61,8 +61,9 @@
     private final static int MAX_REDIRECTS = 20;
 
     public MediaHTTPConnection() {
-        if (CookieHandler.getDefault() == null) {
-            CookieHandler.setDefault(new CookieManager());
+        CookieManager cookieManager = (CookieManager)CookieHandler.getDefault();
+        if (cookieManager == null) {
+            Log.w(TAG, "MediaHTTPConnection: Unexpected. No CookieManager found.");
         }
 
         native_setup();
diff --git a/media/java/android/media/MediaHTTPService.java b/media/java/android/media/MediaHTTPService.java
index 52a68bf..b678630 100644
--- a/media/java/android/media/MediaHTTPService.java
+++ b/media/java/android/media/MediaHTTPService.java
@@ -19,25 +19,78 @@
 import android.os.IBinder;
 import android.util.Log;
 
+import java.net.CookieHandler;
+import java.net.CookieManager;
+import java.net.CookieStore;
+import java.net.HttpCookie;
+import java.util.List;
+
 /** @hide */
 public class MediaHTTPService extends IMediaHTTPService.Stub {
     private static final String TAG = "MediaHTTPService";
+    private List<HttpCookie> mCookies;
+    private Boolean mCookieStoreInitialized = new Boolean(false);
 
-    public MediaHTTPService() {
+    public MediaHTTPService(List<HttpCookie> cookies) {
+        mCookies = cookies;
+        Log.v(TAG, "MediaHTTPService(" + this + "): Cookies: " + cookies);
     }
 
     public IMediaHTTPConnection makeHTTPConnection() {
+
+        synchronized (mCookieStoreInitialized) {
+            // Only need to do it once for all connections
+            if ( !mCookieStoreInitialized )  {
+                CookieManager cookieManager = (CookieManager)CookieHandler.getDefault();
+                if (cookieManager == null) {
+                    cookieManager = new CookieManager();
+                    CookieHandler.setDefault(cookieManager);
+                    Log.v(TAG, "makeHTTPConnection: CookieManager created: " + cookieManager);
+                }
+                else {
+                    Log.v(TAG, "makeHTTPConnection: CookieManager(" + cookieManager + ") exists.");
+                }
+
+                // Applying the bootstrapping cookies
+                if ( mCookies != null ) {
+                    CookieStore store = cookieManager.getCookieStore();
+                    for ( HttpCookie cookie : mCookies ) {
+                        try {
+                            store.add(null, cookie);
+                        } catch ( Exception e ) {
+                            Log.v(TAG, "makeHTTPConnection: CookieStore.add" + e);
+                        }
+                        //for extended debugging when needed
+                        //Log.v(TAG, "MediaHTTPConnection adding Cookie[" + cookie.getName() +
+                        //        "]: " + cookie);
+                    }
+                }   // mCookies
+
+                mCookieStoreInitialized = true;
+
+                Log.v(TAG, "makeHTTPConnection(" + this + "): cookieManager: " + cookieManager +
+                        " Cookies: " + mCookies);
+            }   // mCookieStoreInitialized
+        }   // synchronized
+
         return new MediaHTTPConnection();
     }
 
     /* package private */static IBinder createHttpServiceBinderIfNecessary(
             String path) {
+        return createHttpServiceBinderIfNecessary(path, null);
+    }
+
+    // when cookies are provided
+    static IBinder createHttpServiceBinderIfNecessary(
+            String path, List<HttpCookie> cookies) {
         if (path.startsWith("http://") || path.startsWith("https://")) {
-            return (new MediaHTTPService()).asBinder();
+            return (new MediaHTTPService(cookies)).asBinder();
         } else if (path.startsWith("widevine://")) {
             Log.d(TAG, "Widevine classic is no longer supported");
         }
 
         return null;
     }
+
 }
diff --git a/media/java/android/media/MediaPlayer.java b/media/java/android/media/MediaPlayer.java
index c5a47ec..85c3c1c 100644
--- a/media/java/android/media/MediaPlayer.java
+++ b/media/java/android/media/MediaPlayer.java
@@ -73,6 +73,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.ref.WeakReference;
+import java.net.HttpCookie;
 import java.net.HttpURLConnection;
 import java.net.InetSocketAddress;
 import java.net.URL;
@@ -80,6 +81,7 @@
 import java.util.Arrays;
 import java.util.BitSet;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Scanner;
 import java.util.Set;
@@ -998,7 +1000,7 @@
      */
     public void setDataSource(@NonNull Context context, @NonNull Uri uri)
             throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {
-        setDataSource(context, uri, null);
+        setDataSource(context, uri, null, null);
     }
 
     /**
@@ -1011,11 +1013,13 @@
      *                changed with key/value pairs through the headers parameter with
      *                "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value
      *                to disallow or allow cross domain redirection.
+     *                The headers must not include cookies. Instead, use the cookies param.
+     * @param cookies the cookies to be sent together with the request
      * @throws IllegalStateException if it is called in an invalid state
      */
     public void setDataSource(@NonNull Context context, @NonNull Uri uri,
-            @Nullable Map<String, String> headers) throws IOException, IllegalArgumentException,
-                    SecurityException, IllegalStateException {
+            @Nullable Map<String, String> headers, @Nullable List<HttpCookie> cookies)
+            throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {
         // The context and URI usually belong to the calling user. Get a resolver for that user
         // and strip out the userId from the URI if present.
         final ContentResolver resolver = context.getContentResolver();
@@ -1036,18 +1040,36 @@
             } else if (attemptDataSource(resolver, actualUri)) {
                 return;
             } else {
-                setDataSource(uri.toString(), headers);
+                setDataSource(uri.toString(), headers, cookies);
             }
         } else {
             // Try requested Uri locally first, or fallback to media server
             if (attemptDataSource(resolver, uri)) {
                 return;
             } else {
-                setDataSource(uri.toString(), headers);
+                setDataSource(uri.toString(), headers, cookies);
             }
         }
     }
 
+    /**
+     * Sets the data source as a content Uri.
+     *
+     * @param context the Context to use when resolving the Uri
+     * @param uri the Content URI of the data you want to play
+     * @param headers the headers to be sent together with the request for the data
+     *                Note that the cross domain redirection is allowed by default, but that can be
+     *                changed with key/value pairs through the headers parameter with
+     *                "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value
+     *                to disallow or allow cross domain redirection.
+     * @throws IllegalStateException if it is called in an invalid state
+     */
+    public void setDataSource(@NonNull Context context, @NonNull Uri uri,
+            @Nullable Map<String, String> headers)
+            throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {
+        setDataSource(context, uri, headers, null);
+    }
+
     private boolean attemptDataSource(ContentResolver resolver, Uri uri) {
         try (AssetFileDescriptor afd = resolver.openAssetFileDescriptor(uri, "r")) {
             setDataSource(afd);
@@ -1085,6 +1107,11 @@
      * @hide pending API council
      */
     public void setDataSource(String path, Map<String, String> headers)
+            throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {
+        setDataSource(path, headers, null);
+    }
+
+    private void setDataSource(String path, Map<String, String> headers, List<HttpCookie> cookies)
             throws IOException, IllegalArgumentException, SecurityException, IllegalStateException
     {
         String[] keys = null;
@@ -1101,10 +1128,11 @@
                 ++i;
             }
         }
-        setDataSource(path, keys, values);
+        setDataSource(path, keys, values, cookies);
     }
 
-    private void setDataSource(String path, String[] keys, String[] values)
+    private void setDataSource(String path, String[] keys, String[] values,
+            List<HttpCookie> cookies)
             throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {
         final Uri uri = Uri.parse(path);
         final String scheme = uri.getScheme();
@@ -1113,7 +1141,7 @@
         } else if (scheme != null) {
             // handle non-file sources
             nativeSetDataSource(
-                MediaHTTPService.createHttpServiceBinderIfNecessary(path),
+                MediaHTTPService.createHttpServiceBinderIfNecessary(path, cookies),
                 path,
                 keys,
                 values);
diff --git a/media/java/android/media/tv/TvContract.java b/media/java/android/media/tv/TvContract.java
index ddbd542e..45f88f3 100644
--- a/media/java/android/media/tv/TvContract.java
+++ b/media/java/android/media/tv/TvContract.java
@@ -1977,6 +1977,24 @@
         public static final String COLUMN_RECORDING_PROHIBITED = "recording_prohibited";
 
         /**
+         * The flag indicating whether this TV program is browsable or not.
+         *
+         * <p>This column can only be set by system apps. For other applications, it is a read-only
+         * column. Trying to modify it may cause {@link SecurityException}.
+         *
+         * <p>A value of 1 indicates that the program is browsable and can be shown to users in
+         * the UI. A value of 0 indicates that the program should be hidden from users and the
+         * application who changes this value to 0 should send
+         * {@link TvInputManager#ACTION_PROGRAM_BROWSABLE_DISABLED} to the owner of the program
+         * to notify this change.
+         *
+         * <p>This value is set to 1 (browsable) by default.
+         *
+         * <p>Type: INTEGER (boolean)
+         */
+        public static final String COLUMN_BROWSABLE = "browsable";
+
+        /**
          * The internal ID used by individual TV input services.
          *
          * <p>This is internal to the provider that inserted it, and should not be decoded by other
diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java
index b630270..4c2b031 100644
--- a/media/java/android/media/tv/TvInputManager.java
+++ b/media/java/android/media/tv/TvInputManager.java
@@ -325,23 +325,39 @@
             "android.media.tv.action.VIEW_RECORDING_SCHEDULES";
 
     /**
+     * Action sent by the system to tell the target TV input that one of its program's browsable
+     * state is disabled, i.e., it will no longer be shown to users, which, for example, might
+     * be a result of users' interaction with UI.
+     *
+     * <p>The intent must contain the following bundle parameter:
+     * <ul>
+     *     <li>{@link #EXTRA_PROGRAM_ID} the program ID as a long integer.
+     * </ul>
+     */
+    public static final String ACTION_PROGRAM_BROWSABLE_DISABLED =
+            "android.media.tv.action.PROGRAM_BROWSABLE_DISABLED";
+
+    /**
      * Action sent by an application telling the system to set the given channel as browsable.
      *
      * <p>The intent must contain the following bundle parameters:
      * <ul>
-     *     <li>{@link #EXTRA_CHANNEL_ID} then channel ID as an integer.
+     *     <li>{@link #EXTRA_CHANNEL_ID} the channel ID as a long integer.
      *     <li>{@link #EXTRA_PACKAGE_NAME} the package name of the requesting application.
      * </ul>
      */
     public static final String ACTION_MAKE_CHANNEL_BROWSABLE
             = "android.media.tv.action.MAKE_CHANNEL_BROWSABLE";
 
-    /** The key for a bundle parameter containing a channel ID as an integer */
+    /** The key for a bundle parameter containing a channel ID as a long integer */
     public static final String EXTRA_CHANNEL_ID = "android.media.tv.extra.CHANNEL_ID";
 
     /** The key for a bundle parameter containing a package name as a string. */
     public static final String EXTRA_PACKAGE_NAME = "android.media.tv.extra.PACKAGE_NAME";
 
+    /** The key for a bundle parameter containing a program ID as a long integer */
+    public static final String EXTRA_PROGRAM_ID = "android.media.tv.extra.PROGRAM_ID";
+
     private final ITvInputManager mService;
 
     private final Object mLock = new Object();
diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java
index 11e2a71..8653523 100644
--- a/packages/SettingsLib/src/com/android/settingslib/Utils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java
@@ -215,6 +215,13 @@
         return colorAccent;
     }
 
+    public static Drawable getDrawable(Context context, int attr) {
+        TypedArray ta = context.obtainStyledAttributes(new int[]{attr});
+        Drawable drawable = ta.getDrawable(0);
+        ta.recycle();
+        return drawable;
+    }
+
     /**
      * Determine whether a package is a "system package", in which case certain things (like
      * disabling notifications or disabling the package altogether) should be disallowed.
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java
index 0ec16ae2..9ac4d2d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java
@@ -510,7 +510,7 @@
                 }
 
                 NetworkKey key = NetworkKey.createFromScanResult(result);
-                if (!mRequestedScores.contains(key)) {
+                if (key != null && !mRequestedScores.contains(key)) {
                     scoresToRequest.add(key);
                 }
 
diff --git a/packages/SystemUI/plugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/SampleOverlayPlugin.java b/packages/SystemUI/plugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/SampleOverlayPlugin.java
index 13fc76c..79a0c35 100644
--- a/packages/SystemUI/plugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/SampleOverlayPlugin.java
+++ b/packages/SystemUI/plugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/SampleOverlayPlugin.java
@@ -24,7 +24,9 @@
 import android.view.ViewTreeObserver.InternalInsetsInfo;
 import android.view.ViewTreeObserver.OnComputeInternalInsetsListener;
 import com.android.systemui.plugins.OverlayPlugin;
+import com.android.systemui.plugins.annotations.Requires;
 
+@Requires(target = OverlayPlugin.class, version = OverlayPlugin.VERSION)
 public class SampleOverlayPlugin implements OverlayPlugin {
     private static final String TAG = "SampleOverlayPlugin";
     private Context mPluginContext;
@@ -36,12 +38,6 @@
     private float mStatusBarHeight;
 
     @Override
-    public int getVersion() {
-        Log.d(TAG, "getVersion " + VERSION);
-        return VERSION;
-    }
-
-    @Override
     public void onCreate(Context sysuiContext, Context pluginContext) {
         Log.d(TAG, "onCreate");
         mPluginContext = pluginContext;
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/IntentButtonProvider.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/IntentButtonProvider.java
index 9c173bd..97dbafd 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/IntentButtonProvider.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/IntentButtonProvider.java
@@ -14,6 +14,8 @@
 
 package com.android.systemui.plugins;
 
+import com.android.systemui.plugins.annotations.ProvidesInterface;
+
 import android.content.Intent;
 import android.graphics.drawable.Drawable;
 
@@ -21,6 +23,7 @@
  * An Intent Button represents a triggerable element in SysUI that consists of an
  * Icon and an intent to trigger when it is activated (clicked, swiped, etc.).
  */
+@ProvidesInterface(version = IntentButtonProvider.VERSION)
 public interface IntentButtonProvider extends Plugin {
 
     public static final int VERSION = 1;
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/OverlayPlugin.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/OverlayPlugin.java
index f5074f7..61aa60b 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/OverlayPlugin.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/OverlayPlugin.java
@@ -13,12 +13,15 @@
  */
 package com.android.systemui.plugins;
 
+import com.android.systemui.plugins.annotations.ProvidesInterface;
+
 import android.view.View;
 
+@ProvidesInterface(action = OverlayPlugin.ACTION, version = OverlayPlugin.VERSION)
 public interface OverlayPlugin extends Plugin {
 
     String ACTION = "com.android.systemui.action.PLUGIN_OVERLAY";
-    int VERSION = 1;
+    int VERSION = 2;
 
     void setup(View statusBar, View navBar);
 
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/Plugin.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/Plugin.java
index e75ecb7..bb93367 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/Plugin.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/Plugin.java
@@ -13,6 +13,8 @@
  */
 package com.android.systemui.plugins;
 
+import com.android.systemui.plugins.annotations.Requires;
+
 import android.content.Context;
 
 /**
@@ -111,18 +113,13 @@
 public interface Plugin {
 
     /**
-     * Should be implemented as the following directly referencing the version constant
-     * from the plugin interface being implemented, this will allow recompiles to automatically
-     * pick up the current version.
-     * <pre class="prettyprint">
-     * {@literal
-     * public int getVersion() {
-     *     return VERSION;
-     * }
-     * }
-     * @return
+     * @deprecated
+     * @see Requires
      */
-    int getVersion();
+    default int getVersion() {
+        // Default of -1 indicates the plugin supports the new Requires model.
+        return -1;
+    }
 
     default void onCreate(Context sysuiContext, Context pluginContext) {
     }
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Dependencies.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Dependencies.java
new file mode 100644
index 0000000..dbbf047
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Dependencies.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2017 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.plugins.annotations;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Used for repeated @DependsOn internally, not for plugin
+ * use.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Dependencies {
+    DependsOn[] value();
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/DependsOn.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/DependsOn.java
new file mode 100644
index 0000000..b81d673
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/DependsOn.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 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.plugins.annotations;
+
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Used to indicate that an interface in the plugin library needs another
+ * interface to function properly. When this is added, it will be enforced
+ * that all plugins that @Requires the annotated interface also @Requires
+ * the specified class as well.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Repeatable(value = Dependencies.class)
+public @interface DependsOn {
+    Class<?> target();
+
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/ProvidesInterface.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/ProvidesInterface.java
new file mode 100644
index 0000000..d0e14b8
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/ProvidesInterface.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2017 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.plugins.annotations;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Should be added to all interfaces in plugin lib to specify their
+ * current version and optionally their action to implement the plugin.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ProvidesInterface {
+    int version();
+
+    String action() default "";
+
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Requirements.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Requirements.java
new file mode 100644
index 0000000..9cfa279
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Requirements.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2017 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.plugins.annotations;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Used for repeated @Requires internally, not for plugin
+ * use.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Requirements {
+    Requires[] value();
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Requires.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Requires.java
new file mode 100644
index 0000000..e1b1303
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Requires.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2017 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.plugins.annotations;
+
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Used to annotate which interfaces a given plugin depends on.
+ *
+ * At minimum all plugins should have at least one @Requires annotation
+ * for the plugin interface that they are implementing. They will also
+ * need an @Requires for each class that the plugin interface @DependsOn.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Repeatable(value = Requirements.class)
+public @interface Requires {
+    Class<?> target();
+    int version();
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/doze/DozeProvider.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/doze/DozeProvider.java
index 688df46..0688481 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/doze/DozeProvider.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/doze/DozeProvider.java
@@ -20,10 +20,12 @@
 import android.content.Context;
 
 import com.android.systemui.plugins.Plugin;
+import com.android.systemui.plugins.annotations.ProvidesInterface;
 
 /**
  * Provides a {@link DozeUi}.
  */
+@ProvidesInterface(action = DozeProvider.ACTION, version = DozeProvider.VERSION)
 public interface DozeProvider extends Plugin {
 
     String ACTION = "com.android.systemui.action.PLUGIN_DOZE";
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java
index e21a282..b7467eb 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java
@@ -14,29 +14,32 @@
 
 package com.android.systemui.plugins.qs;
 
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.app.PendingIntent;
+import com.android.systemui.plugins.FragmentBase;
+import com.android.systemui.plugins.annotations.DependsOn;
+import com.android.systemui.plugins.annotations.ProvidesInterface;
+import com.android.systemui.plugins.qs.QS.Callback;
+import com.android.systemui.plugins.qs.QS.DetailAdapter;
+import com.android.systemui.plugins.qs.QS.HeightListener;
+
 import android.content.Context;
 import android.content.Intent;
 import android.util.AttributeSet;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.FrameLayout;
 import android.widget.RelativeLayout;
 
-import com.android.systemui.plugins.FragmentBase;
-
 /**
  * Fragment that contains QS in the notification shade.  Most of the interface is for
  * handling the expand/collapsing of the view interaction.
  */
+@ProvidesInterface(action = QS.ACTION, version = QS.VERSION)
+@DependsOn(target = HeightListener.class)
+@DependsOn(target = Callback.class)
+@DependsOn(target = DetailAdapter.class)
 public interface QS extends FragmentBase {
 
     public static final String ACTION = "com.android.systemui.action.PLUGIN_QS";
 
-    // This should be incremented any time this class or ActivityStarter or BaseStatusBarHeader
-    // change in incompatible ways.
     public static final int VERSION = 5;
 
     String TAG = "QS";
@@ -64,17 +67,23 @@
 
     public abstract void setContainer(ViewGroup container);
 
+    @ProvidesInterface(version = HeightListener.VERSION)
     public interface HeightListener {
+        public static final int VERSION = 1;
         void onQsHeightChanged();
     }
 
+    @ProvidesInterface(version = Callback.VERSION)
     public interface Callback {
+        public static final int VERSION = 1;
         void onShowingDetail(DetailAdapter detail, int x, int y);
         void onToggleStateChanged(boolean state);
         void onScanStateChanged(boolean state);
     }
 
+    @ProvidesInterface(version = DetailAdapter.VERSION)
     public interface DetailAdapter {
+        public static final int VERSION = 1;
         CharSequence getTitle();
         Boolean getToggleState();
         default boolean getToggleEnabled() {
@@ -92,7 +101,9 @@
         default boolean hasHeader() { return true; }
     }
 
+    @ProvidesInterface(version = BaseStatusBarHeader.VERSION)
     public abstract static class BaseStatusBarHeader extends RelativeLayout {
+        public static final int VERSION = 1;
 
         public BaseStatusBarHeader(Context context, AttributeSet attrs) {
             super(context, attrs);
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowProvider.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowProvider.java
index 41a0907..bc98c8e 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowProvider.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowProvider.java
@@ -10,7 +10,10 @@
 import java.util.ArrayList;
 
 import com.android.systemui.plugins.Plugin;
+import com.android.systemui.plugins.annotations.ProvidesInterface;
 
+@ProvidesInterface(action = NotificationMenuRowProvider.ACTION,
+        version = NotificationMenuRowProvider.VERSION)
 public interface NotificationMenuRowProvider extends Plugin {
 
     public static final String ACTION = "com.android.systemui.action.PLUGIN_NOTIFICATION_MENU_ROW";
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavBarButtonProvider.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavBarButtonProvider.java
index d54e33f..5243228 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavBarButtonProvider.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavBarButtonProvider.java
@@ -14,14 +14,15 @@
 
 package com.android.systemui.plugins.statusbar.phone;
 
-import android.annotation.DrawableRes;
 import android.annotation.Nullable;
 import android.graphics.drawable.Drawable;
 import android.view.View;
 import android.view.ViewGroup;
 
 import com.android.systemui.plugins.Plugin;
+import com.android.systemui.plugins.annotations.ProvidesInterface;
 
+@ProvidesInterface(action = NavBarButtonProvider.ACTION, version = NavBarButtonProvider.VERSION)
 public interface NavBarButtonProvider extends Plugin {
 
     public static final String ACTION = "com.android.systemui.action.PLUGIN_NAV_BUTTON";
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavGesture.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavGesture.java
index 918d6e9..ddee89e 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavGesture.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavGesture.java
@@ -17,7 +17,9 @@
 import android.view.MotionEvent;
 
 import com.android.systemui.plugins.Plugin;
+import com.android.systemui.plugins.annotations.ProvidesInterface;
 
+@ProvidesInterface(action = NavGesture.ACTION, version = NavBarButtonProvider.VERSION)
 public interface NavGesture extends Plugin {
 
     public static final String ACTION = "com.android.systemui.action.PLUGIN_NAV_GESTURE";
diff --git a/packages/SystemUI/res/drawable/ic_data_saver.xml b/packages/SystemUI/res/drawable/ic_data_saver.xml
index 9c3bd3a..64bbff0 100644
--- a/packages/SystemUI/res/drawable/ic_data_saver.xml
+++ b/packages/SystemUI/res/drawable/ic_data_saver.xml
@@ -23,7 +23,7 @@
         android:fillColor="#FFFFFFFF"
         android:pathData="M12.0,19.0c-3.9,0.0 -7.0,-3.1 -7.0,-7.0c0.0,-3.5 2.6,-6.4 6.0,-6.9L11.0,2.0C5.9,2.5 2.0,6.8 2.0,12.0c0.0,5.5 4.5,10.0 10.0,10.0c3.3,0.0 6.2,-1.6 8.1,-4.1l-2.6,-1.5C16.2,18.0 14.2,19.0 12.0,19.0z"/>
     <path
-        android:fillColor="#4DFFFFFF"
+        android:fillColor="#54FFFFFF"
         android:pathData="M13.0,2.0l0.0,3.0c3.4,0.5 6.0,3.4 6.0,6.9c0.0,0.9 -0.2,1.8 -0.5,2.5l2.6,1.5c0.6,-1.2 0.9,-2.6 0.9,-4.1C22.0,6.8 18.0,2.6 13.0,2.0z"/>
     <path
         android:fillColor="#FFFFFFFF"
diff --git a/packages/SystemUI/res/drawable/ic_data_saver_off.xml b/packages/SystemUI/res/drawable/ic_data_saver_off.xml
index 918c61c..3001ad9 100644
--- a/packages/SystemUI/res/drawable/ic_data_saver_off.xml
+++ b/packages/SystemUI/res/drawable/ic_data_saver_off.xml
@@ -20,9 +20,9 @@
         android:viewportHeight="24.0"
         android:tint="?android:attr/colorControlNormal">
     <path
-        android:fillColor="#4DFFFFFF"
+        android:fillColor="#FFFFFFFF"
         android:pathData="M12.0,19.0c-3.9,0.0 -7.0,-3.1 -7.0,-7.0c0.0,-3.5 2.6,-6.4 6.0,-6.9L11.0,2.0C5.9,2.5 2.0,6.8 2.0,12.0c0.0,5.5 4.5,10.0 10.0,10.0c3.3,0.0 6.2,-1.6 8.1,-4.1l-2.6,-1.5C16.2,18.0 14.2,19.0 12.0,19.0z"/>
     <path
-        android:fillColor="#4DFFFFFF"
+        android:fillColor="#FFFFFFFF"
         android:pathData="M13.0,2.0l0.0,3.0c3.4,0.5 6.0,3.4 6.0,6.9c0.0,0.9 -0.2,1.8 -0.5,2.5l2.6,1.5c0.6,-1.2 0.9,-2.6 0.9,-4.1C22.0,6.8 18.0,2.6 13.0,2.0z"/>
 </vector>
diff --git a/packages/SystemUI/res/drawable/ic_remove_circle.xml b/packages/SystemUI/res/drawable/ic_remove_circle.xml
new file mode 100644
index 0000000..439cc78
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_remove_circle.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="48dp"
+    android:width="48dp"
+    android:tint="#db4437"
+    android:viewportHeight="48"
+    android:viewportWidth="48" >
+    <path android:fillColor="@android:color/white"
+       android:pathData="M24,4C12.95,4,4,12.95,4,24
+                         s8.95,20,20,20,20-8.95,20-20
+                         S35.05,4,24,4zm10,22H14v-4h20v4z"/>
+</vector>
diff --git a/packages/SystemUI/res/layout/battery_percentage_view.xml b/packages/SystemUI/res/layout/battery_percentage_view.xml
new file mode 100644
index 0000000..acae9f5
--- /dev/null
+++ b/packages/SystemUI/res/layout/battery_percentage_view.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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
+  -->
+
+<!-- Loaded into BatteryMeterView as necessary -->
+<TextView
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/battery_percentage_view"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:singleLine="true"
+        android:textAppearance="@style/TextAppearance.StatusBar.Expanded.Clock"
+        android:textColor="?android:attr/textColorPrimary"
+        android:gravity="center_vertical|start"
+        android:paddingStart="4dp"
+        />
diff --git a/packages/SystemUI/res/layout/divider.xml b/packages/SystemUI/res/layout/divider.xml
index 9581437..f1f0df0 100644
--- a/packages/SystemUI/res/layout/divider.xml
+++ b/packages/SystemUI/res/layout/divider.xml
@@ -16,5 +16,6 @@
 <View xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="72dp"
     android:layout_height="1dp"
+    android:layout_marginTop="8dp"
     android:background="?android:attr/colorForeground"
     android:alpha="?android:attr/disabledAlpha" />
diff --git a/packages/SystemUI/res/layout/preference_widget_radiobutton.xml b/packages/SystemUI/res/layout/preference_widget_radiobutton.xml
new file mode 100644
index 0000000..b3ec43d
--- /dev/null
+++ b/packages/SystemUI/res/layout/preference_widget_radiobutton.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2006 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.
+-->
+
+<!-- Layout used by CheckBoxPreference for the checkbox style. This is inflated
+     inside android.R.layout.preference. -->
+<RadioButton xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@android:id/checkbox"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center"
+    android:focusable="false"
+    android:clickable="false" />
diff --git a/packages/SystemUI/res/layout/qs_page_indicator.xml b/packages/SystemUI/res/layout/qs_page_indicator.xml
index 02bd31a..583753a 100644
--- a/packages/SystemUI/res/layout/qs_page_indicator.xml
+++ b/packages/SystemUI/res/layout/qs_page_indicator.xml
@@ -20,9 +20,8 @@
     android:layout_width="match_parent"
     android:layout_height="48dp"
     android:layout_gravity="center"
-    android:layout_marginTop="40dp"
     android:layout_marginBottom="24dp"
     android:focusable="true"
     android:gravity="center"
     android:importantForAccessibility="yes"
-    android:visibility="gone"/>
\ No newline at end of file
+    android:visibility="gone"/>
diff --git a/packages/SystemUI/res/layout/qs_paged_tile_layout.xml b/packages/SystemUI/res/layout/qs_paged_tile_layout.xml
index 8ff1d1e..00427cb 100644
--- a/packages/SystemUI/res/layout/qs_paged_tile_layout.xml
+++ b/packages/SystemUI/res/layout/qs_paged_tile_layout.xml
@@ -19,6 +19,7 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
+    android:paddingBottom="24dp"
     android:clipChildren="false"
     android:clipToPadding="false">
 
diff --git a/packages/SystemUI/res/layout/qs_tile_label.xml b/packages/SystemUI/res/layout/qs_tile_label.xml
index a093b87..8d1f9e4 100644
--- a/packages/SystemUI/res/layout/qs_tile_label.xml
+++ b/packages/SystemUI/res/layout/qs_tile_label.xml
@@ -16,10 +16,12 @@
 -->
 <LinearLayout
         xmlns:android="http://schemas.android.com/apk/res/android"
-        android:layout_width="wrap_content"
+        android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:orientation="horizontal"
-        android:paddingTop="16dp">
+        android:gravity="center_horizontal"
+        android:paddingTop="8dp"
+        android:paddingBottom="8dp">
      <TextView android:id="@+id/tile_label"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
diff --git a/packages/SystemUI/res/layout/quick_settings_brightness_dialog.xml b/packages/SystemUI/res/layout/quick_settings_brightness_dialog.xml
index 6988c76..080f553 100644
--- a/packages/SystemUI/res/layout/quick_settings_brightness_dialog.xml
+++ b/packages/SystemUI/res/layout/quick_settings_brightness_dialog.xml
@@ -17,7 +17,6 @@
     xmlns:systemui="http://schemas.android.com/apk/res-auto"
     android:layout_height="48dp"
     android:layout_width="match_parent"
-    android:layout_marginBottom="24dp"
     android:paddingLeft="16dp"
     android:paddingRight="16dp"
     style="@style/BrightnessDialogContainer">
diff --git a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
index 3e8e72a..78d4bdd 100644
--- a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
+++ b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
@@ -85,16 +85,6 @@
 
                 <include layout="@layout/system_icons" />
             </FrameLayout>
-
-            <TextView
-                android:id="@+id/battery_level"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_gravity="center_vertical"
-                android:layout_marginStart="@dimen/header_battery_margin_expanded"
-                android:importantForAccessibility="noHideDescendants"
-                android:textColor="?android:attr/textColorPrimary"
-                android:textSize="@dimen/battery_level_text_size"/>
         </LinearLayout>
 
         <com.android.systemui.statusbar.AlphaOptimizedFrameLayout
diff --git a/packages/SystemUI/res/layout/system_icons.xml b/packages/SystemUI/res/layout/system_icons.xml
index 3a33992..bfa92ad 100644
--- a/packages/SystemUI/res/layout/system_icons.xml
+++ b/packages/SystemUI/res/layout/system_icons.xml
@@ -31,9 +31,8 @@
         android:layout_height="wrap_content"
         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="@dimen/status_bar_battery_icon_height"
-        android:layout_width="@dimen/status_bar_battery_icon_width"
-        android:layout_marginBottom="@dimen/battery_margin_bottom"/>
+        android:layout_height="match_parent"
+        android:layout_width="wrap_content"
+        />
 </LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index e737d2d..40d4d6f 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -69,8 +69,8 @@
     <!-- Height of a small notification in the status bar-->
     <dimen name="notification_min_height">92dp</dimen>
 
-    <!-- Height of a small notification in the status bar if it is a large (like messaging)-->
-    <dimen name="notification_min_height_large">132dp</dimen>
+    <!-- Increased height of a small notification in the status bar -->
+    <dimen name="notification_min_height_increased">132dp</dimen>
 
     <!-- Height of a small notification in the status bar which was used before android N -->
     <dimen name="notification_min_height_legacy">64dp</dimen>
@@ -87,6 +87,9 @@
     <!-- Height of a heads up notification in the status bar -->
     <dimen name="notification_max_heads_up_height">148dp</dimen>
 
+    <!-- Height of a heads up notification in the status bar -->
+    <dimen name="notification_max_heads_up_height_increased">188dp</dimen>
+
     <!-- a threshold in dp per second that is considered fast scrolling -->
     <dimen name="scroll_fast_threshold">1500dp</dimen>
 
@@ -216,8 +219,8 @@
     <!-- The size of the gesture span needed to activate the "pull" notification expansion -->
     <dimen name="pull_span_min">25dp</dimen>
 
-    <dimen name="qs_tile_height">80dp</dimen>
-    <dimen name="qs_tile_margin">36dp</dimen>
+    <dimen name="qs_tile_height">88dp</dimen>
+    <dimen name="qs_tile_margin">28dp</dimen>
     <dimen name="qs_tile_margin_top">16dp</dimen>
     <dimen name="qs_quick_tile_size">48dp</dimen>
     <dimen name="qs_quick_tile_padding">12dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 67def4f..77de9a2 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1527,14 +1527,12 @@
     <!-- SysUI Tuner: Button that controls layout of navigation bar [CHAR LIMIT=60] -->
     <string name="nav_bar_layout">Layout</string>
 
-    <!-- SysUI Tuner: Label for section of settings about the left nav button [CHAR LIMIT=60] -->
-    <string name="nav_bar_left">Left</string>
-
-    <!-- SysUI Tuner: Label for section of settings about the right nav button [CHAR LIMIT=60] -->
-    <string name="nav_bar_right">Right</string>
+    <!-- SysUI Tuner: Setting for button type in nav bar [CHAR LIMIT=60] -->
+    <string name="left_nav_bar_button_type">Extra left button type</string>
 
     <!-- SysUI Tuner: Setting for button type in nav bar [CHAR LIMIT=60] -->
-    <string name="nav_bar_button_type">Button type</string>
+    <string name="right_nav_bar_button_type">Extra right button type</string>
+
 
     <!-- SysUI Tuner: Added to nav bar option to indicate it is the default [CHAR LIMIT=60] -->
     <string name="nav_bar_default"> (default)</string>
@@ -1543,7 +1541,7 @@
     <string-array name="nav_bar_buttons">
         <item>Clipboard</item>
         <item>Keycode</item>
-        <item>Menu / Keyboard Switcher</item>
+        <item>Keyboard switcher</item>
         <item>None</item>
     </string-array>
     <string-array name="nav_bar_button_values" translatable="false">
@@ -1555,10 +1553,10 @@
 
     <!-- SysUI Tuner: Labels for different types of navigation bar layouts [CHAR LIMIT=60] -->
     <string-array name="nav_bar_layouts">
-        <item>Divided (default)</item>
-        <item>Centered</item>
-        <item>Left-aligned</item>
-        <item>Right-aligned</item>
+        <item>Normal</item>
+        <item>Compact</item>
+        <item>Left-leaning</item>
+        <item>Right-leaning</item>
     </string-array>
 
     <string-array name="nav_bar_layouts_values" translatable="false">
@@ -1569,7 +1567,7 @@
     </string-array>
 
     <!-- SysUI Tuner: Name of Combination Menu / Keyboard Switcher button [CHAR LIMIT=30] -->
-    <string name="menu_ime">Menu / Keyboard Switcher</string>
+    <string name="menu_ime">Keyboard switcher</string>
     <!-- SysUI Tuner: Save the current settings [CHAR LIMIT=30] -->
     <string name="save">Save</string>
     <!-- SysUI Tuner: Reset to default settings [CHAR LIMIT=30] -->
@@ -1585,10 +1583,16 @@
     <string name="accessibility_key">Custom navigation button</string>
 
     <!-- SysUI Tuner: Nav bar button that emulates a keycode [CHAR LIMIT=30] -->
-    <string name="keycode">Keycode</string>
+    <string name="left_keycode">Left keycode</string>
+
+    <!-- SysUI Tuner: Nav bar button that emulates a keycode [CHAR LIMIT=30] -->
+    <string name="right_keycode">Right keycode</string>
 
     <!-- SysUI Tuner: Settings to change nav bar icon [CHAR LIMIT=30] -->
-    <string name="icon">Icon</string>
+    <string name="left_icon">Left icon</string>
+
+    <!-- SysUI Tuner: Settings to change nav bar icon [CHAR LIMIT=30] -->
+    <string name="right_icon">Right icon</string>
 
     <!-- Label for area where tiles can be dragged out of [CHAR LIMIT=60] -->
     <string name="drag_to_add_tiles">Drag to add tiles</string>
@@ -1725,6 +1729,9 @@
         not appear on production builds ever. -->
     <string name="tuner_doze_always_on" translatable="false">Always on</string>
 
+    <!-- SysUI Tuner: Section to customize lockscreen shortcuts [CHAR LIMIT=60] -->
+    <string name="tuner_lock_screen">Lock screen</string>
+
     <!-- Making the PIP fullscreen [CHAR LIMIT=25] -->
     <string name="pip_phone_expand">Expand</string>
 
@@ -1760,20 +1767,47 @@
     <!-- Text body for dialog alerting user that their phone has reached a certain temperature and may start to slow down in order to cool down. [CHAR LIMIT=300] -->
     <string name="high_temp_dialog_message">Your phone will automatically try to cool down. You can still use your phone, but it may run slower.\n\nOnce your phone has cooled down, it will run normally.</string>
 
-    <!-- SysUI Tuner: Group of settings for left lock screen affordance [CHAR LIMIT=60] -->
-    <string name="lockscreen_left">Left</string>
-
-    <!-- SysUI Tuner: Group of settings for right lock screen affordance [CHAR LIMIT=60] -->
-    <string name="lockscreen_right">Right</string>
-
-    <!-- SysUI Tuner: Switch controlling whether to customize lock screen button [CHAR LIMIT=60] -->
-    <string name="lockscreen_customize">Customize shortcut</string>
+    <!-- SysUI Tuner: Button to select lock screen shortcut [CHAR LIMIT=60] -->
+    <string name="lockscreen_shortcut_left">Left shortcut</string>
 
     <!-- SysUI Tuner: Button to select lock screen shortcut [CHAR LIMIT=60] -->
-    <string name="lockscreen_shortcut">Shortcut</string>
+    <string name="lockscreen_shortcut_right">Right shortcut</string>
 
-    <!-- SysUI Tuner: Switch to control if device gets unlocked [CHAR LIMIT=60] -->
-    <string name="lockscreen_unlock">Prompt for password</string>
+    <!-- SysUI Tuner: Switch to control if device gets unlocked by left shortcut [CHAR LIMIT=60] -->
+    <string name="lockscreen_unlock_left">Left shortcut also unlocks</string>
+
+    <!-- SysUI Tuner: Switch to control if device gets unlocked by right shortcut [CHAR LIMIT=60] -->
+    <string name="lockscreen_unlock_right">Right shortcut also unlocks</string>
+
+    <!-- SysUI Tuner: Summary of no shortcut being selected [CHAR LIMIT=60] -->
+    <string name="lockscreen_none">None</string>
+
+    <!-- SysUI Tuner: Format string for describing launching an app [CHAR LIMIT=60] -->
+    <string name="tuner_launch_app">Launch <xliff:g id="app" example="Settings">%1$s</xliff:g></string>
+
+    <!-- SysUI Tuner: Label for section of other apps that can be launched [CHAR LIMIT=60] -->
+    <string name="tuner_other_apps">Other apps</string>
+
+    <!-- SysUI Tuner: Label for icon shaped like a circle [CHAR LIMIT=60] -->
+    <string name="tuner_circle">Circle</string>
+
+    <!-- SysUI Tuner: Label for icon shaped like a plus [CHAR LIMIT=60] -->
+    <string name="tuner_plus">Plus</string>
+
+    <!-- SysUI Tuner: Label for icon shaped like a minus [CHAR LIMIT=60] -->
+    <string name="tuner_minus">Minus</string>
+
+    <!-- SysUI Tuner: Label for icon shaped like a left [CHAR LIMIT=60] -->
+    <string name="tuner_left">Left</string>
+
+    <!-- SysUI Tuner: Label for icon shaped like a right [CHAR LIMIT=60] -->
+    <string name="tuner_right">Right</string>
+
+    <!-- SysUI Tuner: Label for icon shaped like a menu [CHAR LIMIT=60] -->
+    <string name="tuner_menu">Menu</string>
+
+    <!-- SysUI Tuner: App subheading for shortcut selection [CHAR LIMIT=60] -->
+    <string name="tuner_app"><xliff:g id="app">%1$s</xliff:g> app</string>
 
     <!-- Title for the notification channel containing important alerts like low battery. [CHAR LIMIT=NONE] -->
     <string name="notification_channel_alerts">Alerts</string>
diff --git a/packages/SystemUI/res/xml/lockscreen_settings.xml b/packages/SystemUI/res/xml/lockscreen_settings.xml
index 73e29af..1e7d266 100644
--- a/packages/SystemUI/res/xml/lockscreen_settings.xml
+++ b/packages/SystemUI/res/xml/lockscreen_settings.xml
@@ -18,42 +18,25 @@
     xmlns:sysui="http://schemas.android.com/apk/res-auto"
     android:title="@string/other">
 
-    <PreferenceCategory
-        android:key="left"
-        android:title="@string/lockscreen_left">
 
-        <SwitchPreference
-            android:key="customize"
-            android:title="@string/lockscreen_customize" />
+    <Preference
+        android:key="sysui_keyguard_left"
+        android:title="@string/lockscreen_shortcut_left"
+        android:fragment="com.android.systemui.tuner.ShortcutPicker" />
 
-        <Preference
-            android:key="shortcut"
-            android:title="@string/lockscreen_shortcut" />
+    <com.android.systemui.tuner.TunerSwitch
+        android:key="sysui_keyguard_left_unlock"
+        android:title="@string/lockscreen_unlock_left"
+        sysui:defValue="true" />
 
-        <com.android.systemui.tuner.TunerSwitch
-            android:key="sysui_keyguard_left_unlock"
-            android:title="@string/lockscreen_unlock"
-            sysui:defValue="true" />
+    <Preference
+        android:key="sysui_keyguard_right"
+        android:title="@string/lockscreen_shortcut_right" 
+        android:fragment="com.android.systemui.tuner.ShortcutPicker" />
 
-    </PreferenceCategory>
-
-    <PreferenceCategory
-        android:key="right"
-        android:title="@string/lockscreen_right">
-
-        <SwitchPreference
-            android:key="customize"
-            android:title="@string/lockscreen_customize" />
-
-        <Preference
-            android:key="shortcut"
-            android:title="@string/lockscreen_shortcut" />
-
-        <com.android.systemui.tuner.TunerSwitch
-            android:key="sysui_keyguard_right_unlock"
-            android:title="@string/lockscreen_unlock"
-            sysui:defValue="true" />
-
-    </PreferenceCategory>
+    <com.android.systemui.tuner.TunerSwitch
+        android:key="sysui_keyguard_right_unlock"
+        android:title="@string/lockscreen_unlock_right"
+        sysui:defValue="true" />
 
 </PreferenceScreen>
diff --git a/packages/SystemUI/res/xml/nav_bar_tuner.xml b/packages/SystemUI/res/xml/nav_bar_tuner.xml
index 6fa8bec..68e8fad 100644
--- a/packages/SystemUI/res/xml/nav_bar_tuner.xml
+++ b/packages/SystemUI/res/xml/nav_bar_tuner.xml
@@ -18,7 +18,7 @@
     xmlns:sysui="http://schemas.android.com/apk/res-auto"
     android:title="@string/nav_bar">
 
-    <ListPreference
+    <com.android.systemui.tuner.RadioListPreference
         android:key="layout"
         android:title="@string/nav_bar_layout"
         android:summary="%s"
@@ -26,54 +26,42 @@
         android:entries="@array/nav_bar_layouts"
         android:entryValues="@array/nav_bar_layouts_values" />
 
-    <PreferenceCategory
-        android:key="left"
-        android:title="@string/nav_bar_left">
+    <com.android.systemui.tuner.RadioListPreference
+        android:key="type_left"
+        android:title="@string/left_nav_bar_button_type"
+        android:persistent="false"
+        android:summary="%s"
+        android:entries="@array/nav_bar_buttons"
+        android:entryValues="@array/nav_bar_button_values" />
 
-        <DropDownPreference
-            android:key="type_left"
-            android:title="@string/nav_bar_button_type"
-            android:persistent="false"
-            android:summary="%s"
-            android:entries="@array/nav_bar_buttons"
-            android:entryValues="@array/nav_bar_button_values" />
+    <Preference
+        android:key="keycode_left"
+        android:persistent="false"
+        android:title="@string/left_keycode" />
 
-        <Preference
-            android:key="keycode_left"
-            android:persistent="false"
-            android:title="@string/keycode" />
+    <com.android.systemui.tuner.RadioListPreference
+        android:key="icon_left"
+        android:persistent="false"
+        android:summary="%s"
+        android:title="@string/left_icon" />
 
-        <com.android.systemui.tuner.BetterListPreference
-            android:key="icon_left"
-            android:persistent="false"
-            android:summary="%s"
-            android:title="@string/icon" />
+    <com.android.systemui.tuner.RadioListPreference
+        android:key="type_right"
+        android:title="@string/right_nav_bar_button_type"
+        android:summary="%s"
+        android:persistent="false"
+        android:entries="@array/nav_bar_buttons"
+        android:entryValues="@array/nav_bar_button_values" />
 
-    </PreferenceCategory>
+    <Preference
+        android:key="keycode_right"
+        android:persistent="false"
+        android:title="@string/right_keycode" />
 
-    <PreferenceCategory
-        android:key="right"
-        android:title="@string/nav_bar_right">
-
-        <DropDownPreference
-            android:key="type_right"
-            android:title="@string/nav_bar_button_type"
-            android:summary="%s"
-            android:persistent="false"
-            android:entries="@array/nav_bar_buttons"
-            android:entryValues="@array/nav_bar_button_values" />
-
-        <Preference
-            android:key="keycode_right"
-            android:persistent="false"
-            android:title="@string/keycode" />
-
-        <com.android.systemui.tuner.BetterListPreference
-            android:key="icon_right"
-            android:persistent="false"
-            android:summary="%s"
-            android:title="@string/icon" />
-
-    </PreferenceCategory>
+    <com.android.systemui.tuner.RadioListPreference
+        android:key="icon_right"
+        android:persistent="false"
+        android:summary="%s"
+        android:title="@string/right_icon" />
 
 </PreferenceScreen>
diff --git a/packages/SystemUI/res/xml/tuner_prefs.xml b/packages/SystemUI/res/xml/tuner_prefs.xml
index 6198ab7..c354811 100644
--- a/packages/SystemUI/res/xml/tuner_prefs.xml
+++ b/packages/SystemUI/res/xml/tuner_prefs.xml
@@ -121,6 +121,7 @@
 
     </PreferenceScreen>
 
+    <!--
     <PreferenceScreen
       android:key="picture_in_picture"
       android:title="@string/picture_in_picture">
@@ -143,6 +144,7 @@
           sysui:defValue="false" />
 
     </PreferenceScreen>
+    -->
 
     <Preference
         android:key="nav_bar"
@@ -151,15 +153,10 @@
 
     <Preference
             android:key="lockscreen"
-            android:title="@string/accessibility_desc_lock_screen"
+            android:title="@string/tuner_lock_screen"
             android:fragment="com.android.systemui.tuner.LockscreenFragment" />
 
     <Preference
-            android:key="other"
-            android:title="@string/other"
-            android:fragment="com.android.systemui.tuner.OtherPrefs" />
-
-    <Preference
             android:key="plugins"
             android:title="@string/plugins"
             android:fragment="com.android.systemui.tuner.PluginFragment" />
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 80d4a26..1f58d4c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -595,6 +595,10 @@
         }
     }
 
+    public boolean isScreenOn() {
+        return mScreenOn;
+    }
+
     static class DisplayClientState {
         public int clientGeneration;
         public boolean clearing;
diff --git a/packages/SystemUI/src/com/android/systemui/BatteryMeterDrawable.java b/packages/SystemUI/src/com/android/systemui/BatteryMeterDrawable.java
deleted file mode 100644
index 9068079..0000000
--- a/packages/SystemUI/src/com/android/systemui/BatteryMeterDrawable.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui;
-
-import android.content.Context;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.os.Handler;
-import android.provider.Settings;
-import com.android.settingslib.graph.BatteryMeterDrawableBase;
-import com.android.systemui.statusbar.policy.BatteryController;
-
-public class BatteryMeterDrawable extends BatteryMeterDrawableBase implements
-        BatteryController.BatteryStateChangeCallback {
-
-    public static final String SHOW_PERCENT_SETTING = "status_bar_show_battery_percent";
-
-    private BatteryController mBatteryController;
-    private SettingObserver mSettingObserver;
-
-    public BatteryMeterDrawable(Context context, int frameColor) {
-        super(context, frameColor);
-
-        mSettingObserver = new SettingObserver(new Handler(mContext.getMainLooper()));
-    }
-
-    @Override
-    public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
-        setBatteryLevel(level);
-        setPluggedIn(pluggedIn);
-    }
-
-    @Override
-    public void onPowerSaveChanged(boolean isPowerSave) {
-        setPowerSave(isPowerSave);
-    }
-
-    public void startListening() {
-        mContext.getContentResolver().registerContentObserver(
-                Settings.System.getUriFor(SHOW_PERCENT_SETTING), false, mSettingObserver);
-        updateShowPercent();
-        mBatteryController.addCallback(this);
-    }
-
-    public void stopListening() {
-        mContext.getContentResolver().unregisterContentObserver(mSettingObserver);
-        mBatteryController.removeCallback(this);
-    }
-
-    protected void updateShowPercent() {
-        setShowPercent(0 != Settings.System.getInt(mContext.getContentResolver(),
-                SHOW_PERCENT_SETTING, 0));
-    }
-
-    public void setBatteryController(BatteryController batteryController) {
-        mBatteryController = batteryController;
-        setPowerSave(mBatteryController.isPowerSave());
-    }
-
-    private final class SettingObserver extends ContentObserver {
-        public SettingObserver(Handler handler) {
-            super(handler);
-        }
-
-        @Override
-        public void onChange(boolean selfChange, Uri uri) {
-            super.onChange(selfChange, uri);
-            updateShowPercent();
-            postInvalidate();
-        }
-    }
-
-}
diff --git a/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java
index 69e3874..bda4c95 100644
--- a/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java
+++ b/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java
@@ -23,10 +23,22 @@
 import android.util.ArraySet;
 import android.util.AttributeSet;
 import android.util.TypedValue;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.Settings;
+import android.util.ArraySet;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.LayoutInflater;
 import android.view.View;
+import android.view.ViewGroup;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import com.android.settingslib.graph.BatteryMeterDrawableBase;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
@@ -37,12 +49,23 @@
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.tuner.TunerService.Tunable;
 
-public class BatteryMeterView extends ImageView implements
+import java.text.NumberFormat;
+
+public class BatteryMeterView extends LinearLayout implements
         BatteryStateChangeCallback, Tunable, DarkReceiver, ConfigurationListener {
 
-    private final BatteryMeterDrawable mDrawable;
+    public static final String SHOW_PERCENT_SETTING = "status_bar_show_battery_percent";
+
+    private final BatteryMeterDrawableBase mDrawable;
     private final String mSlotBattery;
+    private final ImageView mBatteryIconView;
+    private TextView mBatteryPercentView;
+
     private BatteryController mBatteryController;
+    private SettingObserver mSettingObserver;
+    private int mTextColor;
+    private int mLevel;
+    private boolean mForceShowPercent;
 
     public BatteryMeterView(Context context) {
         this(context, null, 0);
@@ -55,16 +78,40 @@
     public BatteryMeterView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
 
+        setOrientation(LinearLayout.HORIZONTAL);
+        setGravity(Gravity.CENTER_VERTICAL | Gravity.START);
+
         TypedArray atts = context.obtainStyledAttributes(attrs, R.styleable.BatteryMeterView,
                 defStyle, 0);
         final int frameColor = atts.getColor(R.styleable.BatteryMeterView_frameColor,
                 context.getColor(R.color.batterymeter_frame_color));
-        mDrawable = new BatteryMeterDrawable(context, frameColor);
+        mDrawable = new BatteryMeterDrawableBase(context, frameColor);
         atts.recycle();
 
+        mSettingObserver = new SettingObserver(new Handler(context.getMainLooper()));
+
         mSlotBattery = context.getString(
                 com.android.internal.R.string.status_bar_battery);
-        setImageDrawable(mDrawable);
+        mBatteryIconView = new ImageView(context);
+        mBatteryIconView.setImageDrawable(mDrawable);
+        final MarginLayoutParams mlp = new MarginLayoutParams(
+                getResources().getDimensionPixelSize(R.dimen.status_bar_battery_icon_width),
+                getResources().getDimensionPixelSize(R.dimen.status_bar_battery_icon_height));
+        mlp.setMargins(0, 0, 0,
+                getResources().getDimensionPixelOffset(R.dimen.battery_margin_bottom));
+        addView(mBatteryIconView, mlp);
+
+        updateShowPercent();
+    }
+
+    public void forceShowPercent() {
+        mForceShowPercent = true;
+        updateShowPercent();
+    }
+
+    // StatusBarIconController reaches in here and adjusts the layout parameters of the icon
+    public ImageView getBatteryIconView() {
+        return mBatteryIconView;
     }
 
     @Override
@@ -84,9 +131,10 @@
     public void onAttachedToWindow() {
         super.onAttachedToWindow();
         mBatteryController = Dependency.get(BatteryController.class);
-        mDrawable.setBatteryController(mBatteryController);
         mBatteryController.addCallback(this);
-        mDrawable.startListening();
+        getContext().getContentResolver().registerContentObserver(
+                Settings.System.getUriFor(SHOW_PERCENT_SETTING), false, mSettingObserver);
+        updateShowPercent();
         Dependency.get(TunerService.class).addTunable(this, StatusBarIconController.ICON_BLACKLIST);
         Dependency.get(ConfigurationController.class).addCallback(this);
     }
@@ -95,13 +143,17 @@
     public void onDetachedFromWindow() {
         super.onDetachedFromWindow();
         mBatteryController.removeCallback(this);
-        mDrawable.stopListening();
+        getContext().getContentResolver().unregisterContentObserver(mSettingObserver);
         Dependency.get(TunerService.class).removeTunable(this);
         Dependency.get(ConfigurationController.class).removeCallback(this);
     }
 
     @Override
     public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
+        mDrawable.setBatteryLevel(level);
+        mDrawable.setPluggedIn(pluggedIn);
+        mLevel = level;
+        updatePercentText();
         setContentDescription(
                 getContext().getString(charging ? R.string.accessibility_battery_level_charging
                         : R.string.accessibility_battery_level, level));
@@ -109,7 +161,40 @@
 
     @Override
     public void onPowerSaveChanged(boolean isPowerSave) {
+        mDrawable.setPowerSave(isPowerSave);
+    }
 
+    private TextView loadPercentView() {
+        return (TextView) LayoutInflater.from(getContext())
+                .inflate(R.layout.battery_percentage_view, null);
+    }
+
+    private void updatePercentText() {
+        if (mBatteryPercentView != null) {
+            mBatteryPercentView.setText(
+                    NumberFormat.getPercentInstance().format(mLevel/100f));
+        }
+    }
+
+    private void updateShowPercent() {
+        final boolean showing = mBatteryPercentView != null;
+        if (0 != Settings.System.getInt(getContext().getContentResolver(),
+                BatteryMeterView.SHOW_PERCENT_SETTING, 0) || mForceShowPercent) {
+            if (!showing) {
+                mBatteryPercentView = loadPercentView();
+                if (mTextColor != 0) mBatteryPercentView.setTextColor(mTextColor);
+                updatePercentText();
+                addView(mBatteryPercentView,
+                        new ViewGroup.LayoutParams(
+                                LayoutParams.WRAP_CONTENT,
+                                LayoutParams.MATCH_PARENT));
+            }
+        } else {
+            if (showing) {
+                removeView(mBatteryPercentView);
+                mBatteryPercentView = null;
+            }
+        }
     }
 
     @Override
@@ -133,17 +218,37 @@
 
         LinearLayout.LayoutParams scaledLayoutParams = new LinearLayout.LayoutParams(
                 (int) (batteryWidth * iconScaleFactor), (int) (batteryHeight * iconScaleFactor));
-        scaledLayoutParams.setMarginsRelative(0, 0, 0, marginBottom);
+        scaledLayoutParams.setMargins(0, 0, 0, marginBottom);
 
-        setLayoutParams(scaledLayoutParams);
+        mBatteryIconView.setLayoutParams(scaledLayoutParams);
     }
 
     @Override
     public void onDarkChanged(Rect area, float darkIntensity, int tint) {
         mDrawable.setDarkIntensity(DarkIconDispatcher.isInArea(area, this) ? darkIntensity : 0);
+        setTextColor(DarkIconDispatcher.getTint(area, this, tint));
+    }
+
+    public void setTextColor(int color) {
+        mTextColor = color;
+        if (mBatteryPercentView != null) {
+            mBatteryPercentView.setTextColor(color);
+        }
     }
 
     public void setRawColors(int fgColor, int bgColor) {
         mDrawable.setColors(fgColor, bgColor);
     }
+
+    private final class SettingObserver extends ContentObserver {
+        public SettingObserver(Handler handler) {
+            super(handler);
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            super.onChange(selfChange, uri);
+            updateShowPercent();
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index 273b5e3..f1e7d53 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -25,6 +25,8 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.Preconditions;
 import com.android.systemui.assist.AssistManager;
+import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.fragments.FragmentService;
 import com.android.systemui.plugins.PluginManager;
 import com.android.systemui.statusbar.phone.ConfigurationControllerImpl;
 import com.android.systemui.statusbar.phone.DarkIconDispatcherImpl;
@@ -74,6 +76,7 @@
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.HashMap;
+import java.util.function.Consumer;
 
 /**
  * Class to handle ugly dependencies throughout sysui until we determine the
@@ -227,6 +230,9 @@
         mProviders.put(StatusBarIconController.class, () ->
                 new StatusBarIconControllerImpl(mContext));
 
+        mProviders.put(FragmentService.class, () ->
+                new FragmentService(mContext));
+
         // Put all dependencies above here so the factory can override them if it wants.
         SystemUIFactory.getInstance().injectDependencies(mProviders, mContext);
     }
@@ -282,17 +288,42 @@
         T createDependency();
     }
 
+    private <T> void destroyDependency(Class<T> cls, Consumer<T> destroy) {
+        T dep = (T) mDependencies.remove(cls);
+        if (dep != null && destroy != null) {
+            destroy.accept(dep);
+        }
+    }
+
     /**
      * Used in separate processes (like tuner settings) to init the dependencies.
      */
     public static void initDependencies(Context context) {
         if (sDependency != null) return;
         Dependency d = new Dependency();
-        d.mContext = context.getApplicationContext();
+        d.mContext = context;
         d.mComponents = new HashMap<>();
         d.start();
     }
 
+    /**
+     * Used in separate process teardown to ensure the context isn't leaked.
+     *
+     * TODO: Remove once PreferenceFragment doesn't reference getActivity()
+     * anymore and these context hacks are no longer needed.
+     */
+    public static void clearDependencies() {
+        sDependency = null;
+    }
+
+    /**
+     * Checks to see if a dependency is instantiated, if it is it removes it from
+     * the cache and calls the destroy callback.
+     */
+    public static <T> void destroy(Class<T> cls, Consumer<T> destroy) {
+        sDependency.destroyDependency(cls, destroy);
+    }
+
     public static <T> T get(Class<T> cls) {
         return sDependency.getDependency(cls);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/PluginInflateContainer.java b/packages/SystemUI/src/com/android/systemui/PluginInflateContainer.java
index 9cc6613..ddd4833 100644
--- a/packages/SystemUI/src/com/android/systemui/PluginInflateContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/PluginInflateContainer.java
@@ -53,8 +53,7 @@
 
     private static final String TAG = "PluginInflateContainer";
 
-    private String mAction;
-    private int mVersion;
+    private Class<?> mClass;
     private View mPluginView;
 
     public PluginInflateContainer(Context context, @Nullable AttributeSet attrs) {
@@ -62,28 +61,25 @@
         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PluginInflateContainer);
         String viewType = a.getString(R.styleable.PluginInflateContainer_viewType);
         try {
-            Class c = Class.forName(viewType);
-            mAction = (String) c.getDeclaredField("ACTION").get(null);
-            mVersion = (int) c.getDeclaredField("VERSION").get(null);
+            mClass = Class.forName(viewType);
         } catch (Exception e) {
             Log.d(TAG, "Problem getting class info " + viewType, e);
-            mAction = null;
-            mVersion = 0;
+            mClass = null;
         }
     }
 
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
-        if (mAction != null) {
-            Dependency.get(PluginManager.class).addPluginListener(mAction, this, mVersion);
+        if (mClass != null) {
+            Dependency.get(PluginManager.class).addPluginListener(this, mClass);
         }
     }
 
     @Override
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
-        if (mAction != null) {
+        if (mClass != null) {
             Dependency.get(PluginManager.class).removePluginListener(this);
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
index 187b557..be69867 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
@@ -36,6 +36,7 @@
 import com.android.systemui.media.RingtonePlayer;
 import com.android.systemui.pip.PipUI;
 import com.android.systemui.plugins.OverlayPlugin;
+import com.android.systemui.plugins.Plugin;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.plugins.PluginManager;
 import com.android.systemui.power.PowerUI;
@@ -67,7 +68,6 @@
      */
     private final Class<?>[] SERVICES = new Class[] {
             Dependency.class,
-            FragmentService.class,
             NotificationChannels.class,
             CommandQueue.CommandQueueStart.class,
             KeyguardViewMediator.class,
@@ -207,7 +207,7 @@
                 mServices[i].onBootCompleted();
             }
         }
-        Dependency.get(PluginManager.class).addPluginListener(OverlayPlugin.ACTION,
+        Dependency.get(PluginManager.class).addPluginListener(
                 new PluginListener<OverlayPlugin>() {
                     private ArraySet<OverlayPlugin> mOverlays;
 
@@ -236,7 +236,7 @@
                         Dependency.get(StatusBarWindowManager.class).setForcePluginOpen(
                                 mOverlays.size() != 0);
                     }
-                }, OverlayPlugin.VERSION, true /* Allow multiple plugins */);
+                }, OverlayPlugin.class, true /* Allow multiple plugins */);
 
         mServicesStarted = true;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
index 94dc9a3..6186df1 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
@@ -49,7 +49,7 @@
         }
 
         DozeProvider provider = Dependency.get(PluginManager.class)
-                .getOneShotPlugin(DozeProvider.ACTION, DozeProvider.VERSION);
+                .getOneShotPlugin(DozeProvider.class);
         mDozeMachine = new DozeFactory(provider).assembleMachine(this);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/fragments/FragmentHostManager.java b/packages/SystemUI/src/com/android/systemui/fragments/FragmentHostManager.java
index 0c6bf52..57c75bf 100644
--- a/packages/SystemUI/src/com/android/systemui/fragments/FragmentHostManager.java
+++ b/packages/SystemUI/src/com/android/systemui/fragments/FragmentHostManager.java
@@ -32,6 +32,7 @@
 import android.view.View;
 
 import com.android.settingslib.applications.InterestingConfigChanges;
+import com.android.systemui.Dependency;
 import com.android.systemui.SystemUIApplication;
 import com.android.systemui.plugins.Plugin;
 import com.android.systemui.plugins.PluginManager;
@@ -171,6 +172,10 @@
         return mPlugins;
     }
 
+    void destroy() {
+        mFragments.dispatchDestroy();
+    }
+
     public interface FragmentListener {
         void onFragmentViewCreated(String tag, Fragment fragment);
 
@@ -182,8 +187,7 @@
 
     public static FragmentHostManager get(View view) {
         try {
-            return ((SystemUIApplication) view.getContext().getApplicationContext())
-                    .getComponent(FragmentService.class).getFragmentHostManager(view);
+            return Dependency.get(FragmentService.class).getFragmentHostManager(view);
         } catch (ClassCastException e) {
             // TODO: Some auto handling here?
             throw e;
diff --git a/packages/SystemUI/src/com/android/systemui/fragments/FragmentService.java b/packages/SystemUI/src/com/android/systemui/fragments/FragmentService.java
index 85cde10..9a8512d 100644
--- a/packages/SystemUI/src/com/android/systemui/fragments/FragmentService.java
+++ b/packages/SystemUI/src/com/android/systemui/fragments/FragmentService.java
@@ -14,6 +14,7 @@
 
 package com.android.systemui.fragments;
 
+import android.content.Context;
 import android.content.res.Configuration;
 import android.os.Bundle;
 import android.os.Handler;
@@ -21,6 +22,7 @@
 import android.util.Log;
 import android.view.View;
 
+import com.android.systemui.ConfigurationChangedReceiver;
 import com.android.systemui.SystemUI;
 import com.android.systemui.SystemUIApplication;
 
@@ -28,16 +30,16 @@
  * Holds a map of root views to FragmentHostStates and generates them as needed.
  * Also dispatches the configuration changes to all current FragmentHostStates.
  */
-public class FragmentService extends SystemUI {
+public class FragmentService implements ConfigurationChangedReceiver {
 
     private static final String TAG = "FragmentService";
 
     private final ArrayMap<View, FragmentHostState> mHosts = new ArrayMap<>();
     private final Handler mHandler = new Handler();
+    private final Context mContext;
 
-    @Override
-    public void start() {
-        putComponent(FragmentService.class, this);
+    public FragmentService(Context context) {
+        mContext = context;
     }
 
     public FragmentHostManager getFragmentHostManager(View view) {
@@ -50,8 +52,14 @@
         return state.getFragmentHostManager();
     }
 
+    public void destroyAll() {
+        for (FragmentHostState state : mHosts.values()) {
+            state.mFragmentHostManager.destroy();
+        }
+    }
+
     @Override
-    protected void onConfigurationChanged(Configuration newConfig) {
+    public void onConfigurationChanged(Configuration newConfig) {
         for (FragmentHostState state : mHosts.values()) {
             state.sendConfigurationChange(newConfig);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/fragments/PluginFragmentListener.java b/packages/SystemUI/src/com/android/systemui/fragments/PluginFragmentListener.java
index 1eaca6f..03bb73d 100644
--- a/packages/SystemUI/src/com/android/systemui/fragments/PluginFragmentListener.java
+++ b/packages/SystemUI/src/com/android/systemui/fragments/PluginFragmentListener.java
@@ -44,8 +44,9 @@
         mDefaultClass = defaultFragment;
     }
 
-    public void startListening(String action, int version) {
-        mPluginManager.addPluginListener(action, this, version, false /* Only allow one */);
+    public void startListening() {
+        mPluginManager.addPluginListener(this, mExpectedInterface,
+                false /* Only allow one */);
     }
 
     public void stopListening() {
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java b/packages/SystemUI/src/com/android/systemui/plugins/PluginInstanceManager.java
similarity index 91%
rename from packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java
rename to packages/SystemUI/src/com/android/systemui/plugins/PluginInstanceManager.java
index dd1614b..e895fa2 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java
+++ b/packages/SystemUI/src/com/android/systemui/plugins/PluginInstanceManager.java
@@ -18,12 +18,10 @@
 import android.app.Notification.Action;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.content.Intent;
-import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
@@ -40,6 +38,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+import com.android.systemui.plugins.VersionInfo.InvalidVersionException;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -55,7 +54,7 @@
     private final PluginListener<T> mListener;
     private final String mAction;
     private final boolean mAllowMultiple;
-    private final int mVersion;
+    private final VersionInfo mVersion;
 
     @VisibleForTesting
     final MainHandler mMainHandler;
@@ -66,14 +65,14 @@
     private final PluginManager mManager;
 
     PluginInstanceManager(Context context, String action, PluginListener<T> listener,
-            boolean allowMultiple, Looper looper, int version, PluginManager manager) {
+            boolean allowMultiple, Looper looper, VersionInfo version, PluginManager manager) {
         this(context, context.getPackageManager(), action, listener, allowMultiple, looper, version,
                 manager, Build.IS_DEBUGGABLE);
     }
 
     @VisibleForTesting
     PluginInstanceManager(Context context, PackageManager pm, String action,
-            PluginListener<T> listener, boolean allowMultiple, Looper looper, int version,
+            PluginListener<T> listener, boolean allowMultiple, Looper looper, VersionInfo version,
             PluginManager manager, boolean debuggable) {
         mMainHandler = new MainHandler(Looper.getMainLooper());
         mPluginHandler = new PluginHandler(looper);
@@ -301,8 +300,14 @@
                 Context pluginContext = new PluginContextWrapper(
                         mContext.createApplicationContext(info, 0), classLoader);
                 Class<?> pluginClass = Class.forName(cls, true, classLoader);
+                // TODO: Only create the plugin before version check if we need it for
+                // legacy version check.
                 T plugin = (T) pluginClass.newInstance();
-                if (plugin.getVersion() != mVersion) {
+                try {
+                    checkVersion(pluginClass, plugin, mVersion);
+                    if (DEBUG) Log.d(TAG, "createPlugin");
+                    return new PluginInfo(pkg, cls, plugin, pluginContext);
+                } catch (InvalidVersionException e) {
                     final int icon = mContext.getResources().getIdentifier("tuner", "drawable",
                             mContext.getPackageName());
                     final int color = Resources.getSystem().getIdentifier(
@@ -318,20 +323,18 @@
                     String label = cls;
                     try {
                         label = mPm.getServiceInfo(component, 0).loadLabel(mPm).toString();
-                    } catch (NameNotFoundException e) {
+                    } catch (NameNotFoundException e2) {
                     }
-                    if (plugin.getVersion() < mVersion) {
+                    if (!e.isTooNew()) {
                         // Localization not required as this will never ever appear in a user build.
                         nb.setContentTitle("Plugin \"" + label + "\" is too old")
                                 .setContentText("Contact plugin developer to get an updated"
-                                        + " version.\nPlugin version: " + plugin.getVersion()
-                                        + "\nSystem version: " + mVersion);
+                                        + " version.\n" + e.getMessage());
                     } else {
                         // Localization not required as this will never ever appear in a user build.
                         nb.setContentTitle("Plugin \"" + label + "\" is too new")
                                 .setContentText("Check to see if an OTA is available.\n"
-                                        + "Plugin version: " + plugin.getVersion()
-                                        + "\nSystem version: " + mVersion);
+                                        + e.getMessage());
                     }
                     Intent i = new Intent(PluginManager.DISABLE_PLUGIN).setData(
                             Uri.parse("package://" + component.flattenToString()));
@@ -345,13 +348,24 @@
                             + ", expected " + mVersion);
                     return null;
                 }
-                if (DEBUG) Log.d(TAG, "createPlugin");
-                return new PluginInfo(pkg, cls, plugin, pluginContext);
             } catch (Exception e) {
                 Log.w(TAG, "Couldn't load plugin: " + pkg, e);
                 return null;
             }
         }
+
+        private void checkVersion(Class<?> pluginClass, T plugin, VersionInfo version)
+                throws InvalidVersionException {
+            VersionInfo pv = new VersionInfo().addClass(pluginClass);
+            if (pv.hasVersionInfo()) {
+                version.checkVersion(pv);
+            } else {
+                int fallbackVersion = plugin.getVersion();
+                if (fallbackVersion != version.getDefaultVersion()) {
+                    throw new InvalidVersionException("Invalid legacy version", false);
+                }
+            }
+        }
     }
 
     public static class PluginContextWrapper extends ContextWrapper {
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java b/packages/SystemUI/src/com/android/systemui/plugins/PluginManager.java
similarity index 89%
rename from packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java
rename to packages/SystemUI/src/com/android/systemui/plugins/PluginManager.java
index cef485e..8b4bd7b 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java
+++ b/packages/SystemUI/src/com/android/systemui/plugins/PluginManager.java
@@ -33,6 +33,7 @@
 import android.os.Looper;
 import android.os.SystemProperties;
 import android.os.UserHandle;
+import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 
@@ -40,6 +41,7 @@
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
 import com.android.systemui.plugins.PluginInstanceManager.PluginContextWrapper;
 import com.android.systemui.plugins.PluginInstanceManager.PluginInfo;
+import com.android.systemui.plugins.annotations.ProvidesInterface;
 
 import dalvik.system.PathClassLoader;
 
@@ -93,7 +95,18 @@
         Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler);
     }
 
-    public <T extends Plugin> T getOneShotPlugin(String action, int version) {
+    public <T extends Plugin> T getOneShotPlugin(Class<T> cls) {
+        ProvidesInterface info = cls.getDeclaredAnnotation(ProvidesInterface.class);
+        if (info == null) {
+            throw new RuntimeException(cls + " doesn't provide an interface");
+        }
+        if (TextUtils.isEmpty(info.action())) {
+            throw new RuntimeException(cls + " doesn't provide an action");
+        }
+        return getOneShotPlugin(info.action(), cls);
+    }
+
+    public <T extends Plugin> T getOneShotPlugin(String action, Class<?> cls) {
         if (!isDebuggable) {
             // Never ever ever allow these on production builds, they are only for prototyping.
             return null;
@@ -102,7 +115,7 @@
             throw new RuntimeException("Must be called from UI thread");
         }
         PluginInstanceManager<T> p = mFactory.createPluginInstanceManager(mContext, action, null,
-                false, mBackgroundThread.getLooper(), version, this);
+                false, mBackgroundThread.getLooper(), cls, this);
         mPluginPrefs.addAction(action);
         PluginInfo<T> info = p.getPlugin();
         if (info != null) {
@@ -114,20 +127,36 @@
         return null;
     }
 
-    public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
-            int version) {
-        addPluginListener(action, listener, version, false);
+    public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls) {
+        addPluginListener(listener, cls, false);
+    }
+
+    public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls,
+            boolean allowMultiple) {
+        ProvidesInterface info = cls.getDeclaredAnnotation(ProvidesInterface.class);
+        if (info == null) {
+            throw new RuntimeException(cls + " doesn't provide an interface");
+        }
+        if (TextUtils.isEmpty(info.action())) {
+            throw new RuntimeException(cls + " doesn't provide an action");
+        }
+        addPluginListener(info.action(), listener, cls, allowMultiple);
     }
 
     public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
-            int version, boolean allowMultiple) {
+            Class<?> cls) {
+        addPluginListener(action, listener, cls, false);
+    }
+
+    public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
+            Class cls, boolean allowMultiple) {
         if (!isDebuggable) {
             // Never ever ever allow these on production builds, they are only for prototyping.
             return;
         }
         mPluginPrefs.addAction(action);
         PluginInstanceManager p = mFactory.createPluginInstanceManager(mContext, action, listener,
-                allowMultiple, mBackgroundThread.getLooper(), version, this);
+                allowMultiple, mBackgroundThread.getLooper(), cls, this);
         p.loadAll();
         mPluginMap.put(listener, p);
         startListening();
@@ -282,9 +311,9 @@
     public static class PluginInstanceManagerFactory {
         public <T extends Plugin> PluginInstanceManager createPluginInstanceManager(Context context,
                 String action, PluginListener<T> listener, boolean allowMultiple, Looper looper,
-                int version, PluginManager manager) {
+                Class<?> cls, PluginManager manager) {
             return new PluginInstanceManager(context, action, listener, allowMultiple, looper,
-                    version, manager);
+                    new VersionInfo().addClass(cls), manager);
         }
     }
 
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginPrefs.java b/packages/SystemUI/src/com/android/systemui/plugins/PluginPrefs.java
similarity index 100%
rename from packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginPrefs.java
rename to packages/SystemUI/src/com/android/systemui/plugins/PluginPrefs.java
diff --git a/packages/SystemUI/src/com/android/systemui/plugins/VersionInfo.java b/packages/SystemUI/src/com/android/systemui/plugins/VersionInfo.java
new file mode 100644
index 0000000..84f7761
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/plugins/VersionInfo.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2017 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.plugins;
+
+import com.android.systemui.plugins.annotations.Dependencies;
+import com.android.systemui.plugins.annotations.DependsOn;
+import com.android.systemui.plugins.annotations.ProvidesInterface;
+import com.android.systemui.plugins.annotations.Requirements;
+import com.android.systemui.plugins.annotations.Requires;
+
+import android.util.ArrayMap;
+
+public class VersionInfo {
+
+    private final ArrayMap<Class<?>, Version> mVersions = new ArrayMap<>();
+    private Class<?> mDefault;
+
+    public boolean hasVersionInfo() {
+        return !mVersions.isEmpty();
+    }
+
+    public int getDefaultVersion() {
+        return mVersions.get(mDefault).mVersion;
+    }
+
+    public VersionInfo addClass(Class<?> cls) {
+        if (mDefault == null) {
+            // The legacy default version is from the first class we add.
+            mDefault = cls;
+        }
+        addClass(cls, false);
+        return this;
+    }
+
+    private void addClass(Class<?> cls, boolean required) {
+        ProvidesInterface provider = cls.getDeclaredAnnotation(ProvidesInterface.class);
+        if (provider != null) {
+            mVersions.put(cls, new Version(provider.version(), true));
+        }
+        Requires requires = cls.getDeclaredAnnotation(Requires.class);
+        if (requires != null) {
+            mVersions.put(requires.target(), new Version(requires.version(), required));
+        }
+        Requirements requirements = cls.getDeclaredAnnotation(Requirements.class);
+        if (requirements != null) {
+            for (Requires r : requirements.value()) {
+                mVersions.put(r.target(), new Version(r.version(), required));
+            }
+        }
+        DependsOn depends = cls.getDeclaredAnnotation(DependsOn.class);
+        if (depends != null) {
+            addClass(depends.target(), true);
+        }
+        Dependencies dependencies = cls.getDeclaredAnnotation(Dependencies.class);
+        if (dependencies != null) {
+            for (DependsOn d : dependencies.value()) {
+                addClass(d.target(), true);
+            }
+        }
+    }
+
+    public void checkVersion(VersionInfo plugin) throws InvalidVersionException {
+        ArrayMap<Class<?>, Version> versions = new ArrayMap<>(mVersions);
+        plugin.mVersions.forEach((aClass, version) -> {
+            Version v = versions.remove(aClass);
+            if (v == null) {
+                v = createVersion(aClass);
+            }
+            if (v == null) {
+                throw new InvalidVersionException(aClass.getSimpleName()
+                        + " does not provide an interface", false);
+            }
+            if (v.mVersion != version.mVersion) {
+                throw new InvalidVersionException(aClass, v.mVersion < version.mVersion, v.mVersion,
+                        version.mVersion);
+            }
+        });
+        versions.forEach((aClass, version) -> {
+            if (version.mRequired) {
+                throw new InvalidVersionException("Missing required dependency "
+                        + aClass.getSimpleName(), false);
+            }
+        });
+    }
+
+    private Version createVersion(Class<?> cls) {
+        ProvidesInterface provider = cls.getDeclaredAnnotation(ProvidesInterface.class);
+        if (provider != null) {
+            return new Version(provider.version(), false);
+        }
+        return null;
+    }
+
+    public static class InvalidVersionException extends RuntimeException {
+        private final boolean mTooNew;
+
+        public InvalidVersionException(String str, boolean tooNew) {
+            super(str);
+            mTooNew = tooNew;
+        }
+
+        public InvalidVersionException(Class<?> cls, boolean tooNew, int expected, int actual) {
+            super(cls.getSimpleName() + " expected version " + expected + " but had " + actual);
+            mTooNew = tooNew;
+        }
+
+        public boolean isTooNew() {
+            return mTooNew;
+        }
+    }
+
+    private static class Version {
+
+        private final int mVersion;
+        private final boolean mRequired;
+
+        public Version(int version, boolean required) {
+            mVersion = version;
+            mRequired = required;
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
index a231e79..3559257 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
@@ -242,7 +242,7 @@
                 maxHeight = height;
             }
         }
-        setMeasuredDimension(getMeasuredWidth(), maxHeight);
+        setMeasuredDimension(getMeasuredWidth(), maxHeight + getPaddingBottom());
     }
 
     private final Runnable mDistribute = new Runnable() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
index c85f83b..e0d1cba 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
@@ -160,7 +160,9 @@
         mAllViews.clear();
         mTopFiveQs.clear();
 
-        mAllViews.add((View) mQsPanel.getTileLayout());
+        QSTileLayout tileLayout = mQsPanel.getTileLayout();
+        mAllViews.add((View) tileLayout);
+        firstPageBuilder.addFloat(tileLayout, "translationY", mQsPanel.getHeight(), 0);
 
         for (QSTile<?> tile : tiles) {
             QSTileBaseView tileView = mQsPanel.getTileView(tile);
@@ -168,7 +170,6 @@
                 Log.e(TAG, "tileView is null " + tile.getTileSpec());
                 continue;
             }
-            final TextView label = ((QSTileView) tileView).getLabel();
             final View tileIcon = tileView.getIcon().getIconView();
             View view = mQs.getView();
             if (count < mNumQuickTiles && mAllowFancy) {
@@ -187,12 +188,12 @@
 
                 // Counteract the parent translation on the tile. So we have a static base to
                 // animate the label position off from.
-                firstPageBuilder.addFloat(tileView, "translationY", mQsPanel.getHeight(), 0);
+                //firstPageBuilder.addFloat(tileView, "translationY", mQsPanel.getHeight(), 0);
 
-                // Move the real tile's label from the quick tile position to its final
+                // Move the real tile from the quick tile position to its final
                 // location.
-                translationXBuilder.addFloat(label, "translationX", -xDiff, 0);
-                translationYBuilder.addFloat(label, "translationY", -yDiff, 0);
+                translationXBuilder.addFloat(tileView, "translationX", -xDiff, 0);
+                translationYBuilder.addFloat(tileView, "translationY", -yDiff, 0);
 
                 mTopFiveQs.add(tileView.getIcon());
                 mAllViews.add(tileView.getIcon());
@@ -209,22 +210,22 @@
 
                 firstPageBuilder.addFloat(tileView, "translationY", mQsPanel.getHeight(), 0);
                 translationXBuilder.addFloat(tileView, "translationX", -xDiff, 0);
-                translationYBuilder.addFloat(label, "translationY", -yDiff, 0);
+                translationYBuilder.addFloat(tileView, "translationY", -yDiff, 0);
                 translationYBuilder.addFloat(tileIcon, "translationY", -yDiff, 0);
 
                 mAllViews.add(tileIcon);
             } else {
                 firstPageBuilder.addFloat(tileView, "alpha", 0, 1);
+                firstPageBuilder.addFloat(tileView, "translationY", -mQsPanel.getHeight(), 0);
             }
             mAllViews.add(tileView);
-            mAllViews.add(label);
             count++;
         }
         if (mAllowFancy) {
             // Make brightness appear static position and alpha in through second half.
             View brightness = mQsPanel.getBrightnessView();
             if (brightness != null) {
-//                firstPageBuilder.addFloat(brightness, "translationY", mQsPanel.getHeight(), 0);
+                firstPageBuilder.addFloat(brightness, "translationY", mQsPanel.getHeight(), 0);
                 mBrightnessAnimator = new TouchAnimator.Builder()
                         .addFloat(brightness, "alpha", 0, 1)
                         .addFloat(mQsPanel.getPageIndicator(), "alpha", 0, 1)
@@ -240,7 +241,7 @@
             // Fade in the tiles/labels as we reach the final position.
             mFirstPageDelayedAnimator = new TouchAnimator.Builder()
                     .setStartDelay(EXPANDED_TILE_DELAY)
-                    .addFloat(mQsPanel.getTileLayout(), "alpha", 0, 1)
+                    .addFloat(tileLayout, "alpha", 0, 1)
                     .addFloat(mQsPanel.getFooter().getView(), "alpha", 0, 1).build();
             mAllViews.add(mQsPanel.getFooter().getView());
             float px = 0;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 504678c..e8d5ece 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -86,6 +86,10 @@
 
         setOrientation(VERTICAL);
 
+        mBrightnessView = LayoutInflater.from(context).inflate(
+                R.layout.quick_settings_brightness_dialog, this, false);
+        addView(mBrightnessView);
+
         setupTileLayout();
 
         mFooter = new QSFooter(this, context);
@@ -100,10 +104,6 @@
 
         updateResources();
 
-        mBrightnessView = LayoutInflater.from(context).inflate(
-                R.layout.quick_settings_brightness_dialog, this, false);
-        addView(mBrightnessView);
-
         mBrightnessController = new BrightnessController(getContext(),
                 (ImageView) findViewById(R.id.brightness_icon),
                 (ToggleSliderView) findViewById(R.id.brightness_slider));
@@ -441,8 +441,15 @@
         }
         r.tile.setDetailListening(show);
         int x = r.tileView.getLeft() + r.tileView.getWidth() / 2;
-        int y = r.tileView.getTop() + mTileLayout.getOffsetTop(r) + r.tileView.getHeight() / 2
-                + getTop();
+        int y;
+        if (r.tileView instanceof QSTileView) {
+            View labelContainer = (View) ((QSTileView) r.tileView).getLabel().getParent();
+            y = r.tileView.getTop() + mTileLayout.getOffsetTop(r) + getTop()
+                    + labelContainer.getTop() + labelContainer.getHeight() / 2;
+        } else {
+            y = r.tileView.getTop() + mTileLayout.getOffsetTop(r) + r.tileView.getHeight() / 2
+                    + getTop();
+        }
         handleShowDetailImpl(r, show, x, y);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileBaseView.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileBaseView.java
index 0e04d0a..c750fdc 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTileBaseView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileBaseView.java
@@ -15,38 +15,30 @@
  */
 package com.android.systemui.qs;
 
-import android.animation.ValueAnimator;
 import android.content.Context;
-import android.content.res.ColorStateList;
 import android.content.res.TypedArray;
-import android.graphics.Color;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.RippleDrawable;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
-import android.service.quicksettings.Tile;
 import android.text.TextUtils;
-import android.util.Log;
 import android.view.Gravity;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.ImageView.ScaleType;
 import android.widget.LinearLayout;
 import android.widget.Switch;
 
-import com.android.settingslib.Utils;
-
 import com.android.systemui.R;
 
 public class QSTileBaseView extends LinearLayout {
 
     private static final String TAG = "QSTileBaseView";
     private final H mHandler = new H();
+    private final FrameLayout mIconFrame;
     protected QSIconView mIcon;
     protected RippleDrawable mRipple;
     private Drawable mTileBackground;
@@ -63,15 +55,15 @@
         // Default to Quick Tile padding, and QSTileView will specify its own padding.
         int padding = context.getResources().getDimensionPixelSize(R.dimen.qs_quick_tile_padding);
 
-        FrameLayout frame = new FrameLayout(context);
-        frame.setForegroundGravity(Gravity.CENTER);
+        mIconFrame = new FrameLayout(context);
+        mIconFrame.setForegroundGravity(Gravity.CENTER);
         int size = context.getResources().getDimensionPixelSize(R.dimen.qs_quick_tile_size);
-        addView(frame, new LayoutParams(size, size));
+        addView(mIconFrame, new LayoutParams(size, size));
         mIcon = icon;
         FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
                 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
         params.setMargins(0, padding, 0, padding);
-        frame.addView(mIcon, params);
+        mIconFrame.addView(mIcon, params);
 
         mTileBackground = newTileBackground();
         if (mTileBackground instanceof RippleDrawable) {
@@ -105,7 +97,7 @@
     private void updateRippleSize(int width, int height) {
         // center the touch feedback on the center of the icon, and dial it down a bit
         final int cx = width / 2;
-        final int cy = height / 2;
+        final int cy = mIconFrame.getMeasuredHeight() / 2;
         final int rad = (int) (mIcon.getHeight() * .85f);
         mRipple.setHotspotBounds(cx - rad, cy - rad, cx + rad, cy + rad);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileView.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileView.java
index 232941d..428fe9b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTileView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileView.java
@@ -20,6 +20,7 @@
 
 import android.content.Context;
 import android.content.res.Configuration;
+import android.graphics.drawable.RippleDrawable;
 import android.service.quicksettings.Tile;
 import android.text.SpannableStringBuilder;
 import android.text.style.ForegroundColorSpan;
@@ -28,6 +29,7 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
 import android.widget.ImageView;
 import android.widget.TextView;
 
@@ -43,8 +45,7 @@
     protected TextView mLabel;
     private ImageView mPadLock;
     private int mState;
-    private OnClickListener mClick;
-    private OnClickListener mSecondaryClick;
+    private ViewGroup mLabelContainer;
 
     public QSTileView(Context context, QSIconView icon) {
         this(context, icon, false);
@@ -76,13 +77,15 @@
     }
 
     protected void createLabel() {
-        ViewGroup view = (ViewGroup) LayoutInflater.from(getContext())
-                .inflate(R.layout.qs_tile_label, null);
-        view.setClipChildren(false);
-        view.setClipToPadding(false);
-        mLabel = (TextView) view.findViewById(R.id.tile_label);
-        mPadLock = (ImageView) view.findViewById(R.id.restricted_padlock);
-        addView(view);
+        mLabelContainer = (ViewGroup) LayoutInflater.from(getContext())
+                .inflate(R.layout.qs_tile_label, this, false);
+        mLabelContainer.setClipChildren(false);
+        mLabelContainer.setClipToPadding(false);
+        mLabel = (TextView) mLabelContainer.findViewById(R.id.tile_label);
+        mPadLock = (ImageView) mLabelContainer.findViewById(R.id.restricted_padlock);
+
+        mLabelContainer.setBackground(newTileBackground());
+        addView(mLabelContainer);
     }
 
     @Override
@@ -104,17 +107,10 @@
     }
 
     @Override
-    public void init(OnClickListener click, OnClickListener secondaryClick, OnLongClickListener longClick) {
-        mClick = click;
-        mSecondaryClick = secondaryClick;
+    public void init(OnClickListener click, OnClickListener secondaryClick,
+            OnLongClickListener longClick) {
         super.init(click, secondaryClick, longClick);
-    }
-
-    @Override
-    public boolean onTouchEvent(MotionEvent event) {
-        if (event.getActionMasked() == MotionEvent.ACTION_UP)  {
-            setOnClickListener(event.getY() < (getMeasuredHeight() / 2) ? mClick : mSecondaryClick);
-        }
-        return super.onTouchEvent(event);
+        mLabelContainer.setClickable(true);
+        mLabelContainer.setOnClickListener(secondaryClick);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java
index f2c3e61..4e30797 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java
@@ -301,6 +301,7 @@
         if (position == mEditIndex) position--;
 
         move(mAccessibilityFromIndex, position, v);
+
         notifyDataSetChanged();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
index ce72942..ec4ca7a6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
@@ -102,12 +102,7 @@
             mTokenMap.remove(service.getToken());
             mTiles.remove(tile.getComponent());
             final String slot = tile.getComponent().getClassName();
-            mMainHandler.post(new Runnable() {
-                @Override
-                public void run() {
-                    mHost.getIconController().removeIcon(slot);
-                }
-            });
+            mMainHandler.post(() -> mHost.getIconController().removeIcon(slot));
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BatteryTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BatteryTile.java
index fff8305..f7bfc1e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BatteryTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BatteryTile.java
@@ -35,8 +35,8 @@
 import android.widget.TextView;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.settingslib.BatteryInfo;
+import com.android.settingslib.graph.BatteryMeterDrawableBase;
 import com.android.settingslib.graph.UsageView;
-import com.android.systemui.BatteryMeterDrawable;
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
 import com.android.systemui.plugins.qs.QS.DetailAdapter;
@@ -104,11 +104,6 @@
     }
 
     @Override
-    protected void handleSecondaryClick() {
-        showDetail(true);
-    }
-
-    @Override
     public CharSequence getTileLabel() {
         return mContext.getString(R.string.battery);
     }
@@ -118,7 +113,6 @@
         int level = (arg != null) ? (Integer) arg : mLevel;
         String percentage = NumberFormat.getPercentInstance().format((double) level / 100.0);
 
-        state.dualTarget = true;
         state.state = mCharging ? Tile.STATE_UNAVAILABLE
                 : mPowerSave ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
         state.icon = ResourceIcon.get(R.drawable.ic_qs_battery_saver);
@@ -155,8 +149,10 @@
 
     private final class BatteryDetail implements DetailAdapter, OnClickListener,
             OnAttachStateChangeListener {
-        private final BatteryMeterDrawable mDrawable = new BatteryMeterDrawable(mHost.getContext(),
-                mHost.getContext().getColor(R.color.batterymeter_frame_color));
+        private final BatteryMeterDrawableBase mDrawable
+                = new BatteryMeterDrawableBase(
+                        mHost.getContext(),
+                        mHost.getContext().getColor(R.color.batterymeter_frame_color));
         private View mCurrentView;
 
         @Override
@@ -195,8 +191,9 @@
             if (mCurrentView == null) {
                 return;
             }
-            mDrawable.onBatteryLevelChanged(100, false, false);
-            mDrawable.onPowerSaveChanged(true);
+            mDrawable.setBatteryLevel(100);
+            mDrawable.setPluggedIn(false);
+            mDrawable.setPowerSave(true);
             mDrawable.setShowPercent(false);
             ((ImageView) mCurrentView.findViewById(android.R.id.icon)).setImageDrawable(mDrawable);
             Checkable checkbox = (Checkable) mCurrentView.findViewById(android.R.id.toggle);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
index b48b829..3c28f76 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
@@ -123,7 +123,6 @@
 
     @Override
     protected void handleUpdateState(BooleanState state, Object arg) {
-        state.dualTarget = true;
         state.label = mContext.getString(R.string.quick_settings_cast_title);
         state.contentDescription = state.label;
         state.value = false;
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
index 55491b2..8de4e58 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
@@ -130,6 +130,7 @@
                 launchOpts.numVisibleTaskThumbnails = 2;
                 launchOpts.onlyLoadForCache = true;
                 launchOpts.onlyLoadPausedActivities = true;
+                launchOpts.loadThumbnails = !ActivityManager.ENABLE_TASK_SNAPSHOTS;
                 loader.loadTasks(mContext, plan, launchOpts);
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewThumbnail.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewThumbnail.java
index 4ac0f9e..02b0113 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewThumbnail.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewThumbnail.java
@@ -77,10 +77,11 @@
     @ViewDebug.ExportedProperty(category="recents")
     private float mDimAlpha;
     private Matrix mMatrix = new Matrix();
-    protected Paint mDrawPaint = new Paint();
-    private Paint mLockedPaint = new Paint();
+    private Paint mDrawPaint = new Paint();
+    protected Paint mLockedPaint = new Paint();
     protected Paint mBgFillPaint = new Paint();
     protected BitmapShader mBitmapShader;
+    protected boolean mUserLocked = false;
     private LightingColorFilter mLightingColorFilter = new LightingColorFilter(0xffffffff, 0);
 
     // Clip the top of the thumbnail against the opaque header bar that overlaps this view
@@ -152,7 +153,7 @@
         int thumbnailHeight = Math.min(viewHeight,
                 (int) (mThumbnailRect.height() * mThumbnailScale));
 
-        if (mTask != null && mTask.isLocked) {
+        if (mUserLocked) {
             canvas.drawRoundRect(0, 0, viewWidth, viewHeight, mCornerRadius, mCornerRadius,
                     mLockedPaint);
         } else if (mBitmapShader != null && thumbnailWidth > 0 && thumbnailHeight > 0) {
@@ -344,6 +345,17 @@
     }
 
     /**
+     * Returns the {@link Paint} used to draw a task screenshot, or {@link #mLockedPaint} if the
+     * thumbnail shouldn't be drawn because it belongs to a locked user.
+     */
+    protected Paint getDrawPaint() {
+        if (mUserLocked) {
+            return mLockedPaint;
+        }
+        return mDrawPaint;
+    }
+
+    /**
      * Binds the thumbnail view to the task.
      */
     void bindToTask(Task t, boolean disabledInSafeMode, int displayOrientation, Rect displayRect) {
@@ -354,7 +366,10 @@
         if (t.colorBackground != 0) {
             mBgFillPaint.setColor(t.colorBackground);
         }
-        mLockedPaint.setColor(t.colorPrimary);
+        if (t.colorPrimary != 0) {
+            mLockedPaint.setColor(t.colorPrimary);
+        }
+        mUserLocked = t.isLocked;
         EventBus.getDefault().register(this);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/grid/GridTaskViewThumbnail.java b/packages/SystemUI/src/com/android/systemui/recents/views/grid/GridTaskViewThumbnail.java
index 2c3e42b..bcf4f17 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/grid/GridTaskViewThumbnail.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/grid/GridTaskViewThumbnail.java
@@ -28,7 +28,6 @@
 
     private Path mThumbnailOutline;
     private Path mRestBackgroundOutline;
-    private Path mFullBackgroundOutline;
     // True if either this view's size or thumbnail scale has changed and mThumbnailOutline should
     // be updated.
     private boolean mUpdateThumbnailOutline = true;
@@ -145,10 +144,7 @@
                         90, 90, false); // F
                 mRestBackgroundOutline.lineTo(l, t); // A
                 mRestBackgroundOutline.close();
-
             }
-        } else {
-            mFullBackgroundOutline = mThumbnailOutline;
         }
     }
 
@@ -167,7 +163,10 @@
             updateThumbnailOutline();
             mUpdateThumbnailOutline = false;
         }
-        if (mBitmapShader != null && thumbnailWidth > 0 && thumbnailHeight > 0) {
+
+        if (mUserLocked) {
+            canvas.drawPath(mThumbnailOutline, mLockedPaint);
+        } else if (mBitmapShader != null && thumbnailWidth > 0 && thumbnailHeight > 0) {
             // Draw the background, there will be some small overdraw with the thumbnail
             if (thumbnailWidth < viewWidth) {
                 // Portrait thumbnail on a landscape task view
@@ -177,9 +176,9 @@
                 // Landscape thumbnail on a portrait task view
                 canvas.drawPath(mRestBackgroundOutline, mBgFillPaint);
             }
-            canvas.drawPath(mThumbnailOutline, mDrawPaint);
+            canvas.drawPath(mThumbnailOutline, getDrawPaint());
         } else {
-            canvas.drawPath(mFullBackgroundOutline, mBgFillPaint);
+            canvas.drawPath(mThumbnailOutline, mBgFillPaint);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 5366da1..995901b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -164,15 +164,19 @@
         }
     }
 
-    public void disable(int state1, int state2) {
+    public void disable(int state1, int state2, boolean animate) {
         synchronized (mLock) {
             mDisable1 = state1;
             mDisable2 = state2;
             mHandler.removeMessages(MSG_DISABLE);
-            mHandler.obtainMessage(MSG_DISABLE, state1, state2, null).sendToTarget();
+            mHandler.obtainMessage(MSG_DISABLE, state1, state2, animate).sendToTarget();
         }
     }
 
+    public void disable(int state1, int state2) {
+        disable(state1, state2, true);
+    }
+
     public void animateExpandNotificationsPanel() {
         synchronized (mLock) {
             mHandler.removeMessages(MSG_EXPAND_NOTIFICATIONS);
@@ -433,7 +437,7 @@
                 }
                 case MSG_DISABLE:
                     for (int i = 0; i < mCallbacks.size(); i++) {
-                        mCallbacks.get(i).disable(msg.arg1, msg.arg2, true /* animate */);
+                        mCallbacks.get(i).disable(msg.arg1, msg.arg2, (Boolean) msg.obj);
                     }
                     break;
                 case MSG_EXPAND_NOTIFICATIONS:
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
index 2e9c7fd..3648a06 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
@@ -22,7 +22,6 @@
 import android.animation.ValueAnimator.AnimatorUpdateListener;
 import android.annotation.Nullable;
 import android.content.Context;
-import android.content.res.ColorStateList;
 import android.graphics.drawable.AnimatedVectorDrawable;
 import android.graphics.drawable.AnimationDrawable;
 import android.graphics.drawable.ColorDrawable;
@@ -76,6 +75,7 @@
     private int mNotificationMinHeightLegacy;
     private int mMaxHeadsUpHeightLegacy;
     private int mMaxHeadsUpHeight;
+    private int mMaxHeadsUpHeightIncreased;
     private int mNotificationMinHeight;
     private int mNotificationMinHeightLarge;
     private int mNotificationMaxHeight;
@@ -209,6 +209,9 @@
     private boolean mIsLowPriority;
     private boolean mIsColorized;
     private boolean mUseIncreasedCollapsedHeight;
+    private boolean mUseIncreasedHeadsUpHeight;
+    private float mTranslationWhenRemoved;
+    private boolean mWasChildInGroupWhenRemoved;
 
     @Override
     public boolean isGroupExpansionChanging() {
@@ -327,10 +330,11 @@
         boolean isPreL = Boolean.TRUE.equals(expandedIcon.getTag(R.id.icon_is_pre_L));
         boolean colorize = !isPreL || NotificationUtils.isGrayscale(expandedIcon,
                 NotificationColorUtil.getInstance(mContext));
+        int color = StatusBarIconView.NO_COLOR;
         if (colorize) {
-            int color = mEntry.getContrastedColor(mContext, mIsLowPriority && !isExpanded());
-            expandedIcon.setImageTintList(ColorStateList.valueOf(color));
+            color = mEntry.getContrastedColor(mContext, mIsLowPriority && !isExpanded());
         }
+        expandedIcon.setStaticDrawableColor(color);
     }
 
     private void updateLimits() {
@@ -354,8 +358,14 @@
         boolean headsUpCustom = layout.getHeadsUpChild() != null &&
                 layout.getHeadsUpChild().getId()
                         != com.android.internal.R.id.status_bar_latest_event_content;
-        int headsUpheight = headsUpCustom && beforeN ? mMaxHeadsUpHeightLegacy
-                : mMaxHeadsUpHeight;
+        int headsUpheight;
+        if (headsUpCustom && beforeN) {
+            headsUpheight = mMaxHeadsUpHeightLegacy;
+        } else if (mUseIncreasedHeadsUpHeight && layout == mPrivateLayout) {
+            headsUpheight = mMaxHeadsUpHeightIncreased;
+        } else {
+            headsUpheight = mMaxHeadsUpHeight;
+        }
         layout.setHeights(minHeight, headsUpheight, mNotificationMaxHeight,
                 mNotificationAmbientHeight);
     }
@@ -828,10 +838,22 @@
 
     public void setRemoved() {
         mRemoved = true;
-
+        mTranslationWhenRemoved = getTranslationY();
+        mWasChildInGroupWhenRemoved = isChildInGroup();
+        if (isChildInGroup()) {
+            mTranslationWhenRemoved += getNotificationParent().getTranslationY();
+        }
         mPrivateLayout.setRemoved();
     }
 
+    public boolean wasChildInGroupWhenRemoved() {
+        return mWasChildInGroupWhenRemoved;
+    }
+
+    public float getTranslationWhenRemoved() {
+        return mTranslationWhenRemoved;
+    }
+
     public NotificationChildrenContainer getChildrenContainer() {
         return mChildrenContainer;
     }
@@ -991,6 +1013,10 @@
         mUseIncreasedCollapsedHeight = use;
     }
 
+    public void setUseIncreasedHeadsUpHeight(boolean use) {
+        mUseIncreasedHeadsUpHeight = use;
+    }
+
     public interface ExpansionLogger {
         public void logNotificationExpansion(String key, boolean userAction, boolean expanded);
     }
@@ -1005,12 +1031,14 @@
         mNotificationMinHeightLegacy = getFontScaledHeight(R.dimen.notification_min_height_legacy);
         mNotificationMinHeight = getFontScaledHeight(R.dimen.notification_min_height);
         mNotificationMinHeightLarge = getFontScaledHeight(
-                R.dimen.notification_min_height_large);
+                R.dimen.notification_min_height_increased);
         mNotificationMaxHeight = getFontScaledHeight(R.dimen.notification_max_height);
         mNotificationAmbientHeight = getFontScaledHeight(R.dimen.notification_ambient_height);
         mMaxHeadsUpHeightLegacy = getFontScaledHeight(
                 R.dimen.notification_max_heads_up_height_legacy);
         mMaxHeadsUpHeight = getFontScaledHeight(R.dimen.notification_max_heads_up_height);
+        mMaxHeadsUpHeightIncreased = getFontScaledHeight(
+                R.dimen.notification_max_heads_up_height_increased);
         mIncreasedPaddingBetweenElements = getResources()
                 .getDimensionPixelSize(R.dimen.notification_divider_height_increased);
         mIconTransformContentShiftNoIcon = getResources().getDimensionPixelSize(
@@ -1962,7 +1990,7 @@
         mAboveShelf = aboveShelf;
     }
 
-    public class NotificationViewState extends ExpandableViewState {
+    public static class NotificationViewState extends ExpandableViewState {
 
         private final StackScrollState mOverallState;
 
@@ -1983,8 +2011,11 @@
         @Override
         protected void onYTranslationAnimationFinished(View view) {
             super.onYTranslationAnimationFinished(view);
-            if (mHeadsupDisappearRunning) {
-                setHeadsUpAnimatingAway(false);
+            if (view instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+                if (row.isHeadsUpAnimatingAway()) {
+                    row.setHeadsUpAnimatingAway(false);
+                }
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index d599ec1..bc992d8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -87,6 +87,7 @@
     private KeyguardUpdateMonitorCallback mUpdateMonitor;
 
     private final DevicePolicyManager mDevicePolicyManager;
+    private boolean mDozing;
 
     public KeyguardIndicationController(Context context, ViewGroup indicationArea,
             LockIcon lockIcon) {
@@ -139,7 +140,7 @@
             return;
         }
 
-        if (mDevicePolicyManager.isDeviceManaged()) {
+        if (!mDozing && mDevicePolicyManager.isDeviceManaged()) {
             final CharSequence organizationName =
                     mDevicePolicyManager.getDeviceOwnerOrganizationName();
             if (organizationName != null) {
@@ -224,6 +225,18 @@
         if (mVisible) {
             // Walk down a precedence-ordered list of what should indication
             // should be shown based on user or device state
+            if (mDozing) {
+                // If we're dozing, never show a persistent indication.
+                if (!TextUtils.isEmpty(mTransientIndication)) {
+                    mTextView.switchIndication(mTransientIndication);
+                    mTextView.setTextColor(mTransientTextColor);
+
+                } else {
+                    mTextView.switchIndication(null);
+                }
+                return;
+            }
+
             if (!mUserManager.isUserUnlocked(ActivityManager.getCurrentUser())) {
                 mTextView.switchIndication(com.android.internal.R.string.lockscreen_storage_locked);
                 mTextView.setTextColor(Color.WHITE);
@@ -319,6 +332,12 @@
         }
     };
 
+    public void setDozing(boolean dozing) {
+        mDozing = dozing;
+        updateIndication();
+        updateDisclosure();
+    }
+
     protected class BaseKeyguardCallback extends KeyguardUpdateMonitorCallback {
         private int mLastSuccessiveErrorMessage = -1;
 
@@ -349,7 +368,8 @@
             int errorColor = mContext.getResources().getColor(R.color.system_warning_color, null);
             if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
                 mStatusBarKeyguardViewManager.showBouncerMessage(helpString, errorColor);
-            } else if (updateMonitor.isDeviceInteractive()) {
+            } else if (updateMonitor.isDeviceInteractive()
+                    || mDozing && updateMonitor.isScreenOn()) {
                 mLockIcon.setTransientFpError(true);
                 showTransientIndication(helpString, errorColor);
                 mHandler.removeMessages(MSG_CLEAR_FP_MSG);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java
index f73a5ea..81db429 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java
@@ -126,7 +126,8 @@
         }
 
         public boolean cacheContentViews(Context ctx, Notification updatedNotification,
-                boolean isLowPriority, boolean useIncreasedCollapsedView) {
+                boolean isLowPriority, boolean useIncreasedCollapsedView,
+                boolean useIncreasedHeadsUp) {
             boolean applyInPlace = false;
             if (updatedNotification != null) {
                 final Notification.Builder updatedNotificationBuilder
@@ -136,7 +137,7 @@
                 final RemoteViews newBigContentView = createBigContentView(
                         updatedNotificationBuilder, isLowPriority);
                 final RemoteViews newHeadsUpContentView =
-                        updatedNotificationBuilder.createHeadsUpContentView();
+                        updatedNotificationBuilder.createHeadsUpContentView(useIncreasedHeadsUp);
                 final RemoteViews newPublicNotification
                         = updatedNotificationBuilder.makePublicContentView();
                 final RemoteViews newAmbientNotification
@@ -165,7 +166,7 @@
                 cachedContentView = createContentView(builder, isLowPriority,
                         useIncreasedCollapsedView);
                 cachedBigContentView = createBigContentView(builder, isLowPriority);
-                cachedHeadsUpContentView = builder.createHeadsUpContentView();
+                cachedHeadsUpContentView = builder.createHeadsUpContentView(useIncreasedHeadsUp);
                 cachedPublicContentView = builder.makePublicContentView();
                 cachedAmbientContentView = builder.makeAmbientNotification();
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMenuRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMenuRow.java
index 355022f..534a719 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMenuRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMenuRow.java
@@ -90,8 +90,7 @@
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         Dependency.get(PluginManager.class).addPluginListener(
-                NotificationMenuRowProvider.ACTION, this,
-                NotificationMenuRowProvider.VERSION, false /* Allow multiple */);
+                this, NotificationMenuRowProvider.class, false /* Allow multiple */);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index 2425076..d4ed1dc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -23,6 +23,7 @@
 import android.view.View;
 import android.view.ViewGroup;
 
+import com.android.internal.widget.CachingIconView;
 import com.android.systemui.Interpolators;
 import com.android.systemui.R;
 import com.android.systemui.ViewInvertHelper;
@@ -414,7 +415,8 @@
                 transitionAmount);
         float shelfIconSize = icon.getHeight() * icon.getIconScale();
         float alpha = 1.0f;
-        if (!row.isShowingIcon()) {
+        boolean noIcon = !row.isShowingIcon();
+        if (noIcon) {
             // The view currently doesn't have an icon, lets transform it in!
             alpha = transitionAmount;
             notificationIconSize = shelfIconSize / 2.0f;
@@ -438,6 +440,13 @@
             if (row.isAboveShelf()) {
                 iconState.hidden = true;
             }
+            int shelfColor = icon.getStaticDrawableColor();
+            if (!noIcon && shelfColor != StatusBarIconView.NO_COLOR) {
+                int notificationColor = row.getNotificationHeader().getOriginalNotificationColor();
+                shelfColor = NotificationUtils.interpolateColors(notificationColor, shelfColor,
+                        iconState.iconAppearAmount);
+            }
+            iconState.iconColor = shelfColor;
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
index 6283148..aec9a4b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
@@ -19,9 +19,11 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
 import android.app.Notification;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
+import android.content.res.ColorStateList;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Canvas;
@@ -37,7 +39,6 @@
 import android.util.Log;
 import android.util.Property;
 import android.util.TypedValue;
-import android.view.View;
 import android.view.ViewDebug;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.animation.Interpolator;
@@ -50,6 +51,9 @@
 import java.text.NumberFormat;
 
 public class StatusBarIconView extends AnimatedImageView {
+    public static final int NO_COLOR = 0;
+    private final int ANIMATION_DURATION_FAST = 100;
+
     public static final int STATE_ICON = 0;
     public static final int STATE_DOT = 1;
     public static final int STATE_HIDDEN = 2;
@@ -104,6 +108,17 @@
     private ObjectAnimator mDotAnimator;
     private float mDotAppearAmount;
     private OnVisibilityChangedListener mOnVisibilityChangedListener;
+    private int mDrawableColor;
+    private int mIconColor;
+    private ValueAnimator mColorAnimator;
+    private int mCurrentSetColor = NO_COLOR;
+    private int mAnimationStartColor = NO_COLOR;
+    private final ValueAnimator.AnimatorUpdateListener mColorUpdater
+            = animation -> {
+        int newColor = NotificationUtils.interpolateColors(mAnimationStartColor, mIconColor,
+                animation.getAnimatedFraction());
+        setColorInternal(newColor);
+    };
 
     public StatusBarIconView(Context context, String slot, Notification notification) {
         this(context, slot, notification, false);
@@ -123,7 +138,7 @@
         setScaleType(ScaleType.CENTER);
         mDensity = context.getResources().getDisplayMetrics().densityDpi;
         if (mNotification != null) {
-            setIconTint(getContext().getColor(
+            setDecorColor(getContext().getColor(
                     com.android.internal.R.color.notification_icon_default_color));
         }
         reloadDimens();
@@ -446,10 +461,66 @@
         return c.getString(R.string.accessibility_desc_notification_icon, appName, desc);
     }
 
-    public void setIconTint(int iconTint) {
+    /**
+     * Set the color that is used to draw decoration like the overflow dot. This will not be applied
+     * to the drawable.
+     */
+    public void setDecorColor(int iconTint) {
         mDotPaint.setColor(iconTint);
     }
 
+    /**
+     * Set the static color that should be used for the drawable of this icon if it's not
+     * transitioning this also immediately sets the color.
+     */
+    public void setStaticDrawableColor(int color) {
+        mDrawableColor = color;
+        setColorInternal(color);
+        mIconColor = color;
+    }
+
+    private void setColorInternal(int color) {
+        if (color != NO_COLOR) {
+            setImageTintList(ColorStateList.valueOf(color));
+        } else {
+            setImageTintList(null);
+        }
+        mCurrentSetColor = color;
+    }
+
+    public void setIconColor(int iconColor, boolean animate) {
+        if (mIconColor != iconColor) {
+            mIconColor = iconColor;
+            if (mColorAnimator != null) {
+                mColorAnimator.cancel();
+            }
+            if (mCurrentSetColor == iconColor) {
+                return;
+            }
+            if (animate && mCurrentSetColor != NO_COLOR) {
+                mAnimationStartColor = mCurrentSetColor;
+                mColorAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
+                mColorAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+                mColorAnimator.setDuration(ANIMATION_DURATION_FAST);
+                mColorAnimator.addUpdateListener(mColorUpdater);
+                mColorAnimator.addListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        mColorAnimator = null;
+                        mAnimationStartColor = NO_COLOR;
+                    }
+                });
+                mColorAnimator.start();
+            } else {
+                setColorInternal(iconColor);
+            }
+        }
+    }
+
+    public int getStaticDrawableColor() {
+        return mDrawableColor;
+    }
+
     public void setVisibleState(int state) {
         setVisibleState(state, true /* animate */, null /* endRunnable */);
     }
@@ -467,10 +538,13 @@
         boolean runnableAdded = false;
         if (visibleState != mVisibleState) {
             mVisibleState = visibleState;
+            if (mIconAppearAnimator != null) {
+                mIconAppearAnimator.cancel();
+            }
+            if (mDotAnimator != null) {
+                mDotAnimator.cancel();
+            }
             if (animate) {
-                if (mIconAppearAnimator != null) {
-                    mIconAppearAnimator.cancel();
-                }
                 float targetAmount = 0.0f;
                 Interpolator interpolator = Interpolators.FAST_OUT_LINEAR_IN;
                 if (visibleState == STATE_ICON) {
@@ -482,7 +556,7 @@
                     mIconAppearAnimator = ObjectAnimator.ofFloat(this, ICON_APPEAR_AMOUNT,
                             currentAmount, targetAmount);
                     mIconAppearAnimator.setInterpolator(interpolator);
-                    mIconAppearAnimator.setDuration(100);
+                    mIconAppearAnimator.setDuration(ANIMATION_DURATION_FAST);
                     mIconAppearAnimator.addListener(new AnimatorListenerAdapter() {
                         @Override
                         public void onAnimationEnd(Animator animation) {
@@ -494,9 +568,6 @@
                     runnableAdded = true;
                 }
 
-                if (mDotAnimator != null) {
-                    mDotAnimator.cancel();
-                }
                 targetAmount = visibleState == STATE_ICON ? 2.0f : 0.0f;
                 interpolator = Interpolators.FAST_OUT_LINEAR_IN;
                 if (visibleState == STATE_DOT) {
@@ -508,7 +579,7 @@
                     mDotAnimator = ObjectAnimator.ofFloat(this, DOT_APPEAR_AMOUNT,
                             currentAmount, targetAmount);
                     mDotAnimator.setInterpolator(interpolator);
-                    mDotAnimator.setDuration(100);
+                    mDotAnimator.setDuration(ANIMATION_DURATION_FAST);
                     final boolean runRunnable = !runnableAdded;
                     mDotAnimator.addListener(new AnimatorListenerAdapter() {
                         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ViewTransformationHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/ViewTransformationHelper.java
index 1c89e32..5353005 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ViewTransformationHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ViewTransformationHelper.java
@@ -224,9 +224,6 @@
         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.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java
index d15ab10..c3f1cb1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java
@@ -69,7 +69,8 @@
     public void transformViewFrom(TransformState otherState, float transformationAmount) {
         mTransformedView.animate().cancel();
         if (sameAs(otherState)) {
-            if (mTransformedView.getVisibility() == View.INVISIBLE) {
+            if (mTransformedView.getVisibility() == View.INVISIBLE
+                    || mTransformedView.getAlpha() != 1.0f) {
                 // We have the same content, lets show ourselves
                 mTransformedView.setAlpha(1.0f);
                 mTransformedView.setVisibility(View.VISIBLE);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
index 2836f41..8f63d45 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
@@ -167,6 +167,7 @@
     private IntentButton mLeftPlugin;
     private String mLeftButtonStr;
     private LockscreenGestureLogger mLockscreenGestureLogger = new LockscreenGestureLogger();
+    private boolean mDozing;
 
     public KeyguardBottomAreaView(Context context) {
         this(context, null);
@@ -261,9 +262,9 @@
         super.onAttachedToWindow();
         mAccessibilityController.addStateChangedCallback(this);
         Dependency.get(PluginManager.class).addPluginListener(RIGHT_BUTTON_PLUGIN,
-                mRightListener, IntentButtonProvider.VERSION, false /* Only allow one */);
+                mRightListener, IntentButtonProvider.class, false /* Only allow one */);
         Dependency.get(PluginManager.class).addPluginListener(LEFT_BUTTON_PLUGIN,
-                mLeftListener, IntentButtonProvider.VERSION, false /* Only allow one */);
+                mLeftListener, IntentButtonProvider.class, false /* Only allow one */);
         Dependency.get(TunerService.class).addTunable(this, LockscreenFragment.LOCKSCREEN_LEFT_BUTTON,
                 LockscreenFragment.LOCKSCREEN_RIGHT_BUTTON);
     }
@@ -361,7 +362,7 @@
             // Things are not set up yet; reply hazy, ask again later
             return;
         }
-        mRightAffordanceView.setVisibility(mRightButton.getIcon().isVisible
+        mRightAffordanceView.setVisibility(!mDozing && mRightButton.getIcon().isVisible
                 ? View.VISIBLE : View.GONE);
     }
 
@@ -375,7 +376,7 @@
 
     private void updateLeftAffordanceIcon() {
         IconState state = mLeftButton.getIcon();
-        mLeftAffordanceView.setVisibility(state.isVisible ? View.VISIBLE : View.GONE);
+        mLeftAffordanceView.setVisibility(!mDozing && state.isVisible ? View.VISIBLE : View.GONE);
         mLeftAffordanceView.setImageDrawable(state.drawable, state.tint);
         mLeftAffordanceView.setContentDescription(state.contentDescription);
     }
@@ -846,6 +847,22 @@
         }
     };
 
+    public void setDozing(boolean dozing, boolean animate) {
+        mDozing = dozing;
+
+        updateCameraVisibility();
+        updateLeftAffordanceIcon();
+
+        if (dozing) {
+            mLockIcon.setVisibility(INVISIBLE);
+        } else {
+            mLockIcon.setVisibility(VISIBLE);
+            if (animate) {
+                startFinishDozeAnimation();
+            }
+        }
+    }
+
     private class DefaultLeftButton implements IntentButton {
 
         private IconState mIconState = new IconState();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
index 5fb99da..720ca14 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
@@ -138,8 +138,8 @@
         super.onAttachedToWindow();
         Dependency.get(TunerService.class).addTunable(this, NAV_BAR_VIEWS, NAV_BAR_LEFT,
                 NAV_BAR_RIGHT);
-        Dependency.get(PluginManager.class).addPluginListener(NavBarButtonProvider.ACTION, this,
-                NavBarButtonProvider.VERSION, true /* Allow multiple */);
+        Dependency.get(PluginManager.class).addPluginListener(this,
+                NavBarButtonProvider.class, true /* Allow multiple */);
     }
 
     @Override
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 5d13289..ad875f1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
@@ -783,8 +783,8 @@
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         onPluginDisconnected(null); // Create default gesture helper
-        Dependency.get(PluginManager.class).addPluginListener(NavGesture.ACTION, this,
-                NavGesture.VERSION, false /* Only one */);
+        Dependency.get(PluginManager.class).addPluginListener(this,
+                NavGesture.class, false /* Only one */);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
index 6d7ab47..707997d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
@@ -1,7 +1,6 @@
 package com.android.systemui.statusbar.phone;
 
 import android.content.Context;
-import android.content.res.ColorStateList;
 import android.content.res.Resources;
 import android.graphics.Color;
 import android.graphics.Rect;
@@ -226,12 +225,13 @@
         for (int i = 0; i < mNotificationIcons.getChildCount(); i++) {
             StatusBarIconView v = (StatusBarIconView) mNotificationIcons.getChildAt(i);
             boolean isPreL = Boolean.TRUE.equals(v.getTag(R.id.icon_is_pre_L));
+            int color = StatusBarIconView.NO_COLOR;
             boolean colorize = !isPreL || NotificationUtils.isGrayscale(v, mNotificationColorUtil);
             if (colorize) {
-                v.setImageTintList(ColorStateList.valueOf(
-                        DarkIconDispatcher.getTint(mTintArea, v, mIconTint)));
+                color = DarkIconDispatcher.getTint(mTintArea, v, mIconTint);
             }
-            v.setIconTint(mIconTint);
+            v.setStaticDrawableColor(color);
+            v.setDecorColor(mIconTint);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
index 571ae26..dc5f98c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
@@ -446,6 +446,7 @@
         public boolean useFullTransitionAmount;
         public boolean useLinearTransitionAmount;
         public boolean translateContent;
+        public int iconColor = StatusBarIconView.NO_COLOR;
 
         @Override
         public void applyToView(View view) {
@@ -505,6 +506,7 @@
                     }
                 }
                 icon.setVisibleState(visibleState, animationsAllowed);
+                icon.setIconColor(iconColor, needsCannedAnimation && animationsAllowed);
                 if (animate) {
                     animateTo(icon, animationProperties);
                 } else {
@@ -515,6 +517,14 @@
             needsCannedAnimation = false;
         }
 
+        @Override
+        public void initFrom(View view) {
+            super.initFrom(view);
+            if (view instanceof StatusBarIconView) {
+                iconColor = ((StatusBarIconView) view).getStaticDrawableColor();
+            }
+        }
+
         protected void onYTranslationAnimationFinished(View view) {
             if (hidden) {
                 view.setVisibility(INVISIBLE);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
index 5da3a10..6da9e90 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -1151,9 +1151,7 @@
                     .start();
         } else if (statusBarState == StatusBarState.KEYGUARD
                 || statusBarState == StatusBarState.SHADE_LOCKED) {
-            if (!mDozing) {
-                mKeyguardBottomArea.setVisibility(View.VISIBLE);
-            }
+            mKeyguardBottomArea.setVisibility(View.VISIBLE);
             mKeyguardBottomArea.setAlpha(1f);
         } else {
             mKeyguardBottomArea.setVisibility(View.GONE);
@@ -2103,13 +2101,12 @@
     private void updateDozingVisibilities(boolean animate) {
         if (mDozing) {
             mKeyguardStatusBar.setVisibility(View.INVISIBLE);
-            mKeyguardBottomArea.setVisibility(View.INVISIBLE);
+            mKeyguardBottomArea.setDozing(mDozing, animate);
         } else {
-            mKeyguardBottomArea.setVisibility(View.VISIBLE);
             mKeyguardStatusBar.setVisibility(View.VISIBLE);
+            mKeyguardBottomArea.setDozing(mDozing, animate);
             if (animate) {
                 animateKeyguardStatusBarIn(DOZE_ANIMATION_DURATION);
-                mKeyguardBottomArea.startFinishDozeAnimation();
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStatusBarHeader.java
index 457ed6c..ade1b0b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStatusBarHeader.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStatusBarHeader.java
@@ -26,7 +26,6 @@
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.RippleDrawable;
-import android.icu.text.NumberFormat;
 import android.os.UserManager;
 import android.support.annotation.Nullable;
 import android.support.annotation.VisibleForTesting;
@@ -55,7 +54,6 @@
 import com.android.systemui.qs.TouchAnimator;
 import com.android.systemui.qs.TouchAnimator.Builder;
 import com.android.systemui.statusbar.SignalClusterView;
-import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
 import com.android.systemui.statusbar.policy.NetworkController;
 import com.android.systemui.statusbar.policy.NetworkController.EmergencyListener;
 import com.android.systemui.statusbar.policy.NetworkController.IconState;
@@ -68,7 +66,7 @@
 
 public class QuickStatusBarHeader extends BaseStatusBarHeader implements
         NextAlarmChangeCallback, OnClickListener, OnUserInfoChangedListener, EmergencyListener,
-        BatteryStateChangeCallback, SignalCallback {
+        SignalCallback {
     private static final float EXPAND_INDICATOR_THRESHOLD = .93f;
 
     private ActivityStarter mActivityStarter;
@@ -110,7 +108,6 @@
 
     private boolean mShowFullAlarm;
     private float mDateTimeTranslation;
-    private TextView mBatteryLevel;
     private SparseBooleanArray mRoamingsBySubId = new SparseBooleanArray();
     private boolean mIsRoaming;
 
@@ -161,8 +158,6 @@
         mAlarmStatus = (TextView) findViewById(R.id.alarm_status);
         mAlarmStatus.setOnClickListener(this);
 
-        mBatteryLevel = (TextView) findViewById(R.id.battery_level);
-
         mMultiUserSwitch = (MultiUserSwitch) findViewById(R.id.multi_user_switch);
         mMultiUserAvatar = (ImageView) mMultiUserSwitch.findViewById(R.id.multi_user_avatar);
         mAlwaysShowMultiUserSwitch = res.getBoolean(R.bool.config_alwaysShowMultiUserSwitcher);
@@ -179,7 +174,9 @@
         int colorForeground = Utils.getColorAttr(getContext(), android.R.attr.colorForeground);
         float intensity = colorForeground == Color.WHITE ? 0 : 1;
         cluster.onDarkChanged(new Rect(0, 0, 0, 0), intensity, colorForeground);
+
         BatteryMeterView battery = (BatteryMeterView) findViewById(R.id.battery);
+        battery.forceShowPercent();
         int colorSecondary = Utils.getColorAttr(getContext(), android.R.attr.textColorSecondary);
         battery.setRawColors(colorForeground, colorSecondary);
 
@@ -443,17 +440,6 @@
         }
     }
 
-    @Override
-    public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
-        String percentage = NumberFormat.getPercentInstance().format((double) level / 100.0);
-        mBatteryLevel.setText(percentage);
-    }
-
-    @Override
-    public void onPowerSaveChanged(boolean isPowerSave) {
-        // Don't care.
-    }
-
     public void setMobileDataIndicators(IconState statusIcon, IconState qsIcon, int statusType,
             int qsType, boolean activityIn, boolean activityOut, String typeContentDescription,
             String description, boolean isWide, int subId, boolean roaming) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index 91a71d1..3d6e4b3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -712,6 +712,7 @@
     private NetworkController mNetworkController;
     private KeyguardMonitorImpl mKeyguardMonitor;
     private BatteryController mBatteryController;
+    private boolean mPanelExpanded;
     private LogMaker mStatusBarStateLog;
     private LockscreenGestureLogger mLockscreenGestureLogger = new LockscreenGestureLogger();
     private NotificationIconAreaController mNotificationIconAreaController;
@@ -838,7 +839,7 @@
         createAndAddWindows();
 
         mSettingsObserver.onChange(false); // set up
-        disable(switches[0], switches[6], false /* animate */);
+        mCommandQueue.disable(switches[0], switches[6], false /* animate */);
         setSystemUiVisibility(switches[1], switches[7], switches[8], 0xffffffff,
                 fullscreenStackBounds, dockedStackBounds);
         topAppWindowChanged(switches[2] != 0);
@@ -1128,7 +1129,7 @@
                     .replace(R.id.qs_frame, new QSFragment(), QS.TAG)
                     .commit();
             new PluginFragmentListener(container, QS.TAG, QSFragment.class, QS.class)
-                    .startListening(QS.ACTION, QS.VERSION);
+                    .startListening();
             final QSTileHost qsh = SystemUIFactory.getInstance().createQSTileHost(mContext, this,
                     mIconController);
             mBrightnessMirrorController = new BrightnessMirrorController(mStatusBarWindow);
@@ -2510,7 +2511,7 @@
      * This needs to be called if state used by {@link #adjustDisableFlags} changes.
      */
     public void recomputeDisableFlags(boolean animate) {
-        disable(mDisabledUnmodified1, mDisabledUnmodified2, animate);
+        mCommandQueue.disable(mDisabledUnmodified1, mDisabledUnmodified2, animate);
     }
 
     protected H createHandler() {
@@ -2679,6 +2680,7 @@
     }
 
     public void setPanelExpanded(boolean isExpanded) {
+        mPanelExpanded = isExpanded;
         mStatusBarWindowManager.setPanelExpanded(isExpanded);
         mVisualStabilityManager.setPanelExpanded(isExpanded);
         if (isExpanded && getBarState() != StatusBarState.KEYGUARD) {
@@ -4320,6 +4322,7 @@
         mNotificationPanel.setDozing(mDozing, animate);
         mStackScroller.setDark(mDozing, animate, mWakeUpTouchLocation);
         mScrimController.setDozing(mDozing);
+        mKeyguardIndicationController.setDozing(mDozing);
 
         // Immediately abort the dozing from the doze scrim controller in case of wake-and-unlock
         // for pulsing so the Keyguard fade-out animation scrim can take over.
@@ -6094,8 +6097,10 @@
         boolean isLowPriority = mNotificationData.isAmbient(sbn.getKey());
         boolean useIncreasedCollapsedHeight = mMessagingUtil.isImportantMessaging(sbn,
                 mNotificationData.getImportance(sbn.getKey()));
+        boolean useIncreasedHeadsUp = useIncreasedCollapsedHeight && mPanelExpanded;
         try {
-            entry.cacheContentViews(mContext, null, isLowPriority, useIncreasedCollapsedHeight);
+            entry.cacheContentViews(mContext, null, isLowPriority, useIncreasedCollapsedHeight,
+                    useIncreasedHeadsUp);
         } catch (RuntimeException e) {
             Log.e(TAG, "Unable to get notification remote views", e);
             return false;
@@ -6266,6 +6271,7 @@
         }
         row.setUserLocked(userLocked);
         row.setUseIncreasedCollapsedHeight(useIncreasedCollapsedHeight);
+        row.setUseIncreasedHeadsUpHeight(useIncreasedHeadsUp);
         row.onNotificationUpdated(entry);
         return true;
     }
@@ -6760,10 +6766,13 @@
         boolean useIncreasedCollapsedHeight = mMessagingUtil.isImportantMessaging(notification,
                 mNotificationData.getImportance(notification.getKey()));
         entry.row.setUseIncreasedCollapsedHeight(useIncreasedCollapsedHeight);
+        boolean useIncreasedHeadsUp = useIncreasedCollapsedHeight && mPanelExpanded;
+        entry.row.setUseIncreasedHeadsUpHeight(useIncreasedHeadsUp);
         boolean applyInPlace;
         try {
             applyInPlace = entry.cacheContentViews(mContext, notification.getNotification(),
-                    mNotificationData.isAmbient(key), useIncreasedCollapsedHeight);
+                    mNotificationData.isAmbient(key), useIncreasedCollapsedHeight,
+                    useIncreasedHeadsUp);
         } catch (RuntimeException e) {
             Log.e(TAG, "Unable to get notification remote views", e);
             applyInPlace = false;
@@ -6929,7 +6938,10 @@
             return false;
         }
 
-        if (mNotificationData.getImportance(sbn.getKey()) < NotificationManager.IMPORTANCE_HIGH) {
+        // Allow peeking for DEFAULT notifications only if we're on Ambient Display.
+        int importanceLevel = isDozing() ? NotificationManager.IMPORTANCE_DEFAULT
+                : NotificationManager.IMPORTANCE_HIGH;
+        if (mNotificationData.getImportance(sbn.getKey()) < importanceLevel) {
             if (DEBUG) Log.d(TAG, "No peeking: unimportant notification: " + sbn.getKey());
             return false;
         }
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 99e09c6..67cc5e3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
@@ -88,6 +88,7 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashSet;
+import java.util.List;
 
 /**
  * A layout which handles a dynamic amount of notifications and presents them in a scrollable stack.
@@ -1836,12 +1837,29 @@
      * @return The first child which has visibility unequal to GONE which is currently below the
      *         given translationY or equal to it.
      */
-    private View getFirstChildBelowTranlsationY(float translationY) {
+    private View getFirstChildBelowTranlsationY(float translationY, boolean ignoreChildren) {
         int childCount = getChildCount();
         for (int i = 0; i < childCount; i++) {
             View child = getChildAt(i);
-            if (child.getVisibility() != View.GONE && child.getTranslationY() >= translationY) {
+            if (child.getVisibility() == View.GONE) {
+                continue;
+            }
+            float rowTranslation = child.getTranslationY();
+            if (rowTranslation >= translationY) {
                 return child;
+            } else if (!ignoreChildren && child instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+                if (row.isSummaryWithChildren() && row.areChildrenExpanded()) {
+                    List<ExpandableNotificationRow> notificationChildren =
+                            row.getNotificationChildren();
+                    for (int childIndex = 0; childIndex < notificationChildren.size();
+                            childIndex++) {
+                        ExpandableNotificationRow rowChild = notificationChildren.get(childIndex);
+                        if (rowChild.getTranslationY() + rowTranslation >= translationY) {
+                            return rowChild;
+                        }
+                    }
+                }
             }
         }
         return null;
@@ -2500,7 +2518,7 @@
                     View groupParentWhenDismissed = row.getGroupParentWhenDismissed();
                     nextView = getFirstChildBelowTranlsationY(groupParentWhenDismissed != null
                             ? groupParentWhenDismissed.getTranslationY()
-                            : view.getTranslationY());
+                            : view.getTranslationY(), true /* ignoreChildren */);
                 }
                 if (nextView != null) {
                     nextView.requestAccessibilityFocus();
@@ -2940,7 +2958,17 @@
             AnimationEvent event = new AnimationEvent(child, animationType);
 
             // we need to know the view after this one
-            event.viewAfterChangingView = getFirstChildBelowTranlsationY(child.getTranslationY());
+            float removedTranslation = child.getTranslationY();
+            boolean ignoreChildren = true;
+            if (child instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+                if (row.isRemoved() && row.wasChildInGroupWhenRemoved()) {
+                    removedTranslation = row.getTranslationWhenRemoved();
+                    ignoreChildren = false;
+                }
+            }
+            event.viewAfterChangingView = getFirstChildBelowTranlsationY(removedTranslation,
+                    ignoreChildren);
             mAnimationEvents.add(event);
             mSwipedOutViews.remove(child);
         }
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 55085e5..9893434 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java
@@ -389,10 +389,25 @@
                 // upwards by default
                 float translationDirection = -1.0f;
                 if (viewState != null) {
+                    float ownPosition = changingView.getTranslationY();
+                    if (changingView instanceof ExpandableNotificationRow
+                            && event.viewAfterChangingView instanceof ExpandableNotificationRow) {
+                        ExpandableNotificationRow changingRow =
+                                (ExpandableNotificationRow) changingView;
+                        ExpandableNotificationRow nextRow =
+                                (ExpandableNotificationRow) event.viewAfterChangingView;
+                        if (changingRow.isRemoved()
+                                && changingRow.wasChildInGroupWhenRemoved()
+                                && !nextRow.isChildInGroup()) {
+                            // the next row isn't actually a child from a group! Let's
+                            // compare absolute positions!
+                            ownPosition = changingRow.getTranslationWhenRemoved();
+                        }
+                    }
                     // there was a view after this one, Approximate the distance the next child
                     // travelled
                     translationDirection = ((viewState.yTranslation
-                            - (changingView.getTranslationY() + actualHeight / 2.0f)) * 2 /
+                            - (ownPosition + actualHeight / 2.0f)) * 2 /
                             actualHeight);
                     translationDirection = Math.max(Math.min(translationDirection, 1.0f),-1.0f);
 
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/BatteryPreference.java b/packages/SystemUI/src/com/android/systemui/tuner/BatteryPreference.java
index 3058c0a..1df12ac 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/BatteryPreference.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/BatteryPreference.java
@@ -25,7 +25,7 @@
 import com.android.systemui.Dependency;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
 
-import static com.android.systemui.BatteryMeterDrawable.SHOW_PERCENT_SETTING;
+import static com.android.systemui.BatteryMeterView.SHOW_PERCENT_SETTING;
 
 public class BatteryPreference extends DropDownPreference implements TunerService.Tunable {
 
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/CustomListPreference.java b/packages/SystemUI/src/com/android/systemui/tuner/CustomListPreference.java
new file mode 100644
index 0000000..e50fd5e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/tuner/CustomListPreference.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2017 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.tuner;
+
+import android.annotation.Nullable;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.Bundle;
+import android.support.v14.preference.ListPreferenceDialogFragment;
+import android.support.v7.preference.ListPreference;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+public class CustomListPreference extends ListPreference {
+
+    public CustomListPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public CustomListPreference(Context context, AttributeSet attrs, int defStyleAttr,
+                                int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    protected void onPrepareDialogBuilder(AlertDialog.Builder builder,
+            OnClickListener listener) {
+    }
+
+    protected void onDialogClosed(boolean positiveResult) {
+    }
+
+    protected Dialog onDialogCreated(DialogFragment fragment, Dialog dialog) {
+        return dialog;
+    }
+
+    protected boolean isAutoClosePreference() {
+        return true;
+    }
+
+    /**
+     * Called when a user is about to choose the given value, to determine if we
+     * should show a confirmation dialog.
+     *
+     * @param value the value the user is about to choose
+     * @return the message to show in a confirmation dialog, or {@code null} to
+     *         not request confirmation
+     */
+    protected CharSequence getConfirmationMessage(String value) {
+        return null;
+    }
+
+    protected void onDialogStateRestored(DialogFragment fragment, Dialog dialog,
+            Bundle savedInstanceState) {
+    }
+
+    public static class CustomListPreferenceDialogFragment extends ListPreferenceDialogFragment {
+
+        private static final String KEY_CLICKED_ENTRY_INDEX
+                = "settings.CustomListPrefDialog.KEY_CLICKED_ENTRY_INDEX";
+
+        private int mClickedDialogEntryIndex;
+
+        public static ListPreferenceDialogFragment newInstance(String key) {
+            final ListPreferenceDialogFragment fragment = new CustomListPreferenceDialogFragment();
+            final Bundle b = new Bundle(1);
+            b.putString(ARG_KEY, key);
+            fragment.setArguments(b);
+            return fragment;
+        }
+
+        public CustomListPreference getCustomizablePreference() {
+            return (CustomListPreference) getPreference();
+        }
+
+        @Override
+        protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
+            super.onPrepareDialogBuilder(builder);
+            mClickedDialogEntryIndex = getCustomizablePreference()
+                    .findIndexOfValue(getCustomizablePreference().getValue());
+            getCustomizablePreference().onPrepareDialogBuilder(builder, getOnItemClickListener());
+            if (!getCustomizablePreference().isAutoClosePreference()) {
+                builder.setPositiveButton(com.android.internal.R.string.ok, new OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        onItemConfirmed();
+                    }
+                });
+            }
+        }
+
+        @Override
+        public Dialog onCreateDialog(Bundle savedInstanceState) {
+            Dialog dialog = super.onCreateDialog(savedInstanceState);
+            if (savedInstanceState != null) {
+                mClickedDialogEntryIndex = savedInstanceState.getInt(KEY_CLICKED_ENTRY_INDEX,
+                        mClickedDialogEntryIndex);
+            }
+            return getCustomizablePreference().onDialogCreated(this, dialog);
+        }
+
+        @Override
+        public void onSaveInstanceState(Bundle outState) {
+            super.onSaveInstanceState(outState);
+            outState.putInt(KEY_CLICKED_ENTRY_INDEX, mClickedDialogEntryIndex);
+        }
+
+        @Override
+        public void onActivityCreated(Bundle savedInstanceState) {
+            super.onActivityCreated(savedInstanceState);
+            getCustomizablePreference().onDialogStateRestored(this, getDialog(), savedInstanceState);
+        }
+
+        protected OnClickListener getOnItemClickListener() {
+            return new OnClickListener() {
+                @Override
+                public void onClick(DialogInterface dialog, int which) {
+                    setClickedDialogEntryIndex(which);
+                    if (getCustomizablePreference().isAutoClosePreference()) {
+                        onItemConfirmed();
+                    }
+                }
+            };
+        }
+
+        protected void setClickedDialogEntryIndex(int which) {
+            mClickedDialogEntryIndex = which;
+        }
+
+        private String getValue() {
+            final ListPreference preference = getCustomizablePreference();
+            if (mClickedDialogEntryIndex >= 0 && preference.getEntryValues() != null) {
+                return preference.getEntryValues()[mClickedDialogEntryIndex].toString();
+            } else {
+                return null;
+            }
+        }
+
+        protected void onItemConfirmed() {
+            onClick(getDialog(), DialogInterface.BUTTON_POSITIVE);
+            getDialog().dismiss();
+        }
+
+        @Override
+        public void onDialogClosed(boolean positiveResult) {
+            getCustomizablePreference().onDialogClosed(positiveResult);
+            final ListPreference preference = getCustomizablePreference();
+            final String value = getValue();
+            if (positiveResult && value != null) {
+                if (preference.callChangeListener(value)) {
+                    preference.setValue(value);
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/LockscreenFragment.java b/packages/SystemUI/src/com/android/systemui/tuner/LockscreenFragment.java
index 6f4a3a4..410d3d2 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/LockscreenFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/LockscreenFragment.java
@@ -80,10 +80,8 @@
         mTunerService = Dependency.get(TunerService.class);
         mHandler = new Handler();
         addPreferencesFromResource(R.xml.lockscreen_settings);
-        setupGroup((PreferenceGroup) findPreference(KEY_LEFT), LOCKSCREEN_LEFT_BUTTON,
-                LOCKSCREEN_LEFT_UNLOCK);
-        setupGroup((PreferenceGroup) findPreference(KEY_RIGHT), LOCKSCREEN_RIGHT_BUTTON,
-                LOCKSCREEN_RIGHT_UNLOCK);
+        setupGroup(LOCKSCREEN_LEFT_BUTTON, LOCKSCREEN_LEFT_UNLOCK);
+        setupGroup(LOCKSCREEN_RIGHT_BUTTON, LOCKSCREEN_RIGHT_UNLOCK);
     }
 
     @Override
@@ -92,30 +90,14 @@
         mTunables.forEach(t -> mTunerService.removeTunable(t));
     }
 
-    private void setupGroup(PreferenceGroup group, String buttonSetting, String unlockKey) {
-        SwitchPreference customize = (SwitchPreference) group.findPreference(KEY_CUSTOMIZE);
-        Preference shortcut = group.findPreference(KEY_SHORTCUT);
-        SwitchPreference unlock = (SwitchPreference) group.findPreference(unlockKey);
+    private void setupGroup(String buttonSetting, String unlockKey) {
+        Preference shortcut = findPreference(buttonSetting);
+        SwitchPreference unlock = (SwitchPreference) findPreference(unlockKey);
         addTunable((k, v) -> {
-            boolean visible = v != null;
-            customize.setChecked(visible);
-            shortcut.setVisible(visible);
+            boolean visible = !TextUtils.isEmpty(v);
             unlock.setVisible(visible);
-            if (visible) {
-                setSummary(shortcut, v);
-            }
+            setSummary(shortcut, v);
         }, buttonSetting);
-        customize.setOnPreferenceChangeListener((preference, newValue) -> {
-            boolean hasSetting = mTunerService.getValue(buttonSetting) != null;
-            if (hasSetting != (boolean) newValue) {
-                mHandler.post(() -> mTunerService.setValue(buttonSetting, hasSetting ? null : ""));
-            }
-            return true;
-        });
-        shortcut.setOnPreferenceClickListener(preference -> {
-            showSelectDialog(buttonSetting);
-            return true;
-        });
     }
 
     private void showSelectDialog(String buttonSetting) {
@@ -129,24 +111,15 @@
             mTunerService.setValue(buttonSetting, item.getSettingValue());
             dialog.dismiss();
         });
-        LauncherApps apps = getContext().getSystemService(LauncherApps.class);
-        List<LauncherActivityInfo> activities = apps.getActivityList(null,
-                Process.myUserHandle());
-
-        activities.forEach(info -> {
-            App app = new App(getContext(), info);
-            try {
-                new ShortcutParser(getContext(), info.getComponentName()).getShortcuts().forEach(
-                        shortcut -> app.addChild(new StaticShortcut(getContext(), shortcut)));
-            } catch (NameNotFoundException e) {
-            }
-            adapter.addItem(app);
-        });
 
         v.setAdapter(adapter);
     }
 
     private void setSummary(Preference shortcut, String value) {
+        if (value == null) {
+            shortcut.setSummary(R.string.lockscreen_none);
+            return;
+        }
         if (value.contains("::")) {
             Shortcut info = getShortcutInfo(getContext(), value);
             shortcut.setSummary(info != null ? info.label : null);
@@ -155,7 +128,7 @@
             shortcut.setSummary(info != null ? info.loadLabel(getContext().getPackageManager())
                     : null);
         } else {
-            shortcut.setSummary(null);
+            shortcut.setSummary(R.string.lockscreen_none);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/NavBarTuner.java b/packages/SystemUI/src/com/android/systemui/tuner/NavBarTuner.java
index 28a0057..45abd45 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/NavBarTuner.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/NavBarTuner.java
@@ -33,6 +33,9 @@
 import android.content.DialogInterface.OnClickListener;
 import android.content.res.Resources;
 import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.FontMetricsInt;
+import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
 import android.os.Bundle;
@@ -58,7 +61,7 @@
 
 import java.util.ArrayList;
 
-public class NavBarTuner extends PreferenceFragment {
+public class NavBarTuner extends TunerPreferenceFragment {
 
     private static final String LAYOUT = "layout";
     private static final String LEFT = "left";
@@ -68,13 +71,13 @@
     private static final String KEYCODE = "keycode";
     private static final String ICON = "icon";
 
-    private static final int[] ICONS = new int[]{
-            R.drawable.ic_qs_circle,
-            R.drawable.ic_add,
-            R.drawable.ic_remove,
-            R.drawable.ic_left,
-            R.drawable.ic_right,
-            R.drawable.ic_menu,
+    private static final int[][] ICONS = new int[][]{
+            {R.drawable.ic_qs_circle, R.string.tuner_circle},
+            {R.drawable.ic_add, R.string.tuner_plus},
+            {R.drawable.ic_remove, R.string.tuner_minus},
+            {R.drawable.ic_left, R.string.tuner_left},
+            {R.drawable.ic_right, R.string.tuner_right},
+            {R.drawable.ic_menu, R.string.tuner_menu},
     };
 
     private final ArrayList<Tunable> mTunables = new ArrayList<>();
@@ -96,10 +99,8 @@
     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
         addPreferencesFromResource(R.xml.nav_bar_tuner);
         bindLayout((ListPreference) findPreference(LAYOUT));
-        bindButton((PreferenceCategory) findPreference(LEFT),
-                NAV_BAR_LEFT, NAVSPACE);
-        bindButton((PreferenceCategory) findPreference(RIGHT),
-                NAV_BAR_RIGHT, MENU_IME);
+        bindButton(NAV_BAR_LEFT, NAVSPACE, LEFT);
+        bindButton(NAV_BAR_RIGHT, MENU_IME, RIGHT);
     }
 
     @Override
@@ -129,9 +130,8 @@
         });
     }
 
-    private void bindButton(PreferenceCategory parent, String setting, String def) {
-        String k = parent.getKey();
-        DropDownPreference type = (DropDownPreference) findPreference(TYPE + "_" + k);
+    private void bindButton(String setting, String def, String k) {
+        ListPreference type = (ListPreference) findPreference(TYPE + "_" + k);
         Preference keycode = findPreference(KEYCODE + "_" + k);
         ListPreference icon = (ListPreference) findPreference(ICON + "_" + k);
         setupIcons(icon);
@@ -195,8 +195,14 @@
                     .loadDrawable(getContext());
             d.setTint(Color.BLACK);
             d.setBounds(0, 0, size, size);
-            ImageSpan span = new ImageSpan(d);
+            ImageSpan span = new ImageSpan(d, ImageSpan.ALIGN_BASELINE);
             builder.append("  ", span, 0);
+            builder.append(" ");
+            for (int i = 0; i < ICONS.length; i++) {
+                if (ICONS[i][0] == id) {
+                    builder.append(getString(ICONS[i][1]));
+                }
+            }
             icon.setSummary(builder);
         } catch (Exception e) {
             Log.d("NavButton", "Problem with summary", e);
@@ -204,7 +210,7 @@
         }
     }
 
-    private void setValue(String setting, DropDownPreference type, Preference keycode,
+    private void setValue(String setting, ListPreference type, Preference keycode,
             ListPreference icon) {
         String button = type.getValue();
         if (KEY.equals(button)) {
@@ -226,14 +232,16 @@
                 getContext().getResources().getDisplayMetrics());
         for (int i = 0; i < ICONS.length; i++) {
             SpannableStringBuilder builder = new SpannableStringBuilder();
-            Drawable d = Icon.createWithResource(getContext().getPackageName(), ICONS[i])
+            Drawable d = Icon.createWithResource(getContext().getPackageName(), ICONS[i][0])
                     .loadDrawable(getContext());
             d.setTint(Color.BLACK);
             d.setBounds(0, 0, size, size);
-            ImageSpan span = new ImageSpan(d);
+            ImageSpan span = new ImageSpan(d, ImageSpan.ALIGN_BASELINE);
             builder.append("  ", span, 0);
+            builder.append(" ");
+            builder.append(getString(ICONS[i][1]));
             labels[i] = builder;
-            values[i] = getContext().getPackageName() + "/" + ICONS[i];
+            values[i] = getContext().getPackageName() + "/" + ICONS[i][0];
         }
         icon.setEntries(labels);
         icon.setEntryValues(values);
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/RadioListPreference.java b/packages/SystemUI/src/com/android/systemui/tuner/RadioListPreference.java
new file mode 100644
index 0000000..dc0d8bf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/tuner/RadioListPreference.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2017 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.tuner;
+
+import android.app.AlertDialog.Builder;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.Fragment;
+import android.content.Context;
+import android.content.DialogInterface.OnClickListener;
+import android.os.Bundle;
+import android.support.v14.preference.PreferenceFragment;
+import android.support.v7.preference.Preference;
+import android.support.v7.preference.PreferenceScreen;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.Toolbar;
+
+import com.android.settingslib.Utils;
+import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.R;
+
+import libcore.util.Objects;
+
+public class RadioListPreference extends CustomListPreference {
+
+    private OnClickListener mOnClickListener;
+    private CharSequence mSummary;
+
+    public RadioListPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onPrepareDialogBuilder(Builder builder, OnClickListener listener) {
+        mOnClickListener = listener;
+    }
+
+    @Override
+    public void setSummary(CharSequence summary) {
+        super.setSummary(summary);
+        mSummary = summary;
+    }
+
+    @Override
+    public CharSequence getSummary() {
+        if (mSummary == null || mSummary.toString().contains("%s")) {
+            return super.getSummary();
+        }
+        return mSummary;
+    }
+
+    @Override
+    protected Dialog onDialogCreated(DialogFragment fragment, Dialog dialog) {
+        Dialog d = new Dialog(getContext(), android.R.style.Theme_DeviceDefault_Settings);
+        Toolbar t = (Toolbar) d.findViewById(com.android.internal.R.id.action_bar);
+        View v = new View(getContext());
+        v.setId(R.id.content);
+        d.setContentView(v);
+        t.setTitle(getTitle());
+        t.setNavigationIcon(Utils.getDrawable(d.getContext(), android.R.attr.homeAsUpIndicator));
+        t.setNavigationOnClickListener(view -> d.dismiss());
+
+        RadioFragment f = new RadioFragment();
+        f.setPreference(this);
+        FragmentHostManager.get(v).getFragmentManager()
+                .beginTransaction()
+                .add(android.R.id.content, f)
+                .commit();
+        return d;
+    }
+
+    @Override
+    protected void onDialogStateRestored(DialogFragment fragment, Dialog dialog,
+            Bundle savedInstanceState) {
+        super.onDialogStateRestored(fragment, dialog, savedInstanceState);
+        View view = dialog.findViewById(R.id.content);
+        RadioFragment radioFragment = (RadioFragment) FragmentHostManager.get(view)
+                .getFragmentManager().findFragmentById(R.id.content);
+        if (radioFragment != null) {
+            radioFragment.setPreference(this);
+        }
+    }
+
+    @Override
+    protected void onDialogClosed(boolean positiveResult) {
+        super.onDialogClosed(positiveResult);
+    }
+
+    public static class RadioFragment extends TunerPreferenceFragment {
+        private RadioListPreference mListPref;
+
+        @Override
+        public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+            Context context = getPreferenceManager().getContext();
+            PreferenceScreen screen = getPreferenceManager().createPreferenceScreen(context);
+            setPreferenceScreen(screen);
+            if (mListPref != null) {
+                update();
+            }
+        }
+
+        private void update() {
+            Context context = getPreferenceManager().getContext();
+
+            CharSequence[] entries = mListPref.getEntries();
+            CharSequence[] values = mListPref.getEntryValues();
+            CharSequence current = mListPref.getValue();
+            for (int i = 0; i < entries.length; i++) {
+                CharSequence entry = entries[i];
+                SelectablePreference pref = new SelectablePreference(context);
+                getPreferenceScreen().addPreference(pref);
+                pref.setTitle(entry);
+                pref.setChecked(Objects.equal(current, values[i]));
+                pref.setKey(String.valueOf(i));
+            }
+        }
+
+        @Override
+        public boolean onPreferenceTreeClick(Preference preference) {
+            mListPref.mOnClickListener.onClick(null, Integer.parseInt(preference.getKey()));
+            return true;
+        }
+
+        public void setPreference(RadioListPreference radioListPreference) {
+            mListPref = radioListPreference;
+            if (getPreferenceManager() != null) {
+                update();
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/SelectablePreference.java b/packages/SystemUI/src/com/android/systemui/tuner/SelectablePreference.java
new file mode 100644
index 0000000..1d15708
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/tuner/SelectablePreference.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2017 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.tuner;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.support.v7.preference.CheckBoxPreference;
+import android.util.TypedValue;
+
+import com.android.systemui.statusbar.ScalingDrawableWrapper;
+
+public class SelectablePreference extends CheckBoxPreference {
+    private final int mSize;
+
+    public SelectablePreference(Context context) {
+        super(context);
+        setWidgetLayoutResource(com.android.systemui.R.layout.preference_widget_radiobutton);
+        setSelectable(true);
+        mSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 32,
+                context.getResources().getDisplayMetrics());
+    }
+
+    @Override
+    public void setIcon(Drawable icon) {
+        super.setIcon(new ScalingDrawableWrapper(icon,
+                mSize / (float) icon.getIntrinsicWidth()));
+    }
+
+    @Override
+    public String toString() {
+        return "";
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/ShortcutPicker.java b/packages/SystemUI/src/com/android/systemui/tuner/ShortcutPicker.java
new file mode 100644
index 0000000..533388a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/tuner/ShortcutPicker.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2017 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.tuner;
+
+import static com.android.systemui.tuner.LockscreenFragment.LOCKSCREEN_LEFT_BUTTON;
+
+import android.content.Context;
+import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.graphics.drawable.ColorDrawable;
+import android.os.Bundle;
+import android.os.Process;
+import android.support.v14.preference.PreferenceFragment;
+import android.support.v7.preference.Preference;
+import android.support.v7.preference.PreferenceCategory;
+import android.support.v7.preference.PreferenceScreen;
+import android.support.v7.preference.PreferenceViewHolder;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.tuner.ShortcutParser.Shortcut;
+import com.android.systemui.tuner.TunerService.Tunable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ShortcutPicker extends PreferenceFragment implements Tunable {
+
+    private final ArrayList<SelectablePreference> mSelectablePreferences = new ArrayList<>();
+    private String mKey;
+    private SelectablePreference mNonePreference;
+    private TunerService mTunerService;
+
+    @Override
+    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+        Context context = getPreferenceManager().getContext();
+        PreferenceScreen screen = getPreferenceManager().createPreferenceScreen(context);
+        screen.setOrderingAsAdded(true);
+        PreferenceCategory otherApps = new PreferenceCategory(context);
+        otherApps.setTitle(R.string.tuner_other_apps);
+
+        mNonePreference = new SelectablePreference(context);
+        mSelectablePreferences.add(mNonePreference);
+        mNonePreference.setTitle(R.string.lockscreen_none);
+        mNonePreference.setIcon(R.drawable.ic_remove_circle);
+        screen.addPreference(mNonePreference);
+
+        LauncherApps apps = getContext().getSystemService(LauncherApps.class);
+        List<LauncherActivityInfo> activities = apps.getActivityList(null,
+                Process.myUserHandle());
+
+        screen.addPreference(otherApps);
+        activities.forEach(info -> {
+            try {
+                List<Shortcut> shortcuts = new ShortcutParser(getContext(),
+                        info.getComponentName()).getShortcuts();
+                AppPreference appPreference = new AppPreference(context, info);
+                mSelectablePreferences.add(appPreference);
+                if (shortcuts.size() != 0) {
+                    //PreferenceCategory category = new PreferenceCategory(context);
+                    //screen.addPreference(category);
+                    //category.setTitle(info.getLabel());
+                    screen.addPreference(appPreference);
+                    shortcuts.forEach(shortcut -> {
+                        ShortcutPreference shortcutPref = new ShortcutPreference(context, shortcut,
+                                info.getLabel());
+                        mSelectablePreferences.add(shortcutPref);
+                        screen.addPreference(shortcutPref);
+                    });
+                    return;
+                }
+                otherApps.addPreference(appPreference);
+            } catch (NameNotFoundException e) {
+            }
+        });
+        // Move other apps to the bottom.
+        screen.removePreference(otherApps);
+        for (int i = 0; i < otherApps.getPreferenceCount(); i++) {
+            Preference p = otherApps.getPreference(0);
+            otherApps.removePreference(p);
+            p.setOrder(Preference.DEFAULT_ORDER);
+            screen.addPreference(p);
+        }
+        //screen.addPreference(otherApps);
+
+        setPreferenceScreen(screen);
+        mKey = getArguments().getString(ARG_PREFERENCE_ROOT);
+        mTunerService = Dependency.get(TunerService.class);
+        mTunerService.addTunable(this, mKey);
+    }
+
+    @Override
+    public boolean onPreferenceTreeClick(Preference preference) {
+        mTunerService.setValue(mKey, preference.toString());
+        getActivity().onBackPressed();
+        return true;
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+        if (LOCKSCREEN_LEFT_BUTTON.equals(mKey)) {
+            getActivity().setTitle(R.string.lockscreen_shortcut_left);
+        } else {
+            getActivity().setTitle(R.string.lockscreen_shortcut_right);
+        }
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        mTunerService.removeTunable(this);
+    }
+
+    @Override
+    public void onTuningChanged(String key, String newValue) {
+        String v = newValue != null ? newValue : "";
+        mSelectablePreferences.forEach(p -> p.setChecked(v.equals(p.toString())));
+    }
+
+    private static class AppPreference extends SelectablePreference {
+        private final LauncherActivityInfo mInfo;
+        private boolean mBinding;
+
+        public AppPreference(Context context, LauncherActivityInfo info) {
+            super(context);
+            mInfo = info;
+            setTitle(context.getString(R.string.tuner_launch_app, info.getLabel()));
+            setSummary(context.getString(R.string.tuner_app, info.getLabel()));
+        }
+
+        @Override
+        public void onBindViewHolder(PreferenceViewHolder holder) {
+            mBinding = true;
+            if (getIcon() == null) {
+                setIcon(mInfo.getBadgedIcon(
+                        getContext().getResources().getConfiguration().densityDpi));
+            }
+            mBinding = false;
+            super.onBindViewHolder(holder);
+        }
+
+        @Override
+        protected void notifyChanged() {
+            if (mBinding) return;
+            super.notifyChanged();
+        }
+
+        @Override
+        public String toString() {
+            return mInfo.getComponentName().flattenToString();
+        }
+    }
+
+    private static class ShortcutPreference extends SelectablePreference {
+        private final Shortcut mShortcut;
+        private boolean mBinding;
+
+        public ShortcutPreference(Context context, Shortcut shortcut, CharSequence appLabel) {
+            super(context);
+            mShortcut = shortcut;
+            setTitle(shortcut.label);
+            setSummary(context.getString(R.string.tuner_app, appLabel));
+        }
+
+        @Override
+        public void onBindViewHolder(PreferenceViewHolder holder) {
+            mBinding = true;
+            if (getIcon() == null) {
+                setIcon(mShortcut.icon.loadDrawable(getContext()));
+            }
+            mBinding = false;
+            super.onBindViewHolder(holder);
+        }
+
+        @Override
+        protected void notifyChanged() {
+            if (mBinding) return;
+            super.notifyChanged();
+        }
+
+        @Override
+        public String toString() {
+            return mShortcut.toString();
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java
index 74280a3..4eb1db6 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java
@@ -22,10 +22,12 @@
 import android.support.v7.preference.Preference;
 import android.support.v7.preference.PreferenceScreen;
 import android.util.Log;
+import android.view.MenuItem;
 
 import com.android.settingslib.drawer.SettingsDrawerActivity;
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
+import com.android.systemui.fragments.FragmentService;
 
 public class TunerActivity extends SettingsDrawerActivity implements
         PreferenceFragment.OnPreferenceStartFragmentCallback,
@@ -51,6 +53,22 @@
     }
 
     @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        Dependency.destroy(FragmentService.class, s -> s.destroyAll());
+        Dependency.clearDependencies();
+    }
+
+    @Override
+    public boolean onMenuItemSelected(int featureId, MenuItem item) {
+        if (item.getItemId() == android.R.id.home) {
+            onBackPressed();
+            return true;
+        }
+        return super.onMenuItemSelected(featureId, item);
+    }
+
+    @Override
     public void onBackPressed() {
         if (!getFragmentManager().popBackStackImmediate()) {
             super.onBackPressed();
@@ -62,6 +80,9 @@
         try {
             Class<?> cls = Class.forName(pref.getFragment());
             Fragment fragment = (Fragment) cls.newInstance();
+            final Bundle b = new Bundle(1);
+            b.putString(PreferenceFragment.ARG_PREFERENCE_ROOT, pref.getKey());
+            fragment.setArguments(b);
             FragmentTransaction transaction = getFragmentManager().beginTransaction();
             setTitle(pref.getTitle());
             transaction.replace(R.id.content_frame, fragment);
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerPreferenceFragment.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerPreferenceFragment.java
new file mode 100644
index 0000000..06d40da
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerPreferenceFragment.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2017 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.tuner;
+
+import android.app.DialogFragment;
+import android.os.Bundle;
+import android.support.v14.preference.PreferenceFragment;
+import android.support.v7.preference.Preference;
+
+public abstract class TunerPreferenceFragment extends PreferenceFragment {
+
+    @Override
+    public void onDisplayPreferenceDialog(Preference preference) {
+        DialogFragment f = null;
+        if (preference instanceof CustomListPreference) {
+            f = CustomListPreference.CustomListPreferenceDialogFragment
+                    .newInstance(preference.getKey());
+        } else {
+            super.onDisplayPreferenceDialog(preference);
+        }
+        f.setTargetFragment(this, 0);
+        f.show(getFragmentManager(), "dialog_preference");
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java
index 85be4d7..6a92b2f 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java
@@ -38,7 +38,7 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 
-import static com.android.systemui.BatteryMeterDrawable.SHOW_PERCENT_SETTING;
+import static com.android.systemui.BatteryMeterView.SHOW_PERCENT_SETTING;
 import com.android.systemui.DemoMode;
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/BatteryMeterDrawableTest.java b/packages/SystemUI/tests/src/com/android/systemui/BatteryMeterDrawableTest.java
index 09808d4..6b47ada 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/BatteryMeterDrawableTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/BatteryMeterDrawableTest.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui;
 
+import com.android.settingslib.graph.BatteryMeterDrawableBase;
 
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyFloat;
@@ -41,17 +42,18 @@
 public class BatteryMeterDrawableTest extends SysuiTestCase {
 
     private Resources mResources;
-    private BatteryMeterDrawable mBatteryMeter;
+    private BatteryMeterDrawableBase mBatteryMeter;
 
     @Before
     public void setUp() throws Exception {
         mResources = mContext.getResources();
-        mBatteryMeter = new BatteryMeterDrawable(mContext, 0);
+        mBatteryMeter = new BatteryMeterDrawableBase(mContext, 0);
     }
 
     @Test
     public void testDrawImageButNoTextIfPluggedIn() {
-        mBatteryMeter.onBatteryLevelChanged(0, true, true);
+        mBatteryMeter.setBatteryLevel(0);
+        mBatteryMeter.setPluggedIn(true);
         final Canvas canvas = mock(Canvas.class);
         mBatteryMeter.draw(canvas);
         verify(canvas, atLeastOnce()).drawPath(any(), any());
@@ -60,7 +62,8 @@
 
     @Test
     public void testDrawTextIfNotPluggedIn() {
-        mBatteryMeter.onBatteryLevelChanged(0, false, false);
+        mBatteryMeter.setBatteryLevel(0);
+        mBatteryMeter.setPluggedIn(false);
         final Canvas canvas = mock(Canvas.class);
         mBatteryMeter.draw(canvas);
         verify(canvas, times(1)).drawText(anyString(), anyFloat(), anyFloat(), any());
@@ -68,8 +71,9 @@
 
     @Test
     public void testDrawNoTextIfPowerSaveEnabled() {
-        mBatteryMeter.onBatteryLevelChanged(0, false, false);
-        mBatteryMeter.onPowerSaveChanged(true);
+        mBatteryMeter.setBatteryLevel(0);
+        mBatteryMeter.setPluggedIn(false);
+        mBatteryMeter.setPowerSave(true);
         final Canvas canvas = mock(Canvas.class);
         mBatteryMeter.draw(canvas);
         verify(canvas, never()).drawText(anyString(), anyFloat(), anyFloat(), any());
@@ -79,7 +83,8 @@
     public void testDrawTextWarningAtCriticalLevel() {
         int criticalLevel = mResources.getInteger(
                 com.android.internal.R.integer.config_criticalBatteryWarningLevel);
-        mBatteryMeter.onBatteryLevelChanged(criticalLevel, false, false);
+        mBatteryMeter.setBatteryLevel(criticalLevel);
+        mBatteryMeter.setPluggedIn(false);
         final Canvas canvas = mock(Canvas.class);
         mBatteryMeter.draw(canvas);
         String warningString = mResources.getString(R.string.battery_meter_very_low_overlay_symbol);
@@ -90,7 +95,8 @@
     public void testDrawTextNoWarningAboveCriticalLevel() {
         int criticalLevel = mResources.getInteger(
                 com.android.internal.R.integer.config_criticalBatteryWarningLevel);
-        mBatteryMeter.onBatteryLevelChanged(criticalLevel + 1, false, false);
+        mBatteryMeter.setBatteryLevel(criticalLevel + 1);
+        mBatteryMeter.setPluggedIn(false);
         final Canvas canvas = mock(Canvas.class);
         mBatteryMeter.draw(canvas);
         String warningString = mResources.getString(R.string.battery_meter_very_low_overlay_symbol);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginInstanceManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginInstanceManagerTest.java
index 3715df2..658966c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginInstanceManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginInstanceManagerTest.java
@@ -17,14 +17,27 @@
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertNotNull;
 import static junit.framework.Assert.assertTrue;
-
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyInt;
 import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.PluginInstanceManager.PluginInfo;
+import com.android.systemui.plugins.VersionInfo.InvalidVersionException;
+import com.android.systemui.plugins.annotations.Requires;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
 import android.app.Activity;
 import android.app.NotificationManager;
 import android.content.BroadcastReceiver;
@@ -35,7 +48,6 @@
 import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
 import android.os.HandlerThread;
@@ -44,17 +56,6 @@
 import android.support.test.runner.AndroidJUnit4;
 import android.test.suitebuilder.annotation.SmallTest;
 
-import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.plugins.PluginInstanceManager.PluginInfo;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mockito;
-
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -72,6 +73,7 @@
     private PluginListener mMockListener;
     private PluginInstanceManager mPluginInstanceManager;
     private PluginManager mMockManager;
+    private VersionInfo mMockVersionInfo;
 
     @Before
     public void setup() throws Exception {
@@ -83,8 +85,10 @@
         mMockManager = mock(PluginManager.class);
         when(mMockManager.getClassLoader(any(), any()))
                 .thenReturn(getClass().getClassLoader());
+        mMockVersionInfo = mock(VersionInfo.class);
         mPluginInstanceManager = new PluginInstanceManager(mContextWrapper, mMockPm, "myAction",
-                mMockListener, true, mHandlerThread.getLooper(), 1, mMockManager, true);
+                mMockListener, true, mHandlerThread.getLooper(), mMockVersionInfo,
+                mMockManager, true);
         sMockPlugin = mock(Plugin.class);
         when(sMockPlugin.getVersion()).thenReturn(1);
     }
@@ -145,7 +149,7 @@
         NotificationManager nm = mock(NotificationManager.class);
         mContext.addMockSystemService(Context.NOTIFICATION_SERVICE, nm);
         setupFakePmQuery();
-        when(sMockPlugin.getVersion()).thenReturn(2);
+        doThrow(new InvalidVersionException("", false)).when(mMockVersionInfo).checkVersion(any());
 
         mPluginInstanceManager.loadAll();
 
@@ -181,7 +185,8 @@
     public void testNonDebuggable() throws Exception {
         // Create a version that thinks the build is not debuggable.
         mPluginInstanceManager = new PluginInstanceManager(mContextWrapper, mMockPm, "myAction",
-                mMockListener, true, mHandlerThread.getLooper(), 1, mMockManager, false);
+                mMockListener, true, mHandlerThread.getLooper(), mMockVersionInfo,
+                mMockManager, false);
         setupFakePmQuery();
 
         mPluginInstanceManager.loadAll();
@@ -270,6 +275,9 @@
         }
     }
 
+    // This target class doesn't matter, it just needs to have a Requires to hit the flow where
+    // the mock version info is called.
+    @Requires(target = PluginManagerTest.class, version = 1)
     public static class TestPlugin implements Plugin {
         @Override
         public int getVersion() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginManagerTest.java
index a58407b..09ac5a6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginManagerTest.java
@@ -32,6 +32,7 @@
 
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.annotations.ProvidesInterface;
 import com.android.systemui.plugins.PluginInstanceManager.PluginInfo;
 import com.android.systemui.plugins.PluginManager.PluginInstanceManagerFactory;
 
@@ -63,7 +64,7 @@
         mMockFactory = mock(PluginInstanceManagerFactory.class);
         mMockPluginInstance = mock(PluginInstanceManager.class);
         when(mMockFactory.createPluginInstanceManager(Mockito.any(), Mockito.any(), Mockito.any(),
-                Mockito.anyBoolean(), Mockito.any(), Mockito.anyInt(), Mockito.any()))
+                Mockito.anyBoolean(), Mockito.any(), Mockito.any(), Mockito.any()))
                 .thenReturn(mMockPluginInstance);
         mPluginManager = new PluginManager(getContext(), mMockFactory, true, mMockExceptionHandler);
         resetExceptionHandler();
@@ -76,20 +77,20 @@
         Plugin mockPlugin = mock(Plugin.class);
         when(mMockPluginInstance.getPlugin()).thenReturn(new PluginInfo(null, null, mockPlugin,
                 null));
-        Plugin result = mPluginManager.getOneShotPlugin("myAction", 1);
+        Plugin result = mPluginManager.getOneShotPlugin("myAction", TestPlugin.class);
         assertTrue(result == mockPlugin);
     }
 
     @Test
     public void testAddListener() {
-        mPluginManager.addPluginListener("myAction", mMockListener, 1);
+        mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class);
 
         verify(mMockPluginInstance).loadAll();
     }
 
     @Test
     public void testRemoveListener() {
-        mPluginManager.addPluginListener("myAction", mMockListener, 1);
+        mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class);
 
         mPluginManager.removePluginListener(mMockListener);
         verify(mMockPluginInstance).destroy();
@@ -101,16 +102,16 @@
                 mMockExceptionHandler);
         resetExceptionHandler();
 
-        mPluginManager.addPluginListener("myAction", mMockListener, 1);
+        mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class);
         verify(mMockPluginInstance, Mockito.never()).loadAll();
 
-        assertNull(mPluginManager.getOneShotPlugin("myPlugin", 1));
+        assertNull(mPluginManager.getOneShotPlugin("myPlugin", TestPlugin.class));
         verify(mMockPluginInstance, Mockito.never()).getPlugin();
     }
 
     @Test
     public void testExceptionHandler_foundPlugin() {
-        mPluginManager.addPluginListener("myAction", mMockListener, 1);
+        mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class);
         when(mMockPluginInstance.checkAndDisable(Mockito.any())).thenReturn(true);
 
         mPluginExceptionHandler.uncaughtException(Thread.currentThread(), new Throwable());
@@ -125,7 +126,7 @@
 
     @Test
     public void testExceptionHandler_noFoundPlugin() {
-        mPluginManager.addPluginListener("myAction", mMockListener, 1);
+        mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class);
         when(mMockPluginInstance.checkAndDisable(Mockito.any())).thenReturn(false);
 
         mPluginExceptionHandler.uncaughtException(Thread.currentThread(), new Throwable());
@@ -161,4 +162,10 @@
         // Set back the real exception handler so the test can crash if it wants to.
         Thread.setDefaultUncaughtExceptionHandler(mRealExceptionHandler);
     }
+
+    @ProvidesInterface(action = TestPlugin.ACTION, version = TestPlugin.VERSION)
+    public static interface TestPlugin extends Plugin {
+        public static final String ACTION = "testAction";
+        public static final int VERSION = 1;
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/plugins/VersionInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/plugins/VersionInfoTest.java
new file mode 100644
index 0000000..0d87d6b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/plugins/VersionInfoTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2017 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.plugins;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.VersionInfo.InvalidVersionException;
+import com.android.systemui.plugins.annotations.Requires;
+import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.plugins.qs.QS.Callback;
+import com.android.systemui.plugins.qs.QS.DetailAdapter;
+import com.android.systemui.plugins.qs.QS.HeightListener;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+public class VersionInfoTest extends SysuiTestCase {
+
+    @Rule
+    public ExpectedException mThrown = ExpectedException.none();
+
+    @Test
+    public void testHasInfo() {
+        VersionInfo info = new VersionInfo();
+        info.addClass(VersionInfoTest.class); // Has no annotations.
+        assertFalse(info.hasVersionInfo());
+
+        info.addClass(OverlayPlugin.class);
+        assertTrue(info.hasVersionInfo());
+    }
+
+    @Test
+    public void testSingleProvides() {
+        VersionInfo overlay = new VersionInfo().addClass(OverlayPlugin.class);
+        VersionInfo impl = new VersionInfo().addClass(OverlayImpl.class);
+        overlay.checkVersion(impl);
+    }
+
+    @Test
+    public void testIncorrectVersion() {
+        VersionInfo overlay = new VersionInfo().addClass(OverlayPlugin.class);
+        VersionInfo impl = new VersionInfo().addClass(OverlayImplIncorrectVersion.class);
+        mThrown.expect(InvalidVersionException.class);
+        overlay.checkVersion(impl);
+    }
+
+    @Test
+    public void testMissingRequired() {
+        VersionInfo overlay = new VersionInfo().addClass(OverlayPlugin.class);
+        VersionInfo impl = new VersionInfo();
+        mThrown.expect(InvalidVersionException.class);
+        overlay.checkVersion(impl);
+    }
+
+    @Test
+    public void testMissingDependencies() {
+        VersionInfo overlay = new VersionInfo().addClass(QS.class);
+        VersionInfo impl = new VersionInfo().addClass(QSImplNoDeps.class);
+        mThrown.expect(InvalidVersionException.class);
+        overlay.checkVersion(impl);
+    }
+
+    @Test
+    public void testHasDependencies() {
+        VersionInfo overlay = new VersionInfo().addClass(QS.class);
+        VersionInfo impl = new VersionInfo().addClass(QSImpl.class);
+        overlay.checkVersion(impl);
+    }
+
+    @Requires(target = OverlayPlugin.class, version = OverlayPlugin.VERSION)
+    public static class OverlayImpl {
+    }
+
+    @Requires(target = OverlayPlugin.class, version = 0)
+    public static class OverlayImplIncorrectVersion {
+    }
+
+    @Requires(target = QS.class, version = QS.VERSION)
+    public static class QSImplNoDeps {
+    }
+
+    @Requires(target = QS.class, version = QS.VERSION)
+    @Requires(target = Callback.class, version = Callback.VERSION)
+    @Requires(target = HeightListener.class, version = HeightListener.VERSION)
+    @Requires(target = DetailAdapter.class, version = DetailAdapter.VERSION)
+    public static class QSImpl {
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java
index 4146cb81..70c7d3e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java
@@ -18,26 +18,32 @@
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertTrue;
 
+import static org.mockito.Mockito.mock;
+
 import android.content.ComponentName;
-import android.content.Context;
 import android.os.Looper;
 import android.service.quicksettings.Tile;
-import android.support.test.runner.AndroidJUnit4;
 import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.systemui.SysUIRunner;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.statusbar.phone.QSTileHost;
-import com.android.systemui.statusbar.policy.DataSaverController;
-import com.android.systemui.statusbar.policy.HotspotController;
-import com.android.systemui.statusbar.policy.NetworkController;
-import java.util.ArrayList;
+import com.android.systemui.statusbar.phone.StatusBarIconController;
+import com.android.systemui.utils.TestableLooper;
+import com.android.systemui.utils.TestableLooper.RunWithLooper;
+
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mockito;
 
+import java.util.ArrayList;
+
 @SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(SysUIRunner.class)
+@RunWithLooper(setAsMainLooper = true)
 public class TileServicesTest extends SysuiTestCase {
     private static int NUM_FAKES = TileServices.DEFAULT_MAX_BOUND * 2;
 
@@ -46,16 +52,24 @@
 
     @Before
     public void setUp() throws Exception {
+        TestableLooper.get(this).setAsMainLooper();
         mManagers = new ArrayList<>();
-        QSTileHost host = new QSTileHost(mContext, null, null);
+        QSTileHost host = new QSTileHost(mContext, null,
+                mock(StatusBarIconController.class));
         mTileService = new TestTileServices(host, Looper.getMainLooper());
     }
 
+    @After
+    public void tearDown() throws Exception {
+        mTileService.getHost().destroy();
+        TestableLooper.get(this).processAllMessages();
+    }
+
     @Test
     public void testRecalculateBindAllowance() {
         // Add some fake tiles.
         for (int i = 0; i < NUM_FAKES; i++) {
-            mTileService.getTileWrapper(Mockito.mock(CustomTile.class));
+            mTileService.getTileWrapper(mock(CustomTile.class));
         }
         assertEquals(NUM_FAKES, mManagers.size());
 
@@ -91,7 +105,7 @@
     @Test
     public void testCalcFew() {
         for (int i = 0; i < TileServices.DEFAULT_MAX_BOUND - 1; i++) {
-            mTileService.getTileWrapper(Mockito.mock(CustomTile.class));
+            mTileService.getTileWrapper(mock(CustomTile.class));
         }
         mTileService.recalculateBindAllowance();
 
@@ -115,7 +129,7 @@
 
         @Override
         protected TileServiceManager onCreateTileService(ComponentName component, Tile qsTile) {
-            TileServiceManager manager = Mockito.mock(TileServiceManager.class);
+            TileServiceManager manager = mock(TileServiceManager.class);
             mManagers.add(manager);
             return manager;
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakePluginManager.java b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakePluginManager.java
index d1abcca..59a9361 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakePluginManager.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakePluginManager.java
@@ -31,13 +31,7 @@
 
     @Override
     public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
-            int version) {
-        mLeakChecker.addCallback(listener);
-    }
-
-    @Override
-    public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
-            int version, boolean allowMultiple) {
+            Class cls, boolean allowMultiple) {
         mLeakChecker.addCallback(listener);
     }
 
@@ -47,7 +41,7 @@
     }
 
     @Override
-    public <T extends Plugin> T getOneShotPlugin(String action, int version) {
+    public <T extends Plugin> T getOneShotPlugin(String action, Class<?> cls) {
         return null;
     }
 }
diff --git a/proto/src/metrics_constants.proto b/proto/src/metrics_constants.proto
index 653e80a..36dee49 100644
--- a/proto/src/metrics_constants.proto
+++ b/proto/src/metrics_constants.proto
@@ -41,6 +41,9 @@
 
     // The view or control was dismissed.
     TYPE_DISMISS = 5;
+
+    // The view or control was updated.
+    TYPE_UPDATE = 6;
   }
 
   // Known visual elements: views or controls.
@@ -3397,6 +3400,22 @@
     // ACTION: A tile in Settings information architecture is clicked
     ACTION_SETTINGS_TILE_CLICK = 830;
 
+    // OPEN: Notification unsnoozed. CLOSE: Notification snoozed. UPDATE: snoozed notification
+    // updated
+    // CATEGORY: NOTIFICATION
+    // OS: O
+    NOTIFICATION_SNOOZED = 831;
+
+    // Tagged data for NOTIFICATION_SNOOZED. TRUE: snoozed until context, FALSE: snoozed for time.
+    // OS: O
+    NOTIFICATION_SNOOZED_CRITERIA = 832;
+
+    // FIELD - The context (source) from which an action is performed
+    FIELD_CONTEXT = 833;
+
+    // ACTION: Settings advanced button is expanded
+    ACTION_SETTINGS_ADVANCED_BUTTON_EXPAND = 834;
+
     // ---- End O Constants, all O constants go above this line ----
 
     // Add new aosp constants above this line.
diff --git a/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java
index 8d43dfb..85bf5c2 100644
--- a/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java
+++ b/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java
@@ -39,6 +39,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentSender;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ServiceInfo;
 import android.graphics.Rect;
@@ -181,6 +182,23 @@
         updateLocked();
     }
 
+    CharSequence getServiceName() {
+        if (mInfo == null) {
+            return null;
+        }
+        final ComponentName serviceComponent = mInfo.getServiceInfo().getComponentName();
+        final String packageName = serviceComponent.getPackageName();
+
+        try {
+            final PackageManager pm = mContext.getPackageManager();
+            final ApplicationInfo info = pm.getApplicationInfo(packageName, 0);
+            return pm.getApplicationLabel(info);
+        } catch (Exception e) {
+            Slog.w(TAG, "Could not get label for " + packageName + ": " + e);
+            return packageName;
+        }
+    }
+
     void updateLocked() {
         ComponentName serviceComponent = null;
         ServiceInfo serviceInfo = null;
@@ -438,14 +456,20 @@
 
         final AutoFillId mId;
         private final Listener mListener;
-        // // TODO(b/33197203): does it really need a reference to the session's response?
-        private FillResponse mResponse;
+        // TODO(b/33197203): would not need a reference to response and session if it was an inner
+        // class of Session...
+        private final Session mSession;
+        // TODO(b/33197203): encapsulate access so it's not called by UI
+        FillResponse mResponse;
+        Intent mAuthIntent;
+
         private AutoFillValue mAutoFillValue;
         private Rect mBounds;
 
         private boolean mValueUpdated;
 
-        ViewState(AutoFillId id, Listener listener) {
+        ViewState(Session session, AutoFillId id, Listener listener) {
+            mSession = session;
             mId = id;
             mListener = listener;
         }
@@ -458,6 +482,18 @@
             maybeCallOnFillReady();
         }
 
+        /**
+         * Used when a {@link FillResponse} requires authentication to be unlocked.
+         */
+        void setResponse(FillResponse response, Intent authIntent) {
+            mAuthIntent = authIntent;
+            setResponse(response);
+        }
+
+        CharSequence getServiceName() {
+            return mSession.getServiceName();
+        }
+
         // TODO(b/33197203): need to refactor / rename / document this method to make it clear that
         // it can change  the value and update the UI; similarly, should replace code that
         // directly sets mAutoFilLValue to use encapsulation.
@@ -495,6 +531,7 @@
             pw.print(prefix); pw.print("value:" ); pw.println(mAutoFillValue);
             pw.print(prefix); pw.print("updated:" ); pw.println(mValueUpdated);
             pw.print(prefix); pw.print("bounds:" ); pw.println(mBounds);
+            pw.print(prefix); pw.print("authIntent:" ); pw.println(mAuthIntent);
         }
     }
 
@@ -565,7 +602,6 @@
             }
         }
 
-
         // FillServiceCallbacks
         @Override
         public void onFillRequestSuccess(FillResponse response) {
@@ -763,7 +799,7 @@
 
             ViewState viewState = mViewStates.get(id);
             if (viewState == null) {
-                viewState = new ViewState(id, this);
+                viewState = new ViewState(this, id, this);
                 mViewStates.put(id, viewState);
             }
 
@@ -844,13 +880,19 @@
 
             // TODO(b/33197203): add MetricsLogger calls
 
+            if (mCurrentViewState == null) {
+                // TODO(b/33197203): temporary sanity check; should never happen
+                Slog.w(TAG, "processResponseLocked(): mCurrentResponse is null");
+                return;
+            }
+
             mCurrentResponse = response;
 
             if (mCurrentResponse.getAuthentication() != null) {
                 // Handle authentication.
                 final Intent fillInIntent = createAuthFillInIntent(mStructure);
-                getUiForShowing().showFillResponseAuthRequest(
-                        mCurrentResponse.getAuthentication(), fillInIntent);
+
+                mCurrentViewState.setResponse(mCurrentResponse, fillInIntent);
                 return;
             }
 
@@ -864,10 +906,7 @@
                 return;
             }
 
-            // TODO(b/33197203): Consider using mCurrentResponse, depends on partitioning design
-            if (mCurrentViewState != null) {
-                mCurrentViewState.setResponse(mCurrentResponse);
-            }
+            mCurrentViewState.setResponse(mCurrentResponse);
         }
 
         void autoFill(Dataset dataset) {
@@ -884,6 +923,10 @@
             }
         }
 
+        CharSequence getServiceName() {
+            return AutoFillManagerServiceImpl.this.getServiceName();
+        }
+
         private Intent createAuthFillInIntent(AssistStructure structure) {
             Intent fillInIntent = new Intent();
             fillInIntent.putExtra(AutoFillManager.EXTRA_ASSIST_STRUCTURE, structure);
diff --git a/services/autofill/java/com/android/server/autofill/AutoFillUI.java b/services/autofill/java/com/android/server/autofill/AutoFillUI.java
index 9770040..e83dc1e 100644
--- a/services/autofill/java/com/android/server/autofill/AutoFillUI.java
+++ b/services/autofill/java/com/android/server/autofill/AutoFillUI.java
@@ -56,19 +56,15 @@
     private static final long SNACK_BAR_LIFETIME_MS = 30 * DateUtils.SECOND_IN_MILLIS;
     private static final int MSG_HIDE_SNACK_BAR = 1;
 
-    private static final String EXTRA_AUTH_INTENT_SENDER =
-            "com.android.server.autofill.extra.AUTH_INTENT_SENDER";
-    private static final String EXTRA_AUTH_FILL_IN_INTENT =
-            "com.android.server.autofill.extra.AUTH_FILL_IN_INTENT";
-
     private final Context mContext;
     private final WindowManager mWm;
 
     // TODO(b/33197203) Fix locking - some state requires lock and some not - requires refactoring
+    private final Object mLock = new Object();
 
     // Fill UI variables
     private AnchoredWindow mFillWindow;
-    private DatasetPicker mFillView;
+    private View mFillView;
     private ViewState mViewState;
 
     private AutoFillUiCallback mCallback;
@@ -156,63 +152,76 @@
 
         UiThread.getHandler().runWithScissors(() -> {
             hideSnackbarUiThread();
-            hideFillResponseAuthUiUiThread();
         }, 0);
 
-        if (datasets == null) {
+        if (datasets == null && viewState.mAuthIntent == null) {
             // TODO(b/33197203): shouldn't be called, but keeping the WTF for a while just to be
             // safe, otherwise it would crash system server...
             Slog.wtf(TAG, "showFillUI(): no dataset");
             return;
         }
 
+        // TODO(b/33197203): should not display UI after we launched an authentication intent, since
+        // we have no warranty the provider will call onFailure() if the authentication failed or
+        // user dismissed the auth window
+        // because if the service does not handle calling the callback,
+
+
         UiThread.getHandler().runWithScissors(() -> {
+            // The dataset picker is only shown when authentication is not required...
+            DatasetPicker datasetPicker = null;
+
             if (mViewState == null || !mViewState.mId.equals(viewState.mId)) {
                 hideFillUiUiThread();
 
                 mViewState = viewState;
+                if (viewState.mAuthIntent != null) {
+                    final CharSequence serviceName = viewState.getServiceName();
 
-                mFillView = new DatasetPicker(mContext, datasets,
-                        (dataset) -> {
-                            final AutoFillUiCallback callback;
-                            synchronized (mLock) {
-                                callback = mCallback;
-                            }
-                            if (callback != null) {
-                                callback.fill(dataset);
-                            } else {
-                                Slog.w(TAG, "null callback on showFillUi() for " + viewState.mId);
-                            }
-                            hideFillUi();
-                        });
+                    mFillView = new SignInPrompt(mContext, serviceName, (e) -> {
+                        final IntentSender intentSender = viewState.mResponse.getAuthentication();
+                        final AutoFillUiCallback callback;
+                        final Intent authIntent;
+                        synchronized (mLock) {
+                            callback = mCallback;
+                            authIntent = viewState.mAuthIntent;
+                            // Must reset the authentication intent so UI display the datasets after
+                            // the user authenticated.
+                            viewState.mAuthIntent = null;
+                        }
+                        if (callback != null) {
+                            callback.authenticate(intentSender, authIntent);
+                        } else {
+                            // TODO(b/33197203): need to figure out why it's null sometimes
+                            Slog.w(TAG, "no callback on showFillUi().auth for " + viewState.mId);
+                        }
+                    });
 
+                } else {
+                    mFillView = datasetPicker = new DatasetPicker(mContext, datasets,
+                            (dataset) -> {
+                                final AutoFillUiCallback callback;
+                                synchronized (mLock) {
+                                    callback = mCallback;
+                                }
+                                if (callback != null) {
+                                    callback.fill(dataset);
+                                } else {
+                                    // TODO(b/33197203): need to figure out why it's null sometimes
+                                    Slog.w(TAG, "no callback on showFillUi() for " + viewState.mId);
+                                }
+                                hideFillUiUiThread();
+                            });
+                }
                 mFillWindow = new AnchoredWindow(mWm, appToken, mFillView);
 
-                if (DEBUG) Slog.d(TAG, "showFillUi(): view changed");
+                if (DEBUG) Slog.d(TAG, "showFillUi(): view changed for: " + viewState.mId);
             }
-
-            if (DEBUG) Slog.d(TAG, "showFillUi(): bounds=" + bounds + ", filterText=" + filterText);
-            mFillView.update(filterText);
+            if (datasetPicker != null) {
+                datasetPicker.update(filterText);
+            }
             mFillWindow.show(bounds);
-        }, 0);
-    }
 
-    /**
-     * Shows an UI affordance indicating that user action is required before a {@link
-     * android.service.autofill.FillResponse}
-     * can be used.
-     *
-     * <p>It typically replaces the auto-fill bar with a message saying "Press fingerprint or tap to
-     * autofill" or "Tap to autofill", depending on the value of {@code usesFingerprint}.
-     */
-    void showFillResponseAuthRequest(IntentSender intent, Intent fillInIntent) {
-        if (!hasCallback()) {
-            return;
-        }
-        hideAll();
-        UiThread.getHandler().runWithScissors(() -> {
-            // TODO(b/33197203): proper implementation
-            showFillResponseAuthUiUiThread(intent, fillInIntent);
         }, 0);
     }
 
@@ -250,14 +259,12 @@
         UiThread.getHandler().runWithScissors(() -> {
             hideSnackbarUiThread();
             hideFillUiUiThread();
-            hideFillResponseAuthUiUiThread();
         }, 0);
     }
 
     void dump(PrintWriter pw) {
         pw.println("AufoFill UI");
         final String prefix = "  ";
-        pw.print(prefix); pw.print("sResultCode: "); pw.println(sResultCode);
         pw.print(prefix); pw.print("mActivityToken: "); pw.println(mActivityToken);
         pw.print(prefix); pw.print("mSnackBar: "); pw.println(mSnackbar);
         pw.print(prefix); pw.print("mViewState: "); pw.println(mViewState);
@@ -310,103 +317,4 @@
         void fill(Dataset dataset);
         void save();
     }
-
-    /////////////////////////////////////////////////////////////////////////////////
-    // TODO(b/33197203): temporary code using a notification to request auto-fill. //
-    // Will be removed once UX decide the right way to present it to the user.     //
-    /////////////////////////////////////////////////////////////////////////////////
-
-    // TODO(b/33197203): remove from frameworks/base/core/res/AndroidManifest.xml once not used
-    private static final String NOTIFICATION_AUTO_FILL_INTENT =
-            "com.android.internal.autofill.action.REQUEST_AUTOFILL";
-
-    private BroadcastReceiver mNotificationReceiver;
-    private final Object mLock = new Object();
-
-    // Hack used to generate unique pending intents
-    static int sResultCode = 0;
-
-    private void ensureNotificationListener() {
-        synchronized (mLock) {
-            if (mNotificationReceiver == null) {
-                mNotificationReceiver = new NotificationReceiver();
-                mContext.registerReceiver(mNotificationReceiver,
-                        new IntentFilter(NOTIFICATION_AUTO_FILL_INTENT));
-            }
-        }
-    }
-
-    final class NotificationReceiver extends BroadcastReceiver {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            final AutoFillUiCallback callback;
-            synchronized (mLock) {
-                callback = mCallback;
-            }
-            if (callback != null) {
-                IntentSender intentSender = intent.getParcelableExtra(EXTRA_AUTH_INTENT_SENDER);
-                Intent fillInIntent = intent.getParcelableExtra(EXTRA_AUTH_FILL_IN_INTENT);
-                callback.authenticate(intentSender, fillInIntent);
-            }
-            collapseStatusBar();
-        }
-    }
-
-    @android.annotation.UiThread
-    private void showFillResponseAuthUiUiThread(IntentSender intent, Intent fillInIntent) {
-        final String title = "AutoFill Authentication";
-        final StringBuilder subTitle = new StringBuilder("Provider require user authentication.\n");
-
-        final Intent authIntent = new Intent(NOTIFICATION_AUTO_FILL_INTENT);
-        authIntent.putExtra(EXTRA_AUTH_INTENT_SENDER, intent);
-        authIntent.putExtra(EXTRA_AUTH_FILL_IN_INTENT, fillInIntent);
-
-        final PendingIntent authPendingIntent = PendingIntent.getBroadcast(
-                mContext, ++sResultCode, authIntent, PendingIntent.FLAG_ONE_SHOT);
-
-        subTitle.append("Tap notification to launch its authentication UI.");
-
-        final Notification.Builder notification = newNotificationBuilder()
-                .setAutoCancel(true)
-                .setOngoing(false)
-                .setContentTitle(title)
-                .setStyle(new Notification.BigTextStyle().bigText(subTitle.toString()))
-                .setContentIntent(authPendingIntent);
-
-        ensureNotificationListener();
-
-        final long identity = Binder.clearCallingIdentity();
-        try {
-            NotificationManager.from(mContext).notify(0, notification.build());
-        } finally {
-            Binder.restoreCallingIdentity(identity);
-        }
-    }
-
-    @android.annotation.UiThread
-    private void hideFillResponseAuthUiUiThread() {
-        final long identity = Binder.clearCallingIdentity();
-        try {
-            NotificationManager.from(mContext).cancel(0);
-        } finally {
-            Binder.restoreCallingIdentity(identity);
-        }
-    }
-
-    private Notification.Builder newNotificationBuilder() {
-        return new Notification.Builder(mContext)
-                .setCategory(Notification.CATEGORY_SYSTEM)
-                .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
-                .setLocalOnly(true)
-                .setColor(mContext.getColor(
-                        com.android.internal.R.color.system_notification_accent_color));
-    }
-
-    private void collapseStatusBar() {
-        final StatusBarManager sbm = (StatusBarManager) mContext.getSystemService("statusbar");
-        sbm.collapsePanels();
-    }
-    /////////////////////////////////////////
-    // End of temporary notification code. //
-    /////////////////////////////////////////
 }
diff --git a/services/autofill/java/com/android/server/autofill/SignInPrompt.java b/services/autofill/java/com/android/server/autofill/SignInPrompt.java
new file mode 100644
index 0000000..6d17acd
--- /dev/null
+++ b/services/autofill/java/com/android/server/autofill/SignInPrompt.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.autofill;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.Button;
+
+/**
+ * A view displaying the sign-in prompt for an auto-fill service.
+ */
+final class SignInPrompt extends Button {
+
+    SignInPrompt(Context context, CharSequence serviceName, View.OnClickListener listener) {
+        super(context);
+        // TODO(b/33197203): use strings.xml
+        final String text = serviceName != null
+                ? "Sign in to " + serviceName + " to autofill"
+                : "Sign in to autofill";
+
+        // TODO(b/33197203): polish UI / use better altenative than a button...
+        setText(text);
+        setOnClickListener(listener);
+    }
+}
diff --git a/services/core/java/com/android/server/InputMethodManagerService.java b/services/core/java/com/android/server/InputMethodManagerService.java
index b6b3c43..e6f6e57 100644
--- a/services/core/java/com/android/server/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/InputMethodManagerService.java
@@ -149,11 +149,15 @@
 import java.lang.annotation.Retention;
 import java.nio.charset.StandardCharsets;
 import java.security.InvalidParameterException;
+import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.WeakHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * This class provides a system service that manages input methods.
@@ -525,20 +529,189 @@
      * </p>
      */
     private static class StartInputInfo {
+        private static final AtomicInteger sSequenceNumber = new AtomicInteger(0);
+
+        final int mSequenceNumber;
+        final long mTimestamp;
+        final long mWallTime;
         @NonNull
         final IBinder mImeToken;
+        @NonNull
+        final String mImeId;
+        // @InputMethodClient.StartInputReason
+        final int mStartInputReason;
+        final boolean mRestarting;
         @Nullable
         final IBinder mTargetWindow;
+        @NonNull
+        final EditorInfo mEditorInfo;
+        final int mTargetWindowSoftInputMode;
+        final int mClientBindSequenceNumber;
 
-        StartInputInfo(@NonNull IBinder imeToken, @Nullable IBinder targetWindow) {
+        StartInputInfo(@NonNull IBinder imeToken, @NonNull String imeId,
+                /* @InputMethodClient.StartInputReason */ int startInputReason, boolean restarting,
+                @Nullable IBinder targetWindow, @NonNull EditorInfo editorInfo,
+                int targetWindowSoftInputMode, int clientBindSequenceNumber) {
+            mSequenceNumber = sSequenceNumber.getAndIncrement();
+            mTimestamp = SystemClock.uptimeMillis();
+            mWallTime = System.currentTimeMillis();
             mImeToken = imeToken;
+            mImeId = imeId;
+            mStartInputReason = startInputReason;
+            mRestarting = restarting;
             mTargetWindow = targetWindow;
+            mEditorInfo = editorInfo;
+            mTargetWindowSoftInputMode = targetWindowSoftInputMode;
+            mClientBindSequenceNumber = clientBindSequenceNumber;
         }
     }
 
     @GuardedBy("mMethodMap")
     private final WeakHashMap<IBinder, StartInputInfo> mStartInputMap = new WeakHashMap<>();
 
+    /**
+     * A ring buffer to store the history of {@link StartInputInfo}.
+     */
+    private static final class StartInputHistory {
+        /**
+         * Entry size for non low-RAM devices.
+         *
+         * <p>TODO: Consider to follow what other system services have been doing to manage
+         * constants (e.g. {@link android.provider.Settings.Global#ACTIVITY_MANAGER_CONSTANTS}).</p>
+         */
+        private final static int ENTRY_SIZE_FOR_HIGH_RAM_DEVICE = 16;
+
+        /**
+         * Entry size for non low-RAM devices.
+         *
+         * <p>TODO: Consider to follow what other system services have been doing to manage
+         * constants (e.g. {@link android.provider.Settings.Global#ACTIVITY_MANAGER_CONSTANTS}).</p>
+         */
+        private final static int ENTRY_SIZE_FOR_LOW_RAM_DEVICE = 5;
+
+        private static int getEntrySize() {
+            if (ActivityManager.isLowRamDeviceStatic()) {
+                return ENTRY_SIZE_FOR_LOW_RAM_DEVICE;
+            } else {
+                return ENTRY_SIZE_FOR_HIGH_RAM_DEVICE;
+            }
+        }
+
+        /**
+         * Backing store for the ring bugger.
+         */
+        private final Entry[] mEntries = new Entry[getEntrySize()];
+
+        /**
+         * An index of {@link #mEntries}, to which next {@link #addEntry(StartInputInfo)} should
+         * write.
+         */
+        private int mNextIndex = 0;
+
+        /**
+         * Recyclable entry to store the information in {@link StartInputInfo}.
+         */
+        private static final class Entry {
+            int mSequenceNumber;
+            long mTimestamp;
+            long mWallTime;
+            @NonNull
+            String mImeTokenString;
+            @NonNull
+            String mImeId;
+            /* @InputMethodClient.StartInputReason */
+            int mStartInputReason;
+            boolean mRestarting;
+            @NonNull
+            String mTargetWindowString;
+            @NonNull
+            EditorInfo mEditorInfo;
+            int mTargetWindowSoftInputMode;
+            int mClientBindSequenceNumber;
+
+            Entry(@NonNull StartInputInfo original) {
+                set(original);
+            }
+
+            void set(@NonNull StartInputInfo original) {
+                mSequenceNumber = original.mSequenceNumber;
+                mTimestamp = original.mTimestamp;
+                mWallTime = original.mWallTime;
+                // Intentionally convert to String so as not to keep a strong reference to a Binder
+                // object.
+                mImeTokenString = String.valueOf(original.mImeToken);
+                mImeId = original.mImeId;
+                mStartInputReason = original.mStartInputReason;
+                mRestarting = original.mRestarting;
+                // Intentionally convert to String so as not to keep a strong reference to a Binder
+                // object.
+                mTargetWindowString = String.valueOf(original.mTargetWindow);
+                mEditorInfo = original.mEditorInfo;
+                mTargetWindowSoftInputMode = original.mTargetWindowSoftInputMode;
+                mClientBindSequenceNumber = original.mClientBindSequenceNumber;
+            }
+        }
+
+        /**
+         * Add a new entry and discard the oldest entry as needed.
+         * @param info {@lin StartInputInfo} to be added.
+         */
+        void addEntry(@NonNull StartInputInfo info) {
+            final int index = mNextIndex;
+            if (mEntries[index] == null) {
+                mEntries[index] = new Entry(info);
+            } else {
+                mEntries[index].set(info);
+            }
+            mNextIndex = (mNextIndex + 1) % mEntries.length;
+        }
+
+        void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
+            final SimpleDateFormat dataFormat =
+                    new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US);
+
+            for (int i = 0; i < mEntries.length; ++i) {
+                final Entry entry = mEntries[(i + mNextIndex) % mEntries.length];
+                if (entry == null) {
+                    continue;
+                }
+                pw.print(prefix);
+                pw.println("StartInput #" + entry.mSequenceNumber + ":");
+
+                pw.print(prefix);
+                pw.println(" time=" + dataFormat.format(new Date(entry.mWallTime))
+                        + " (timestamp=" + entry.mTimestamp + ")"
+                        + " reason="
+                        + InputMethodClient.getStartInputReason(entry.mStartInputReason)
+                        + " restarting=" + entry.mRestarting);
+
+                pw.print(prefix);
+                pw.println(" imeToken=" + entry.mImeTokenString + " [" + entry.mImeId + "]");
+
+                pw.print(prefix);
+                pw.println(" targetWin=" + entry.mTargetWindowString
+                        + " [" + entry.mEditorInfo.packageName + "]"
+                        + " clientBindSeq=" + entry.mClientBindSequenceNumber);
+
+                pw.print(prefix);
+                pw.println(" softInputMode=" + InputMethodClient.softInputModeToString(
+                                entry.mTargetWindowSoftInputMode));
+
+                pw.print(prefix);
+                pw.println(" inputType=0x" + Integer.toHexString(entry.mEditorInfo.inputType)
+                        + " imeOptions=0x" + Integer.toHexString(entry.mEditorInfo.imeOptions)
+                        + " fieldId=0x" + Integer.toHexString(entry.mEditorInfo.fieldId)
+                        + " fieldName=" + entry.mEditorInfo.fieldName
+                        + " actionId=" + entry.mEditorInfo.actionId
+                        + " actionLabel=" + entry.mEditorInfo.actionLabel);
+            }
+        }
+    }
+
+    @GuardedBy("mMethodMap")
+    @NonNull
+    private final StartInputHistory mStartInputHistory = new StartInputHistory();
+
     class SettingsObserver extends ContentObserver {
         int mUserId;
         boolean mRegistered = false;
@@ -728,7 +901,8 @@
             mPackagesToMonitorComponentChange.add(packageName);
         }
 
-        private boolean isChangingPackagesOfCurrentUser() {
+        @GuardedBy("mMethodMap")
+        private boolean isChangingPackagesOfCurrentUserLocked() {
             final int userId = getChangingUserId();
             final boolean retval = userId == mSettings.getCurrentUserId();
             if (DEBUG) {
@@ -741,10 +915,10 @@
 
         @Override
         public boolean onHandleForceStop(Intent intent, String[] packages, int uid, boolean doit) {
-            if (!isChangingPackagesOfCurrentUser()) {
-                return false;
-            }
             synchronized (mMethodMap) {
+                if (!isChangingPackagesOfCurrentUserLocked()) {
+                    return false;
+                }
                 String curInputMethodId = mSettings.getSelectedInputMethod();
                 final int N = mMethodList.size();
                 if (curInputMethodId != null) {
@@ -778,10 +952,10 @@
 
         @Override
         public void onSomePackagesChanged() {
-            if (!isChangingPackagesOfCurrentUser()) {
-                return;
-            }
             synchronized (mMethodMap) {
+                if (!isChangingPackagesOfCurrentUserLocked()) {
+                    return;
+                }
                 InputMethodInfo curIm = null;
                 String curInputMethodId = mSettings.getSelectedInputMethod();
                 final int N = mMethodList.size();
@@ -1380,8 +1554,11 @@
         }
 
         final Binder startInputToken = new Binder();
-        final StartInputInfo info = new StartInputInfo(mCurToken, mCurFocusedWindow);
+        final StartInputInfo info = new StartInputInfo(mCurToken, mCurId, startInputReason,
+                !initial, mCurFocusedWindow, mCurAttribute, mCurFocusedWindowSoftInputMode,
+                mCurSeq);
         mStartInputMap.put(startInputToken, info);
+        mStartInputHistory.addEntry(info);
 
         final SessionState session = mCurClient.curSession;
         executeOrSendMessage(session.method, mCaller.obtainMessageIIOOOO(
@@ -4156,6 +4333,9 @@
             mSwitchingController.dump(p);
             p.println("  mSettings:");
             mSettings.dumpLocked(p, "    ");
+
+            p.println("  mStartInputHistory:");
+            mStartInputHistory.dump(pw, "   ");
         }
 
         p.println(" ");
diff --git a/services/core/java/com/android/server/LockSettingsService.java b/services/core/java/com/android/server/LockSettingsService.java
index a073d8e..4a44530 100644
--- a/services/core/java/com/android/server/LockSettingsService.java
+++ b/services/core/java/com/android/server/LockSettingsService.java
@@ -20,6 +20,8 @@
 import static android.Manifest.permission.READ_CONTACTS;
 import static android.content.Context.KEYGUARD_SERVICE;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT;
+import static com.android.internal.widget.LockPatternUtils.SYNTHETIC_PASSWORD_ENABLED_KEY;
+import static com.android.internal.widget.LockPatternUtils.SYNTHETIC_PASSWORD_HANDLE_KEY;
 
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
@@ -61,6 +63,7 @@
 import android.provider.Settings;
 import android.provider.Settings.Secure;
 import android.provider.Settings.SettingNotFoundException;
+import android.security.GateKeeper;
 import android.security.KeyStore;
 import android.security.keystore.AndroidKeyStoreProvider;
 import android.security.keystore.KeyProperties;
@@ -79,6 +82,8 @@
 import com.android.internal.widget.LockPatternUtils;
 import com.android.internal.widget.VerifyCredentialResponse;
 import com.android.server.LockSettingsStorage.CredentialHash;
+import com.android.server.SyntheticPasswordManager.AuthenticationResult;
+import com.android.server.SyntheticPasswordManager.AuthenticationToken;
 
 import libcore.util.HexEncoding;
 
@@ -86,6 +91,7 @@
 import java.io.FileDescriptor;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.PrintWriter;
 import java.nio.charset.StandardCharsets;
 import java.security.InvalidAlgorithmParameterException;
 import java.security.InvalidKeyException;
@@ -145,12 +151,14 @@
 
     private boolean mFirstCallToVold;
     protected IGateKeeperService mGateKeeperService;
+    private SyntheticPasswordManager mSpManager;
+
     /**
      * The UIDs that are used for system credential storage in keystore.
      */
     private static final int[] SYSTEM_CREDENTIAL_UIDS = {
             Process.WIFI_UID, Process.VPN_UID,
-            Process.ROOT_UID, Process.SYSTEM_UID };
+            Process.ROOT_UID };
 
     // This class manages life cycle events for encrypted users on File Based Encryption (FBE)
     // devices. The most basic of these is to show/hide notifications about missing features until
@@ -335,6 +343,14 @@
             }
             return null;
         }
+
+        public SyntheticPasswordManager getSyntheticPasswordManager(LockSettingsStorage storage) {
+            return new SyntheticPasswordManager(storage);
+        }
+
+        public int binderGetCallingUid() {
+            return Binder.getCallingUid();
+        }
     }
 
     public LockSettingsService(Context context) {
@@ -365,6 +381,8 @@
         mUserManager = injector.getUserManager();
         mStrongAuthTracker = injector.getStrongAuthTracker();
         mStrongAuthTracker.register(mStrongAuth);
+
+        mSpManager = injector.getSyntheticPasswordManager(mStorage);
     }
 
     /**
@@ -801,17 +819,42 @@
 
     @Override
     public boolean havePassword(int userId) throws RemoteException {
+        synchronized (mSpManager) {
+            if (isSyntheticPasswordBasedCredentialLocked(userId)) {
+                long handle = getSyntheticPasswordHandleLocked(userId);
+                return mSpManager.getCredentialType(handle, userId) ==
+                        LockPatternUtils.CREDENTIAL_TYPE_PASSWORD;
+            }
+        }
         // Do we need a permissions check here?
         return mStorage.hasPassword(userId);
     }
 
     @Override
     public boolean havePattern(int userId) throws RemoteException {
+        synchronized (mSpManager) {
+            if (isSyntheticPasswordBasedCredentialLocked(userId)) {
+                long handle = getSyntheticPasswordHandleLocked(userId);
+                return mSpManager.getCredentialType(handle, userId) ==
+                        LockPatternUtils.CREDENTIAL_TYPE_PATTERN;
+            }
+        }
         // Do we need a permissions check here?
         return mStorage.hasPattern(userId);
     }
 
     private boolean isUserSecure(int userId) {
+        synchronized (mSpManager) {
+            try {
+                if (isSyntheticPasswordBasedCredentialLocked(userId)) {
+                    long handle = getSyntheticPasswordHandleLocked(userId);
+                    return mSpManager.getCredentialType(handle, userId) !=
+                            LockPatternUtils.CREDENTIAL_TYPE_NONE;
+                }
+            } catch (RemoteException e) {
+                // fall through
+            }
+        }
         return mStorage.hasCredential(userId);
     }
 
@@ -1021,6 +1064,13 @@
 
     private void setLockCredentialInternal(String credential, int credentialType,
             String savedCredential, int userId) throws RemoteException {
+        synchronized (mSpManager) {
+            if (isSyntheticPasswordBasedCredentialLocked(userId)) {
+                spBasedSetLockCredentialInternalLocked(credential, credentialType, savedCredential,
+                        userId);
+                return;
+            }
+        }
         if (credentialType == LockPatternUtils.CREDENTIAL_TYPE_NONE) {
             if (credential != null) {
                 Slog.wtf(TAG, "CredentialType is none, but credential is non-null.");
@@ -1061,7 +1111,16 @@
                 savedCredential = null;
             }
         }
-
+        synchronized (mSpManager) {
+            if (shouldMigrateToSyntheticPasswordLocked(userId)) {
+                initializeSyntheticPasswordLocked(currentHandle.hash, savedCredential,
+                        currentHandle.type, userId);
+                spBasedSetLockCredentialInternalLocked(credential, credentialType, savedCredential,
+                        userId);
+                return;
+            }
+        }
+        if (DEBUG) Slog.d(TAG, "setLockCredentialInternal: user=" + userId);
         byte[] enrolledHandle = enrollCredential(currentHandle.hash, savedCredential, credential,
                 userId);
         if (enrolledHandle != null) {
@@ -1189,6 +1248,11 @@
         return hash;
     }
 
+    private void setAuthlessUserKeyProtection(int userId, byte[] key) throws RemoteException {
+        if (DEBUG) Slog.d(TAG, "setAuthlessUserKeyProtectiond: user=" + userId);
+        addUserKeyAuth(userId, null, key);
+    }
+
     private void setUserKeyProtection(int userId, String credential, VerifyCredentialResponse vcr)
             throws RemoteException {
         if (DEBUG) Slog.d(TAG, "setUserKeyProtection: user=" + userId);
@@ -1320,7 +1384,16 @@
         if (TextUtils.isEmpty(credential)) {
             throw new IllegalArgumentException("Credential can't be null or empty");
         }
-
+        synchronized (mSpManager) {
+            if (isSyntheticPasswordBasedCredentialLocked(userId)) {
+                VerifyCredentialResponse response = spBasedDoVerifyCredentialLocked(credential,
+                        credentialType, hasChallenge, challenge, userId, progressCallback);
+                if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) {
+                    mStrongAuth.reportSuccessfulStrongAuthUnlock(userId);
+                }
+                return response;
+            }
+        }
         CredentialHash storedHash = mStorage.readCredentialHash(userId);
         if (storedHash.type != credentialType) {
             Slog.wtf(TAG, "doVerifyCredential type mismatch with stored credential??"
@@ -1456,8 +1529,8 @@
             notifyActivePasswordMetricsAvailable(credential, userId);
             unlockKeystore(credential, userId);
 
-            Slog.i(TAG, "Unlocking user " + userId +
-                    " with token length " + response.getPayload().length);
+            Slog.i(TAG, "Unlocking user " + userId + " with token length "
+                    + response.getPayload().length);
             unlockUser(userId, response.getPayload(), secretFromCredential(credential));
 
             if (isManagedProfileWithSeparatedLock(userId)) {
@@ -1467,6 +1540,15 @@
             }
             if (shouldReEnroll) {
                 setLockCredentialInternal(credential, storedHash.type, credential, userId);
+            } else {
+                // Now that we've cleared of all required GK migration, let's do the final
+                // migration to synthetic password.
+                synchronized (mSpManager) {
+                    if (shouldMigrateToSyntheticPasswordLocked(userId)) {
+                        initializeSyntheticPasswordLocked(storedHash.hash, credential,
+                                storedHash.type, userId);
+                    }
+                }
             }
         } else if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_RETRY) {
             if (response.getTimeout() > 0) {
@@ -1697,7 +1779,7 @@
         }
     }
 
-    private synchronized IGateKeeperService getGateKeeperService()
+    protected synchronized IGateKeeperService getGateKeeperService()
             throws RemoteException {
         if (mGateKeeperService != null) {
             return mGateKeeperService;
@@ -1713,4 +1795,431 @@
         Slog.e(TAG, "Unable to acquire GateKeeperService");
         return null;
     }
+
+    /**
+     * Precondition: vold and keystore unlocked.
+     *
+     * Create new synthetic password, set up synthetic password blob protected by the supplied
+     * user credential, and make the newly-created SP blob active.
+     *
+     * The invariant under a synthetic password is:
+     * 1. If user credential exists, then both vold and keystore and protected with keys derived
+     *     from the synthetic password.
+     * 2. If user credential does not exist, vold and keystore protection are cleared. This is to
+     *     make it consistent with current behaviour. It also allows ActivityManager to call
+     *     unlockUser() with empty secret.
+     * 3. Once a user is migrated to have synthetic password, its value will never change, no matter
+     *     whether the user changes his lockscreen PIN or clear/reset it. When the user clears its
+     *     lockscreen PIN, we still maintain the existing synthetic password in a password blob
+     *     protected by a default PIN. The only exception is when the DPC performs an untrusted
+     *     credential change, in which case we have no way to derive the existing synthetic password
+     *     and has to create a new one.
+     * 4. The user SID is linked with synthetic password, but its cleared/re-created when the user
+     *     clears/re-creates his lockscreen PIN.
+     *
+     *
+     * Different cases of calling this method:
+     * 1. credentialHash != null
+     *     This implies credential != null, a new SP blob will be provisioned, and existing SID
+     *     migrated to associate with the new SP.
+     *     This happens during a normal migration case when the user currently has password.
+     *
+     * 2. credentialhash == null and credential == null
+     *     A new SP blob and a new SID will be created, while the user has no credentials.
+     *     This can happens when we are activating an escrow token on a unsecured device, during
+     *     which we want to create the SP structure with an empty user credential.
+     *
+     * 3. credentialhash == null and credential != null
+     *     This is the untrusted credential reset, OR the user sets a new lockscreen password
+     *     FOR THE FIRST TIME on a SP-enabled device. New credential and new SID will be created
+     */
+    private AuthenticationToken initializeSyntheticPasswordLocked(byte[] credentialHash,
+            String credential, int credentialType, int userId) throws RemoteException {
+        Slog.i(TAG, "Initialize SyntheticPassword for user: " + userId);
+        AuthenticationToken auth = mSpManager.newSyntheticPasswordAndSid(getGateKeeperService(),
+                credentialHash, credential, userId);
+        if (auth == null) {
+            Slog.wtf(TAG, "initializeSyntheticPasswordLocked returns null auth token");
+            return null;
+        }
+        long handle = mSpManager.createPasswordBasedSyntheticPassword(getGateKeeperService(),
+                credential, credentialType, auth, userId);
+        if (credential != null) {
+            if (credentialHash == null) {
+                // Since when initializing SP, we didn't provide an existing password handle
+                // for it to migrate SID, we need to create a new SID for the user.
+                mSpManager.newSidForUser(getGateKeeperService(), auth, userId);
+            }
+            mSpManager.verifyChallenge(getGateKeeperService(), auth, 0L, userId);
+            setAuthlessUserKeyProtection(userId, auth.deriveDiskEncryptionKey());
+            setKeystorePassword(auth.deriveKeyStorePassword(), userId);
+        } else {
+            clearUserKeyProtection(userId);
+            setKeystorePassword(null, userId);
+            getGateKeeperService().clearSecureUserId(userId);
+        }
+        fixateNewestUserKeyAuth(userId);
+        setLong(SYNTHETIC_PASSWORD_HANDLE_KEY, handle, userId);
+        return auth;
+    }
+
+    private long getSyntheticPasswordHandleLocked(int userId) {
+        try {
+            return getLong(SYNTHETIC_PASSWORD_HANDLE_KEY, 0, userId);
+        } catch (RemoteException e) {
+            return SyntheticPasswordManager.DEFAULT_HANDLE;
+        }
+    }
+
+    private boolean isSyntheticPasswordBasedCredentialLocked(int userId) throws RemoteException {
+        long handle = getSyntheticPasswordHandleLocked(userId);
+        // This is a global setting
+        long enabled = getLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 0, UserHandle.USER_SYSTEM);
+      return enabled != 0 && handle != SyntheticPasswordManager.DEFAULT_HANDLE;
+    }
+
+    private boolean shouldMigrateToSyntheticPasswordLocked(int userId) throws RemoteException {
+        long handle = getSyntheticPasswordHandleLocked(userId);
+        // This is a global setting
+        long enabled = getLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 0, UserHandle.USER_SYSTEM);
+        return enabled != 0 && handle == SyntheticPasswordManager.DEFAULT_HANDLE;
+    }
+
+    private void enableSyntheticPasswordLocked() throws RemoteException {
+        setLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 1, UserHandle.USER_SYSTEM);
+    }
+
+    private VerifyCredentialResponse spBasedDoVerifyCredentialLocked(String userCredential, int
+            credentialType, boolean hasChallenge, long challenge, int userId,
+            ICheckCredentialProgressCallback progressCallback) throws RemoteException {
+        if (DEBUG) Slog.d(TAG, "spBasedDoVerifyCredentialLocked: user=" + userId);
+        if (credentialType == LockPatternUtils.CREDENTIAL_TYPE_NONE) {
+            userCredential = null;
+        }
+        long handle = getSyntheticPasswordHandleLocked(userId);
+        AuthenticationResult authResult = mSpManager.unwrapPasswordBasedSyntheticPassword(
+                getGateKeeperService(), handle, userCredential, userId);
+
+        VerifyCredentialResponse response = authResult.gkResponse;
+        if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) {
+            // credential has matched
+            // perform verifyChallenge with synthetic password which generates the real auth
+            // token for the current user
+            response = mSpManager.verifyChallenge(getGateKeeperService(), authResult.authToken,
+                    challenge, userId);
+            if (response.getResponseCode() != VerifyCredentialResponse.RESPONSE_OK) {
+                Slog.wtf(TAG, "verifyChallenge with SP failed.");
+                return VerifyCredentialResponse.ERROR;
+            }
+            if (progressCallback != null) {
+                progressCallback.onCredentialVerified();
+            }
+            notifyActivePasswordMetricsAvailable(userCredential, userId);
+            unlockKeystore(authResult.authToken.deriveKeyStorePassword(), userId);
+
+            final byte[] secret = authResult.authToken.deriveDiskEncryptionKey();
+            Slog.i(TAG, "Unlocking user " + userId + " with secret only, length " + secret.length);
+            unlockUser(userId, null, secret);
+
+            if (isManagedProfileWithSeparatedLock(userId)) {
+                TrustManager trustManager =
+                        (TrustManager) mContext.getSystemService(Context.TRUST_SERVICE);
+                trustManager.setDeviceLockedForUser(userId, false);
+            }
+            activateEscrowTokens(authResult.authToken, userId);
+        } else if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_RETRY) {
+            if (response.getTimeout() > 0) {
+                requireStrongAuth(STRONG_AUTH_REQUIRED_AFTER_LOCKOUT, userId);
+            }
+        }
+
+        return response;
+    }
+
+    /**
+     * Change the user's lockscreen password by creating a new SP blob and update the handle, based
+     * on an existing authentication token. Even though a new SP blob is created, the underlying
+     * synthetic password is never changed.
+     *
+     * When clearing credential, we keep the SP unchanged, but clear its password handle so its
+     * SID is gone. We also clear password from (software-based) keystore and vold, which will be
+     * added back when new password is set in future.
+     */
+    private long setLockCredentialWithAuthTokenLocked(String credential, int credentialType,
+            AuthenticationToken auth, int userId) throws RemoteException {
+        if (DEBUG) Slog.d(TAG, "setLockCredentialWithAuthTokenLocked: user=" + userId);
+        long newHandle = mSpManager.createPasswordBasedSyntheticPassword(getGateKeeperService(),
+                credential, credentialType, auth, userId);
+        final Map<Integer, String> profilePasswords;
+        if (credential != null) {
+            // // not needed by synchronizeUnifiedWorkChallengeForProfiles()
+            profilePasswords = null;
+
+            if (mSpManager.hasSidForUser(userId)) {
+                // We are changing password of a secured device, nothing more needed as
+                // createPasswordBasedSyntheticPassword has already taken care of maintaining
+                // the password handle and SID unchanged.
+
+                //refresh auth token
+                mSpManager.verifyChallenge(getGateKeeperService(), auth, 0L, userId);
+            } else {
+                // A new password is set on a previously-unsecured device, we need to generate
+                // a new SID, and re-add keys to vold and keystore.
+                mSpManager.newSidForUser(getGateKeeperService(), auth, userId);
+                mSpManager.verifyChallenge(getGateKeeperService(), auth, 0L, userId);
+                setAuthlessUserKeyProtection(userId, auth.deriveDiskEncryptionKey());
+                fixateNewestUserKeyAuth(userId);
+                setKeystorePassword(auth.deriveKeyStorePassword(), userId);
+            }
+        } else {
+            // Cache all profile password if they use unified work challenge. This will later be
+            // used to clear the profile's password in synchronizeUnifiedWorkChallengeForProfiles()
+            profilePasswords = getDecryptedPasswordsForAllTiedProfiles(userId);
+
+            // we are clearing password of a secured device, so need to nuke SID as well.
+            mSpManager.clearSidForUser(userId);
+            getGateKeeperService().clearSecureUserId(userId);
+            // Clear key from vold so ActivityManager can just unlock the user with empty secret
+            // during boot.
+            clearUserKeyProtection(userId);
+            fixateNewestUserKeyAuth(userId);
+            setKeystorePassword(null, userId);
+        }
+        setLong(SYNTHETIC_PASSWORD_HANDLE_KEY, newHandle, userId);
+        synchronizeUnifiedWorkChallengeForProfiles(userId, profilePasswords);
+        return newHandle;
+    }
+
+    private void spBasedSetLockCredentialInternalLocked(String credential, int credentialType,
+            String savedCredential, int userId) throws RemoteException {
+        if (DEBUG) Slog.d(TAG, "spBasedSetLockCredentialInternalLocked: user=" + userId);
+        if (isManagedProfileWithUnifiedLock(userId)) {
+            // get credential from keystore when managed profile has unified lock
+            try {
+                savedCredential = getDecryptedPasswordForTiedProfile(userId);
+            } catch (FileNotFoundException e) {
+                Slog.i(TAG, "Child profile key not found");
+            } catch (UnrecoverableKeyException | InvalidKeyException | KeyStoreException
+                    | NoSuchAlgorithmException | NoSuchPaddingException
+                    | InvalidAlgorithmParameterException | IllegalBlockSizeException
+                    | BadPaddingException | CertificateException | IOException e) {
+                Slog.e(TAG, "Failed to decrypt child profile key", e);
+            }
+        }
+        long handle = getSyntheticPasswordHandleLocked(userId);
+        AuthenticationToken auth = mSpManager.unwrapPasswordBasedSyntheticPassword(
+                getGateKeeperService(), handle, savedCredential, userId).authToken;
+        if (auth != null) {
+            // We are performing a trusted credential change i.e. a correct existing credential
+            // is provided
+            setLockCredentialWithAuthTokenLocked(credential, credentialType, auth, userId);
+            mSpManager.destroyPasswordBasedSyntheticPassword(handle, userId);
+        } else {
+            // We are performing an untrusted credential change i.e. by DevicePolicyManager.
+            // So provision a new SP and SID. This would invalidate existing escrow tokens.
+            // Still support this for now but this flow will be removed in the next release.
+
+            Slog.w(TAG, "Untrusted credential change invoked");
+            initializeSyntheticPasswordLocked(null, credential, credentialType, userId);
+            synchronizeUnifiedWorkChallengeForProfiles(userId, null);
+            mSpManager.destroyPasswordBasedSyntheticPassword(handle, userId);
+        }
+        notifyActivePasswordMetricsAvailable(credential, userId);
+
+    }
+
+    @Override
+    public long addEscrowToken(byte[] token, int userId) throws RemoteException {
+        ensureCallerSystemUid();
+        if (DEBUG) Slog.d(TAG, "addEscrowToken: user=" + userId);
+        synchronized (mSpManager) {
+            enableSyntheticPasswordLocked();
+            // Migrate to synthetic password based credentials if ther user has no password,
+            // the token can then be activated immediately.
+            AuthenticationToken auth = null;
+            if (!isUserSecure(userId)) {
+                if (shouldMigrateToSyntheticPasswordLocked(userId)) {
+                    auth = initializeSyntheticPasswordLocked(null, null,
+                            LockPatternUtils.CREDENTIAL_TYPE_NONE, userId);
+                } else /* isSyntheticPasswordBasedCredentialLocked(userId) */ {
+                    long pwdHandle = getSyntheticPasswordHandleLocked(userId);
+                    auth = mSpManager.unwrapPasswordBasedSyntheticPassword(getGateKeeperService(),
+                            pwdHandle, null, userId).authToken;
+                }
+            }
+            disableEscrowTokenOnNonManagedDevicesIfNeeded(userId);
+            if (!mSpManager.hasEscrowData(userId)) {
+                throw new SecurityException("Escrow token is disabled on the current user");
+            }
+            long handle = mSpManager.createTokenBasedSyntheticPassword(token, userId);
+            if (auth != null) {
+                mSpManager.activateTokenBasedSyntheticPassword(handle, auth, userId);
+            }
+            return handle;
+        }
+    }
+
+    private void activateEscrowTokens(AuthenticationToken auth, int userId) throws RemoteException {
+        if (DEBUG) Slog.d(TAG, "activateEscrowTokens: user=" + userId);
+        synchronized (mSpManager) {
+            for (long handle : mSpManager.getPendingTokensForUser(userId)) {
+                Slog.i(TAG, String.format("activateEscrowTokens: %x %d ", handle, userId));
+                mSpManager.activateTokenBasedSyntheticPassword(handle, auth, userId);
+            }
+        }
+    }
+
+    @Override
+    public boolean isEscrowTokenActive(long handle, int userId) throws RemoteException {
+        ensureCallerSystemUid();
+        synchronized (mSpManager) {
+            return mSpManager.existsHandle(handle, userId);
+        }
+    }
+
+    @Override
+    public boolean removeEscrowToken(long handle, int userId) throws RemoteException {
+        ensureCallerSystemUid();
+        synchronized (mSpManager) {
+            if (handle == getSyntheticPasswordHandleLocked(userId)) {
+                Slog.w(TAG, "Cannot remove password handle");
+                return false;
+            }
+            if (mSpManager.removePendingToken(handle, userId)) {
+                return true;
+            }
+            if (mSpManager.existsHandle(handle, userId)) {
+                mSpManager.destroyTokenBasedSyntheticPassword(handle, userId);
+                return true;
+            } else {
+                return false;
+            }
+        }
+    }
+
+    @Override
+    public boolean setLockCredentialWithToken(String credential, int type, long tokenHandle,
+            byte[] token, int userId) throws RemoteException {
+        ensureCallerSystemUid();
+        boolean result;
+        synchronized (mSpManager) {
+            if (!mSpManager.hasEscrowData(userId)) {
+                throw new SecurityException("Escrow token is disabled on the current user");
+            }
+            result = setLockCredentialWithTokenInternal(credential, type, tokenHandle, token,
+                    userId);
+        }
+        if (result) {
+            synchronized (mSeparateChallengeLock) {
+                setSeparateProfileChallengeEnabled(userId, true, null);
+            }
+            notifyPasswordChanged(userId);
+        }
+        return result;
+    }
+
+    private boolean setLockCredentialWithTokenInternal(String credential, int type,
+            long tokenHandle, byte[] token, int userId) throws RemoteException {
+        synchronized (mSpManager) {
+            AuthenticationResult result = mSpManager.unwrapTokenBasedSyntheticPassword(
+                    getGateKeeperService(), tokenHandle, token, userId);
+            if (result.authToken == null) {
+                Slog.w(TAG, "Invalid escrow token supplied");
+                return false;
+            }
+            long oldHandle = getSyntheticPasswordHandleLocked(userId);
+            setLockCredentialWithAuthTokenLocked(credential, type, result.authToken, userId);
+            mSpManager.destroyPasswordBasedSyntheticPassword(oldHandle, userId);
+            return true;
+        }
+    }
+
+    @Override
+    public void unlockUserWithToken(long tokenHandle, byte[] token, int userId)
+            throws RemoteException {
+        ensureCallerSystemUid();
+        AuthenticationResult authResult;
+        synchronized (mSpManager) {
+            if (!mSpManager.hasEscrowData(userId)) {
+                throw new SecurityException("Escrow token is disabled on the current user");
+            }
+            authResult = mSpManager.unwrapTokenBasedSyntheticPassword(getGateKeeperService(),
+                    tokenHandle, token, userId);
+            if (authResult.authToken == null) {
+                Slog.w(TAG, "Invalid escrow token supplied");
+                return;
+            }
+        }
+        unlockUser(userId, null, authResult.authToken.deriveDiskEncryptionKey());
+    }
+
+    @Override
+    protected void dump(FileDescriptor fd, PrintWriter pw, String[] args){
+        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
+                != PackageManager.PERMISSION_GRANTED) {
+
+            pw.println("Permission Denial: can't dump LockSettingsService from from pid="
+                    + Binder.getCallingPid()
+                    + ", uid=" + Binder.getCallingUid());
+            return;
+        }
+
+        synchronized (this) {
+            pw.println("Current lock settings service state:");
+            pw.println(String.format("SP Enabled = %b",
+                    mLockPatternUtils.isSyntheticPasswordEnabled()));
+
+            List<UserInfo> users = mUserManager.getUsers();
+            for (int user = 0; user < users.size(); user++) {
+                final int userId = users.get(user).id;
+                pw.println("    User " + userId);
+                pw.println(String.format("        SP Handle = %x",
+                        getSyntheticPasswordHandleLocked(userId)));
+                try {
+                    pw.println(String.format("        SID = %x",
+                            getGateKeeperService().getSecureUserId(userId)));
+                } catch (RemoteException e) {
+                    // ignore.
+                }
+            }
+        }
+    }
+
+    private void disableEscrowTokenOnNonManagedDevicesIfNeeded(int userId) {
+        long ident = Binder.clearCallingIdentity();
+        try {
+            // Managed profile should have escrow enabled
+            if (mUserManager.getUserInfo(userId).isManagedProfile()) {
+                return;
+            }
+            DevicePolicyManager dpm = (DevicePolicyManager)
+                    mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
+            // Devices with Device Owner should have escrow enabled on all users.
+            if (dpm.getDeviceOwnerComponentOnAnyUser() != null) {
+                return;
+            }
+            // If the device is yet to be provisioned (still in SUW), there is still
+            // a chance that Device Owner will be set on the device later, so postpone
+            // disabling escrow token for now.
+            if (!dpm.isDeviceProvisioned()) {
+                return;
+            }
+            // Disable escrow token permanently on all other device/user types.
+            Slog.i(TAG, "Disabling escrow token on user " + userId);
+            if (isSyntheticPasswordBasedCredentialLocked(userId)) {
+                mSpManager.destroyEscrowData(userId);
+            }
+        } catch (RemoteException e) {
+            Slog.e(TAG, "disableEscrowTokenOnNonManagedDevices", e);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    private void ensureCallerSystemUid() throws SecurityException {
+        final int callingUid = mInjector.binderGetCallingUid();
+        if (callingUid != Process.SYSTEM_UID) {
+            throw new SecurityException("Only system can call this API.");
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/LockSettingsShellCommand.java b/services/core/java/com/android/server/LockSettingsShellCommand.java
index 1ab5303..91bd98e 100644
--- a/services/core/java/com/android/server/LockSettingsShellCommand.java
+++ b/services/core/java/com/android/server/LockSettingsShellCommand.java
@@ -22,12 +22,9 @@
 
 import android.app.ActivityManager;
 import android.content.Context;
-import android.os.Binder;
-import android.os.Process;
 import android.os.RemoteException;
 import android.os.ShellCommand;
 
-import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.internal.widget.LockPatternUtils.RequestThrottledException;
 
@@ -37,6 +34,7 @@
     private static final String COMMAND_SET_PIN = "set-pin";
     private static final String COMMAND_SET_PASSWORD = "set-password";
     private static final String COMMAND_CLEAR = "clear";
+    private static final String COMMAND_SP = "sp";
 
     private int mCurrentUserId;
     private final LockPatternUtils mLockPatternUtils;
@@ -71,6 +69,9 @@
                 case COMMAND_CLEAR:
                     runClear();
                     break;
+                case COMMAND_SP:
+                    runEnableSp();
+                    break;
                 default:
                     getErrPrintWriter().println("Unknown command: " + cmd);
                     break;
@@ -92,6 +93,8 @@
         while ((opt = getNextOption()) != null) {
             if ("--old".equals(opt)) {
                 mOld = getNextArgRequired();
+            } else if ("--user".equals(opt)) {
+                mCurrentUserId = Integer.parseInt(getNextArgRequired());
             } else {
                 getErrPrintWriter().println("Unknown option: " + opt);
                 throw new IllegalArgumentException();
@@ -100,6 +103,15 @@
         mNew = getNextArg();
     }
 
+    private void runEnableSp() {
+        if (mNew != null) {
+            mLockPatternUtils.enableSyntheticPassword();
+            getOutPrintWriter().println("Synthetic password enabled");
+        }
+        getOutPrintWriter().println(String.format("SP Enabled = %b",
+                mLockPatternUtils.isSyntheticPasswordEnabled()));
+    }
+
     private void runSetPattern() throws RemoteException {
         mLockPatternUtils.saveLockPattern(stringToPattern(mNew), mOld, mCurrentUserId);
         getOutPrintWriter().println("Pattern set to '" + mNew + "'");
diff --git a/services/core/java/com/android/server/LockSettingsStorage.java b/services/core/java/com/android/server/LockSettingsStorage.java
index 385b1cf..f5bae7c 100644
--- a/services/core/java/com/android/server/LockSettingsStorage.java
+++ b/services/core/java/com/android/server/LockSettingsStorage.java
@@ -66,6 +66,8 @@
     private static final String LEGACY_LOCK_PASSWORD_FILE = "password.key";
     private static final String CHILD_PROFILE_LOCK_FILE = "gatekeeper.profile.key";
 
+    private static final String SYNTHETIC_PASSWORD_DIRECTORY = "spblob/";
+
     private static final Object DEFAULT = new Object();
 
     private final DatabaseHelper mOpenHelper;
@@ -412,8 +414,7 @@
     }
 
     private String getLockCredentialFilePathForUser(int userId, String basename) {
-        String dataSystemDirectory =
-                android.os.Environment.getDataDirectory().getAbsolutePath() +
+        String dataSystemDirectory = Environment.getDataDirectory().getAbsolutePath() +
                         SYSTEM_DIRECTORY;
         if (userId == 0) {
             // Leave it in the same place for user 0
@@ -423,6 +424,40 @@
         }
     }
 
+    public void writeSyntheticPasswordState(int userId, long handle, String name, byte[] data) {
+        writeFile(getSynthenticPasswordStateFilePathForUser(userId, handle, name), data);
+    }
+
+    public byte[] readSyntheticPasswordState(int userId, long handle, String name) {
+        return readFile(getSynthenticPasswordStateFilePathForUser(userId, handle, name));
+    }
+
+    public void deleteSyntheticPasswordState(int userId, long handle, String name, boolean secure) {
+        String path = getSynthenticPasswordStateFilePathForUser(userId, handle, name);
+        File file = new File(path);
+        if (file.exists()) {
+            //TODO: (b/34600579) invoke secdiscardable
+            file.delete();
+            mCache.putFile(path, null);
+        }
+    }
+
+    @VisibleForTesting
+    protected File getSyntheticPasswordDirectoryForUser(int userId) {
+        return new File(Environment.getDataSystemDeDirectory(userId) ,SYNTHETIC_PASSWORD_DIRECTORY);
+    }
+
+    @VisibleForTesting
+    protected String getSynthenticPasswordStateFilePathForUser(int userId, long handle,
+            String name) {
+        File baseDir = getSyntheticPasswordDirectoryForUser(userId);
+        String baseName = String.format("%016x.%s", handle, name);
+        if (!baseDir.exists()) {
+            baseDir.mkdir();
+        }
+        return new File(baseDir, baseName).getAbsolutePath();
+    }
+
     public void removeUser(int userId) {
         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
 
@@ -446,15 +481,20 @@
                 }
             }
         } else {
-            // Manged profile
+            // Managed profile
             removeChildProfileLock(userId);
         }
 
+        File spStateDir = getSyntheticPasswordDirectoryForUser(userId);
         try {
             db.beginTransaction();
             db.delete(TABLE, COLUMN_USERID + "='" + userId + "'", null);
             db.setTransactionSuccessful();
             mCache.removeUser(userId);
+            // The directory itself will be deleted as part of user deletion operation by the
+            // framework, so only need to purge cache here.
+            //TODO: (b/34600579) invoke secdiscardable
+            mCache.purgePath(spStateDir.getAbsolutePath());
         } finally {
             db.endTransaction();
         }
@@ -619,6 +659,16 @@
             mVersion++;
         }
 
+        synchronized void purgePath(String path) {
+            for (int i = mCache.size() - 1; i >= 0; i--) {
+                CacheKey entry = mCache.keyAt(i);
+                if (entry.type == CacheKey.TYPE_FILE && entry.key.startsWith(path)) {
+                    mCache.removeAt(i);
+                }
+            }
+            mVersion++;
+        }
+
         synchronized void clear() {
             mCache.clear();
             mVersion++;
diff --git a/services/core/java/com/android/server/NetworkScoreService.java b/services/core/java/com/android/server/NetworkScoreService.java
index 3e89852..b33538cb 100644
--- a/services/core/java/com/android/server/NetworkScoreService.java
+++ b/services/core/java/com/android/server/NetworkScoreService.java
@@ -603,7 +603,10 @@
             mScanResultKeys = new ArraySet<>(size);
             for (int i = 0; i < size; i++) {
                 ScanResult scanResult = scanResults.get(i);
-                mScanResultKeys.add(NetworkKey.createFromScanResult(scanResult));
+                NetworkKey key = NetworkKey.createFromScanResult(scanResult);
+                if (key != null) {
+                    mScanResultKeys.add(key);
+                }
             }
         }
 
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index 32b7e4d..0415971 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -2922,10 +2922,10 @@
         waitForReady();
 
         if (StorageManager.isFileEncryptedNativeOrEmulated()) {
-            // When a user has secure lock screen, require a challenge token to
-            // actually unlock. This check is mostly in place for emulation mode.
-            if (mLockPatternUtils.isSecure(userId) && ArrayUtils.isEmpty(token)) {
-                throw new IllegalStateException("Token required to unlock secure user " + userId);
+            // When a user has secure lock screen, require secret to actually unlock.
+            // This check is mostly in place for emulation mode.
+            if (mLockPatternUtils.isSecure(userId) && ArrayUtils.isEmpty(secret)) {
+                throw new IllegalStateException("Secret required to unlock secure user " + userId);
             }
 
             try {
diff --git a/services/core/java/com/android/server/SyntheticPasswordCrypto.java b/services/core/java/com/android/server/SyntheticPasswordCrypto.java
new file mode 100644
index 0000000..12d91c5
--- /dev/null
+++ b/services/core/java/com/android/server/SyntheticPasswordCrypto.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import android.security.keystore.KeyProperties;
+import android.security.keystore.KeyProtection;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.util.Arrays;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.KeyGenerator;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+public class SyntheticPasswordCrypto {
+    private static final int PROFILE_KEY_IV_SIZE = 12;
+    private static final int AES_KEY_LENGTH = 32; // 256-bit AES key
+    private static final byte[] APPLICATION_ID_PERSONALIZATION = "application-id".getBytes();
+    // Time between the user credential is verified with GK and the decryption of synthetic password
+    // under the auth-bound key. This should always happen one after the other, but give it 15
+    // seconds just to be sure.
+    private static final int USER_AUTHENTICATION_VALIDITY = 15;
+
+    private static byte[] decrypt(SecretKey key, byte[] blob)
+            throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
+            InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
+        if (blob == null) {
+            return null;
+        }
+        byte[] iv = Arrays.copyOfRange(blob, 0, PROFILE_KEY_IV_SIZE);
+        byte[] ciphertext = Arrays.copyOfRange(blob, PROFILE_KEY_IV_SIZE, blob.length);
+        Cipher cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
+                + KeyProperties.BLOCK_MODE_GCM + "/" + KeyProperties.ENCRYPTION_PADDING_NONE);
+        cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv));
+        return cipher.doFinal(ciphertext);
+    }
+
+    private static byte[] encrypt(SecretKey key, byte[] blob)
+            throws IOException, NoSuchAlgorithmException, NoSuchPaddingException,
+            InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
+        if (blob == null) {
+            return null;
+        }
+        Cipher cipher = Cipher.getInstance(
+                KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_GCM + "/"
+                        + KeyProperties.ENCRYPTION_PADDING_NONE);
+        cipher.init(Cipher.ENCRYPT_MODE, key);
+        byte[] ciphertext = cipher.doFinal(blob);
+        byte[] iv = cipher.getIV();
+        if (iv.length != PROFILE_KEY_IV_SIZE) {
+            throw new RuntimeException("Invalid iv length: " + iv.length);
+        }
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        outputStream.write(iv);
+        outputStream.write(ciphertext);
+        return outputStream.toByteArray();
+    }
+
+    public static byte[] encrypt(byte[] keyBytes, byte[] personalisation, byte[] message) {
+        byte[] keyHash = personalisedHash(personalisation, keyBytes);
+        SecretKeySpec key = new SecretKeySpec(Arrays.copyOf(keyHash, AES_KEY_LENGTH),
+                KeyProperties.KEY_ALGORITHM_AES);
+        try {
+            return encrypt(key, message);
+        } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException
+                | IllegalBlockSizeException | BadPaddingException | IOException e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    public static byte[] decrypt(byte[] keyBytes, byte[] personalisation, byte[] ciphertext) {
+        byte[] keyHash = personalisedHash(personalisation, keyBytes);
+        SecretKeySpec key = new SecretKeySpec(Arrays.copyOf(keyHash, AES_KEY_LENGTH),
+                KeyProperties.KEY_ALGORITHM_AES);
+        try {
+            return decrypt(key, ciphertext);
+        } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException
+                | IllegalBlockSizeException | BadPaddingException
+                | InvalidAlgorithmParameterException e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    public static byte[] decryptBlob(String keyAlias, byte[] blob, byte[] applicationId) {
+        try {
+            KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
+            keyStore.load(null);
+
+            SecretKey decryptionKey = (SecretKey) keyStore.getKey(keyAlias, null);
+            byte[] intermediate = decrypt(applicationId, APPLICATION_ID_PERSONALIZATION, blob);
+            return decrypt(decryptionKey, intermediate);
+        } catch (CertificateException | IOException | BadPaddingException
+                | IllegalBlockSizeException
+                | KeyStoreException | NoSuchPaddingException | NoSuchAlgorithmException
+                | InvalidKeyException | UnrecoverableKeyException
+                | InvalidAlgorithmParameterException e) {
+            e.printStackTrace();
+            throw new RuntimeException("Failed to decrypt blob", e);
+        }
+    }
+
+    public static byte[] createBlob(String keyAlias, byte[] data, byte[] applicationId, long sid) {
+        try {
+            KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES);
+            keyGenerator.init(new SecureRandom());
+            SecretKey secretKey = keyGenerator.generateKey();
+            KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
+            keyStore.load(null);
+            KeyProtection.Builder builder = new KeyProtection.Builder(KeyProperties.PURPOSE_DECRYPT)
+                    .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
+                    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE);
+            if (sid != 0) {
+                builder.setUserAuthenticationRequired(true)
+                        .setBoundToSpecificSecureUserId(sid)
+                        .setUserAuthenticationValidityDurationSeconds(USER_AUTHENTICATION_VALIDITY);
+            }
+            keyStore.setEntry(keyAlias,
+                    new KeyStore.SecretKeyEntry(secretKey),
+                    builder.build());
+            byte[] intermediate = encrypt(secretKey, data);
+            return encrypt(applicationId, APPLICATION_ID_PERSONALIZATION, intermediate);
+
+        } catch (CertificateException | IOException | BadPaddingException
+                | IllegalBlockSizeException
+                | KeyStoreException | NoSuchPaddingException | NoSuchAlgorithmException
+                | InvalidKeyException e) {
+            e.printStackTrace();
+            throw new RuntimeException("Failed to encrypt blob", e);
+        }
+    }
+
+    public static void destroyBlobKey(String keyAlias) {
+        KeyStore keyStore;
+        try {
+            keyStore = KeyStore.getInstance("AndroidKeyStore");
+            keyStore.load(null);
+            keyStore.deleteEntry(keyAlias);
+        } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException
+                | IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    protected static byte[] personalisedHash(byte[] personalisation, byte[]... message) {
+        try {
+            final int PADDING_LENGTH = 128;
+            MessageDigest digest = MessageDigest.getInstance("SHA-512");
+            if (personalisation.length > PADDING_LENGTH) {
+                throw new RuntimeException("Personalisation too long");
+            }
+            // Personalize the hash
+            // Pad it to the block size of the hash function
+            personalisation = Arrays.copyOf(personalisation, PADDING_LENGTH);
+            digest.update(personalisation);
+            for (byte[] data : message) {
+                digest.update(data);
+            }
+            return digest.digest();
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException("NoSuchAlgorithmException for SHA-512", e);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/SyntheticPasswordManager.java b/services/core/java/com/android/server/SyntheticPasswordManager.java
new file mode 100644
index 0000000..6267880
--- /dev/null
+++ b/services/core/java/com/android/server/SyntheticPasswordManager.java
@@ -0,0 +1,692 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.RemoteException;
+import android.service.gatekeeper.GateKeeperResponse;
+import android.service.gatekeeper.IGateKeeperService;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.widget.VerifyCredentialResponse;
+
+import libcore.util.HexEncoding;
+
+import java.nio.ByteBuffer;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+
+
+/**
+ * A class that maintains the wrapping of synthetic password by user credentials or escrow tokens.
+ * It's (mostly) a pure storage for synthetic passwords, providing APIs to creating and destroying
+ * synthetic password blobs which are wrapped by user credentials or escrow tokens.
+ *
+ * Here is the assumptions it makes:
+ *   Each user has one single synthetic password at any time.
+ *   The SP has an associated password handle, which binds to the SID for that user. The password
+ *   handle is persisted by SyntheticPasswordManager internally.
+ *   If the user credential is null, it's treated as if the credential is DEFAULT_PASSWORD
+ */
+public class SyntheticPasswordManager {
+    private static final String SP_BLOB_NAME = "spblob";
+    private static final String SP_E0_NAME = "e0";
+    private static final String SP_P1_NAME = "p1";
+    private static final String SP_HANDLE_NAME = "handle";
+    private static final String SECDISCARDABLE_NAME = "secdis";
+    private static final int SECDISCARDABLE_LENGTH = 16 * 1024;
+    private static final String PASSWORD_DATA_NAME = "pwd";
+
+    public static final long DEFAULT_HANDLE = 0;
+    private static final String DEFAULT_PASSWORD = "default-password";
+
+    private static final byte SYNTHETIC_PASSWORD_VERSION = 1;
+    private static final byte SYNTHETIC_PASSWORD_PASSWORD_BASED = 0;
+    private static final byte SYNTHETIC_PASSWORD_TOKEN_BASED = 1;
+
+    // 256-bit synthetic password
+    private static final byte SYNTHETIC_PASSWORD_LENGTH = 256 / 8;
+
+    private static final int PASSWORD_SCRYPT_N = 13;
+    private static final int PASSWORD_SCRYPT_R = 3;
+    private static final int PASSWORD_SCRYPT_P = 1;
+    private static final int PASSWORD_SALT_LENGTH = 16;
+    private static final int PASSWORD_TOKEN_LENGTH = 32;
+    private static final String TAG = "SyntheticPasswordManager";
+
+    private static final byte[] PERSONALISATION_SECDISCARDABLE = "secdiscardable-transform".getBytes();
+    private static final byte[] PERSONALIZATION_KEY_STORE_PASSWORD = "keystore-password".getBytes();
+    private static final byte[] PERSONALIZATION_USER_GK_AUTH = "user-gk-authentication".getBytes();
+    private static final byte[] PERSONALIZATION_SP_GK_AUTH = "sp-gk-authentication".getBytes();
+    private static final byte[] PERSONALIZATION_FBE_KEY = "fbe-key".getBytes();
+    private static final byte[] PERSONALIZATION_SP_SPLIT = "sp-split".getBytes();
+    private static final byte[] PERSONALIZATION_E0 = "e0-encryption".getBytes();
+
+    static class AuthenticationResult {
+        public AuthenticationToken authToken;
+        public VerifyCredentialResponse gkResponse;
+    }
+
+    static class AuthenticationToken {
+        /*
+         * Here is the relationship between all three fields:
+         * P0 and P1 are two randomly-generated blocks. P1 is stored on disk but P0 is not.
+         * syntheticPassword = hash(P0 || P1)
+         * E0 = P0 encrypted under syntheticPassword, stored on disk.
+         */
+        private @Nullable byte[] E0;
+        private @Nullable byte[] P1;
+        private @NonNull String syntheticPassword;
+
+        public String deriveKeyStorePassword() {
+            return bytesToHex(SyntheticPasswordCrypto.personalisedHash(
+                    PERSONALIZATION_KEY_STORE_PASSWORD, syntheticPassword.getBytes()));
+        }
+
+        public byte[] deriveGkPassword() {
+            return SyntheticPasswordCrypto.personalisedHash(PERSONALIZATION_SP_GK_AUTH,
+                    syntheticPassword.getBytes());
+        }
+
+        public byte[] deriveDiskEncryptionKey() {
+            return SyntheticPasswordCrypto.personalisedHash(PERSONALIZATION_FBE_KEY,
+                    syntheticPassword.getBytes());
+        }
+
+        private void initialize(byte[] P0, byte[] P1) {
+            this.P1 = P1;
+            this.syntheticPassword = String.valueOf(HexEncoding.encode(
+                    SyntheticPasswordCrypto.personalisedHash(
+                            PERSONALIZATION_SP_SPLIT, P0, P1)));
+            this.E0 = SyntheticPasswordCrypto.encrypt(this.syntheticPassword.getBytes(),
+                    PERSONALIZATION_E0, P0);
+        }
+
+        public void recreate(byte[] secret) {
+            initialize(secret, this.P1);
+        }
+
+        protected static AuthenticationToken create() {
+            AuthenticationToken result = new AuthenticationToken();
+            result.initialize(secureRandom(SYNTHETIC_PASSWORD_LENGTH),
+                    secureRandom(SYNTHETIC_PASSWORD_LENGTH));
+            return result;
+        }
+
+        public byte[] computeP0() {
+            if (E0 == null) {
+                return null;
+            }
+            return SyntheticPasswordCrypto.decrypt(syntheticPassword.getBytes(), PERSONALIZATION_E0,
+                    E0);
+        }
+    }
+
+    static class PasswordData {
+        byte scryptN;
+        byte scryptR;
+        byte scryptP;
+        public int passwordType;
+        byte[] salt;
+        public byte[] passwordHandle;
+
+        public static PasswordData create(int passwordType) {
+            PasswordData result = new PasswordData();
+            result.scryptN = PASSWORD_SCRYPT_N;
+            result.scryptR = PASSWORD_SCRYPT_R;
+            result.scryptP = PASSWORD_SCRYPT_P;
+            result.passwordType = passwordType;
+            result.salt = secureRandom(PASSWORD_SALT_LENGTH);
+            return result;
+        }
+
+        public static PasswordData fromBytes(byte[] data) {
+            PasswordData result = new PasswordData();
+            ByteBuffer buffer = ByteBuffer.allocate(data.length);
+            buffer.put(data, 0, data.length);
+            buffer.flip();
+            result.passwordType = buffer.getInt();
+            result.scryptN = buffer.get();
+            result.scryptR = buffer.get();
+            result.scryptP = buffer.get();
+            int saltLen = buffer.getInt();
+            result.salt = new byte[saltLen];
+            buffer.get(result.salt);
+            int handleLen = buffer.getInt();
+            result.passwordHandle = new byte[handleLen];
+            buffer.get(result.passwordHandle);
+            return result;
+        }
+
+        public byte[] toBytes() {
+            ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES + 3 * Byte.BYTES
+                    + Integer.BYTES + salt.length + Integer.BYTES + passwordHandle.length);
+            buffer.putInt(passwordType);
+            buffer.put(scryptN);
+            buffer.put(scryptR);
+            buffer.put(scryptP);
+            buffer.putInt(salt.length);
+            buffer.put(salt);
+            buffer.putInt(passwordHandle.length);
+            buffer.put(passwordHandle);
+            return buffer.array();
+        }
+    }
+
+    private LockSettingsStorage mStorage;
+
+    public SyntheticPasswordManager(LockSettingsStorage storage) {
+        mStorage = storage;
+    }
+
+
+    public int getCredentialType(long handle, int userId) {
+        byte[] passwordData = loadState(PASSWORD_DATA_NAME, handle, userId);
+        if (passwordData == null) {
+            Log.w(TAG, "getCredentialType: encountered empty password data for user " + userId);
+            return LockPatternUtils.CREDENTIAL_TYPE_NONE;
+        }
+        return PasswordData.fromBytes(passwordData).passwordType;
+    }
+
+    /**
+     * Initializing a new Authentication token, possibly from an existing credential and hash.
+     *
+     * The authentication token would bear a randomly-generated synthetic password.
+     *
+     * This method has the side effect of rebinding the SID of the given user to the
+     * newly-generated SP.
+     *
+     * If the existing credential hash is non-null, the existing SID mill be migrated so
+     * the synthetic password in the authentication token will produce the same SID
+     * (the corresponding synthetic password handle is persisted by SyntheticPasswordManager
+     * in a per-user data storage.
+     *
+     * If the existing credential hash is null, it means the given user should have no SID so
+     * SyntheticPasswordManager will nuke any SP handle previously persisted. In this case,
+     * the supplied credential parameter is also ignored.
+     *
+     * Also saves the escrow information necessary to re-generate the synthetic password under
+     * an escrow scheme. This information can be removed with {@link #destroyEscrowData} if
+     * password escrow should be disabled completely on the given user.
+     *
+     */
+    public AuthenticationToken newSyntheticPasswordAndSid(IGateKeeperService gatekeeper,
+            byte[] hash, String credential, int userId) throws RemoteException {
+        AuthenticationToken result = AuthenticationToken.create();
+        GateKeeperResponse response;
+        if (hash != null) {
+            response = gatekeeper.enroll(userId, hash, credential.getBytes(),
+                    result.deriveGkPassword());
+            if (response.getResponseCode() != GateKeeperResponse.RESPONSE_OK) {
+                Log.w(TAG, "Fail to migrate SID, assuming no SID, user " + userId);
+                clearSidForUser(userId);
+            } else {
+                saveSyntheticPasswordHandle(response.getPayload(), userId);
+            }
+        } else {
+            clearSidForUser(userId);
+        }
+        saveEscrowData(result, userId);
+        return result;
+    }
+
+    /**
+     * Enroll a new password handle and SID for the given synthetic password and persist it on disk.
+     * Used when adding password to previously-unsecured devices.
+     */
+    public void newSidForUser(IGateKeeperService gatekeeper, AuthenticationToken authToken,
+            int userId) throws RemoteException {
+        GateKeeperResponse response = gatekeeper.enroll(userId, null, null,
+                authToken.deriveGkPassword());
+        if (response.getResponseCode() != GateKeeperResponse.RESPONSE_OK) {
+            Log.e(TAG, "Fail to create new SID for user " + userId);
+            return;
+        }
+        saveSyntheticPasswordHandle(response.getPayload(), userId);
+    }
+
+    // Nuke the SP handle (and as a result, its SID) for the given user.
+    public void clearSidForUser(int userId) {
+        destroyState(SP_HANDLE_NAME, true, DEFAULT_HANDLE, userId);
+    }
+
+    public boolean hasSidForUser(int userId) {
+        return hasState(SP_HANDLE_NAME, DEFAULT_HANDLE, userId);
+    }
+
+    // if null, it means there is no SID associated with the user
+    // This can happen if the user is migrated to SP but currently
+    // do not have a lockscreen password.
+    private byte[] loadSyntheticPasswordHandle(int userId) {
+        return loadState(SP_HANDLE_NAME, DEFAULT_HANDLE, userId);
+    }
+
+    private void saveSyntheticPasswordHandle(byte[] spHandle, int userId) {
+        saveState(SP_HANDLE_NAME, spHandle, DEFAULT_HANDLE, userId);
+    }
+
+    private boolean loadEscrowData(AuthenticationToken authToken, int userId) {
+        authToken.E0 = loadState(SP_E0_NAME, DEFAULT_HANDLE, userId);
+        authToken.P1 = loadState(SP_P1_NAME, DEFAULT_HANDLE, userId);
+        return authToken.E0 != null && authToken.P1 != null;
+    }
+
+    private void saveEscrowData(AuthenticationToken authToken, int userId) {
+        saveState(SP_E0_NAME, authToken.E0, DEFAULT_HANDLE, userId);
+        saveState(SP_P1_NAME, authToken.P1, DEFAULT_HANDLE, userId);
+    }
+
+    public boolean hasEscrowData(int userId) {
+        return hasState(SP_E0_NAME, DEFAULT_HANDLE, userId)
+                && hasState(SP_P1_NAME, DEFAULT_HANDLE, userId);
+    }
+
+    public void destroyEscrowData(int userId) {
+        destroyState(SP_E0_NAME, true, DEFAULT_HANDLE, userId);
+        destroyState(SP_P1_NAME, true, DEFAULT_HANDLE, userId);
+    }
+
+    /**
+     * Create a new password based SP blob based on the supplied authentication token, such that
+     * a future successful authentication with unwrapPasswordBasedSyntheticPassword() would result
+     * in the same authentication token.
+     *
+     * This method only creates SP blob wrapping around the given synthetic password and does not
+     * handle logic around SID or SP handle. The caller should separately ensure that the user's SID
+     * is consistent with the device state by calling other APIs in this class.
+     *
+     * @see #newSidForUser
+     * @see #clearSidForUser
+     */
+    public long createPasswordBasedSyntheticPassword(IGateKeeperService gatekeeper,
+            String credential, int credentialType, AuthenticationToken authToken, int userId)
+                    throws RemoteException {
+        if (credential == null || credentialType == LockPatternUtils.CREDENTIAL_TYPE_NONE) {
+            credentialType = LockPatternUtils.CREDENTIAL_TYPE_NONE;
+            credential = DEFAULT_PASSWORD;
+        }
+
+        long handle = generateHandle();
+        PasswordData pwd = PasswordData.create(credentialType);
+        byte[] pwdToken = computePasswordToken(credential, pwd);
+
+        GateKeeperResponse response = gatekeeper.enroll(fakeUid(userId), null, null,
+                passwordTokenToGkInput(pwdToken));
+        if (response.getResponseCode() != GateKeeperResponse.RESPONSE_OK) {
+            Log.e(TAG, "Fail to enroll user password when creating SP for user " + userId);
+            return 0;
+        }
+        pwd.passwordHandle = response.getPayload();
+        long sid = sidFromPasswordHandle(pwd.passwordHandle);
+        saveState(PASSWORD_DATA_NAME, pwd.toBytes(), handle, userId);
+
+        byte[] applicationId = transformUnderSecdiscardable(pwdToken,
+                createSecdiscardable(handle, userId));
+        createSyntheticPasswordBlob(handle, SYNTHETIC_PASSWORD_PASSWORD_BASED, authToken,
+                applicationId, sid, userId);
+        return handle;
+    }
+
+    private ArrayMap<Integer, ArrayMap<Long, byte[]>> tokenMap = new ArrayMap<>();
+
+    public long createTokenBasedSyntheticPassword(byte[] token, int userId) {
+        long handle = generateHandle();
+        byte[] applicationId = transformUnderSecdiscardable(token,
+                createSecdiscardable(handle, userId));
+        if (!tokenMap.containsKey(userId)) {
+            tokenMap.put(userId, new ArrayMap<>());
+        }
+        tokenMap.get(userId).put(handle, applicationId);
+        return handle;
+    }
+
+    public Set<Long> getPendingTokensForUser(int userId) {
+        if (!tokenMap.containsKey(userId)) {
+            return Collections.emptySet();
+        }
+        return tokenMap.get(userId).keySet();
+    }
+
+    public boolean removePendingToken(long handle, int userId) {
+        if (!tokenMap.containsKey(userId)) {
+            return false;
+        }
+        return tokenMap.get(userId).remove(handle) != null;
+    }
+
+    public boolean activateTokenBasedSyntheticPassword(long handle, AuthenticationToken authToken,
+            int userId) {
+        if (!tokenMap.containsKey(userId)) {
+            return false;
+        }
+        byte[] applicationId = tokenMap.get(userId).get(handle);
+        if (applicationId == null) {
+            return false;
+        }
+        if (!loadEscrowData(authToken, userId)) {
+            Log.w(TAG, "User is not escrowable");
+            return false;
+        }
+        createSyntheticPasswordBlob(handle, SYNTHETIC_PASSWORD_TOKEN_BASED, authToken,
+                applicationId, 0L, userId);
+        tokenMap.get(userId).remove(handle);
+        return true;
+    }
+
+    private void createSyntheticPasswordBlob(long handle, byte type, AuthenticationToken authToken,
+            byte[] applicationId, long sid, int userId) {
+        final byte[] secret;
+        if (type == SYNTHETIC_PASSWORD_TOKEN_BASED) {
+            secret = authToken.computeP0();
+        } else {
+            secret = authToken.syntheticPassword.getBytes();
+        }
+        byte[] content = createSPBlob(getHandleName(handle), secret, applicationId, sid);
+        byte[] blob = new byte[content.length + 1 + 1];
+        blob[0] = SYNTHETIC_PASSWORD_VERSION;
+        blob[1] = type;
+        System.arraycopy(content, 0, blob, 2, content.length);
+        saveState(SP_BLOB_NAME, blob, handle, userId);
+    }
+
+    /**
+     * Decrypt a synthetic password by supplying the user credential and corresponding password
+     * blob handle generated previously. If the decryption is successful, initiate a GateKeeper
+     * verification to referesh the SID & Auth token maintained by the system.
+     */
+    public AuthenticationResult unwrapPasswordBasedSyntheticPassword(IGateKeeperService gatekeeper,
+            long handle, String credential, int userId) throws RemoteException {
+        if (credential == null) {
+            credential = DEFAULT_PASSWORD;
+        }
+        AuthenticationResult result = new AuthenticationResult();
+        PasswordData pwd = PasswordData.fromBytes(loadState(PASSWORD_DATA_NAME, handle, userId));
+        byte[] pwdToken = computePasswordToken(credential, pwd);
+        byte[] gkPwdToken = passwordTokenToGkInput(pwdToken);
+
+        GateKeeperResponse response = gatekeeper.verifyChallenge(fakeUid(userId), 0L,
+                pwd.passwordHandle, gkPwdToken);
+        int responseCode = response.getResponseCode();
+        if (responseCode == GateKeeperResponse.RESPONSE_OK) {
+            result.gkResponse = VerifyCredentialResponse.OK;
+            if (response.getShouldReEnroll()) {
+                GateKeeperResponse reenrollResponse = gatekeeper.enroll(fakeUid(userId),
+                        pwd.passwordHandle, gkPwdToken, gkPwdToken);
+                if (reenrollResponse.getResponseCode() == GateKeeperResponse.RESPONSE_OK) {
+                    pwd.passwordHandle = reenrollResponse.getPayload();
+                    saveState(PASSWORD_DATA_NAME, pwd.toBytes(), handle, userId);
+                } else {
+                    Log.w(TAG, "Fail to re-enroll user password for user " + userId);
+                    // continue the flow anyway
+                }
+            }
+        } else if (responseCode == GateKeeperResponse.RESPONSE_RETRY) {
+            result.gkResponse = new VerifyCredentialResponse(response.getTimeout());
+            return result;
+        } else  {
+            result.gkResponse = VerifyCredentialResponse.ERROR;
+            return result;
+        }
+
+
+        byte[] applicationId = transformUnderSecdiscardable(pwdToken,
+                loadSecdiscardable(handle, userId));
+        result.authToken = unwrapSyntheticPasswordBlob(handle, SYNTHETIC_PASSWORD_PASSWORD_BASED,
+                applicationId, userId);
+
+        // Perform verifyChallenge to refresh auth tokens for GK if user password exists.
+        result.gkResponse = verifyChallenge(gatekeeper, result.authToken, 0L, userId);
+        return result;
+    }
+
+    /**
+     * Decrypt a synthetic password by supplying an escrow token and corresponding token
+     * blob handle generated previously. If the decryption is successful, initiate a GateKeeper
+     * verification to referesh the SID & Auth token maintained by the system.
+     */
+    public @NonNull AuthenticationResult unwrapTokenBasedSyntheticPassword(
+            IGateKeeperService gatekeeper, long handle, byte[] token, int userId)
+                    throws RemoteException {
+        AuthenticationResult result = new AuthenticationResult();
+        byte[] applicationId = transformUnderSecdiscardable(token,
+                loadSecdiscardable(handle, userId));
+        result.authToken = unwrapSyntheticPasswordBlob(handle, SYNTHETIC_PASSWORD_TOKEN_BASED,
+                applicationId, userId);
+        if (result.authToken != null) {
+            result.gkResponse = verifyChallenge(gatekeeper, result.authToken, 0L, userId);
+            if (result.gkResponse == null) {
+                // The user currently has no password. return OK with null payload so null
+                // is propagated to unlockUser()
+                result.gkResponse = VerifyCredentialResponse.OK;
+            }
+        } else {
+            result.gkResponse = VerifyCredentialResponse.ERROR;
+        }
+        return result;
+    }
+
+    private AuthenticationToken unwrapSyntheticPasswordBlob(long handle, byte type,
+            byte[] applicationId, int userId) {
+        byte[] blob = loadState(SP_BLOB_NAME, handle, userId);
+        if (blob == null) {
+            return null;
+        }
+        if (blob[0] != SYNTHETIC_PASSWORD_VERSION) {
+            throw new RuntimeException("Unknown blob version");
+        }
+        if (blob[1] != type) {
+            throw new RuntimeException("Invalid blob type");
+        }
+        byte[] secret = decryptSPBlob(getHandleName(handle),
+                Arrays.copyOfRange(blob, 2, blob.length), applicationId);
+        if (secret == null) {
+            Log.e(TAG, "Fail to decrypt SP for user " + userId);
+            return null;
+        }
+        AuthenticationToken result = new AuthenticationToken();
+        if (type == SYNTHETIC_PASSWORD_TOKEN_BASED) {
+            if (!loadEscrowData(result, userId)) {
+                Log.e(TAG, "User is not escrowable: " + userId);
+                return null;
+            }
+            result.recreate(secret);
+        } else {
+            result.syntheticPassword = new String(secret);
+        }
+        return result;
+    }
+
+    /**
+     * performs GK verifyChallenge and returns auth token, re-enrolling SP password handle
+     * if required.
+     *
+     * Normally performing verifyChallenge with an AuthenticationToken should always return
+     * RESPONSE_OK, since user authentication failures are detected earlier when trying to
+     * decrypt SP.
+     */
+    public VerifyCredentialResponse verifyChallenge(IGateKeeperService gatekeeper,
+            @NonNull AuthenticationToken auth, long challenge, int userId) throws RemoteException {
+        byte[] spHandle = loadSyntheticPasswordHandle(userId);
+        if (spHandle == null) {
+            // There is no password handle associated with the given user, i.e. the user is not
+            // secured by lockscreen and has no SID, so just return here;
+            return null;
+        }
+        VerifyCredentialResponse result;
+        GateKeeperResponse response = gatekeeper.verifyChallenge(userId, challenge,
+                spHandle, auth.deriveGkPassword());
+        int responseCode = response.getResponseCode();
+        if (responseCode == GateKeeperResponse.RESPONSE_OK) {
+            result = new VerifyCredentialResponse(response.getPayload());
+            if (response.getShouldReEnroll()) {
+                response = gatekeeper.enroll(userId, spHandle,
+                        spHandle, auth.deriveGkPassword());
+                if (response.getResponseCode() == GateKeeperResponse.RESPONSE_OK) {
+                    spHandle = response.getPayload();
+                    saveSyntheticPasswordHandle(spHandle, userId);
+                    // Call self again to re-verify with updated handle
+                    return verifyChallenge(gatekeeper, auth, challenge, userId);
+                } else {
+                    Log.w(TAG, "Fail to re-enroll SP handle for user " + userId);
+                    // Fall through, return existing handle
+                }
+            }
+        } else if (responseCode == GateKeeperResponse.RESPONSE_RETRY) {
+            result = new VerifyCredentialResponse(response.getTimeout());
+        } else {
+            result = VerifyCredentialResponse.ERROR;
+        }
+        return result;
+    }
+
+    public boolean existsHandle(long handle, int userId) {
+        return hasState(SP_BLOB_NAME, handle, userId);
+    }
+
+    public void destroyTokenBasedSyntheticPassword(long handle, int userId) {
+        destroySyntheticPassword(handle, userId);
+        destroyState(SECDISCARDABLE_NAME, true, handle, userId);
+    }
+
+    public void destroyPasswordBasedSyntheticPassword(long handle, int userId) {
+        destroySyntheticPassword(handle, userId);
+        destroyState(SECDISCARDABLE_NAME, true, handle, userId);
+        destroyState(PASSWORD_DATA_NAME, true, handle, userId);
+    }
+
+    private void destroySyntheticPassword(long handle, int userId) {
+        destroyState(SP_BLOB_NAME, true, handle, userId);
+        destroyState(SP_E0_NAME, true, handle, userId);
+        destroyState(SP_P1_NAME, true, handle, userId);
+        destroySPBlobKey(getHandleName(handle));
+    }
+
+    private byte[] transformUnderSecdiscardable(byte[] data, byte[] rawSecdiscardable) {
+        byte[] secdiscardable = SyntheticPasswordCrypto.personalisedHash(
+                PERSONALISATION_SECDISCARDABLE, rawSecdiscardable);
+        byte[] result = new byte[data.length + secdiscardable.length];
+        System.arraycopy(data, 0, result, 0, data.length);
+        System.arraycopy(secdiscardable, 0, result, data.length, secdiscardable.length);
+        return result;
+    }
+
+    private byte[] createSecdiscardable(long handle, int userId) {
+        byte[] data = secureRandom(SECDISCARDABLE_LENGTH);
+        saveState(SECDISCARDABLE_NAME, data, handle, userId);
+        return data;
+    }
+
+    private byte[] loadSecdiscardable(long handle, int userId) {
+        return loadState(SECDISCARDABLE_NAME, handle, userId);
+    }
+
+    private boolean hasState(String stateName, long handle, int userId) {
+        return !ArrayUtils.isEmpty(loadState(stateName, handle, userId));
+    }
+
+    private byte[] loadState(String stateName, long handle, int userId) {
+        return mStorage.readSyntheticPasswordState(userId, handle, stateName);
+    }
+
+    private void saveState(String stateName, byte[] data, long handle, int userId) {
+        mStorage.writeSyntheticPasswordState(userId, handle, stateName, data);
+    }
+
+    private void destroyState(String stateName, boolean secure, long handle, int userId) {
+        mStorage.deleteSyntheticPasswordState(userId, handle, stateName, secure);
+    }
+
+    protected byte[] decryptSPBlob(String blobKeyName, byte[] blob, byte[] applicationId) {
+        return SyntheticPasswordCrypto.decryptBlob(blobKeyName, blob, applicationId);
+    }
+
+    protected byte[] createSPBlob(String blobKeyName, byte[] data, byte[] applicationId, long sid) {
+        return SyntheticPasswordCrypto.createBlob(blobKeyName, data, applicationId, sid);
+    }
+
+    protected void destroySPBlobKey(String keyAlias) {
+        SyntheticPasswordCrypto.destroyBlobKey(keyAlias);
+    }
+
+    public static long generateHandle() {
+        SecureRandom rng = new SecureRandom();
+        long result;
+        do {
+            result = rng.nextLong();
+        } while (result == DEFAULT_HANDLE);
+        return result;
+    }
+
+    private int fakeUid(int uid) {
+        return 100000 + uid;
+    }
+
+    protected static byte[] secureRandom(int length) {
+        try {
+            return SecureRandom.getInstance("SHA1PRNG").generateSeed(length);
+        } catch (NoSuchAlgorithmException e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    private String getHandleName(long handle) {
+        return String.format("%s%x", LockPatternUtils.SYNTHETIC_PASSWORD_KEY_PREFIX, handle);
+    }
+
+    private byte[] computePasswordToken(String password, PasswordData data) {
+        return scrypt(password, data.salt, 1 << data.scryptN, 1 << data.scryptR, 1 << data.scryptP,
+                PASSWORD_TOKEN_LENGTH);
+    }
+
+    private byte[] passwordTokenToGkInput(byte[] token) {
+        return SyntheticPasswordCrypto.personalisedHash(PERSONALIZATION_USER_GK_AUTH, token);
+    }
+
+    protected long sidFromPasswordHandle(byte[] handle) {
+        return nativeSidFromPasswordHandle(handle);
+    }
+
+    protected byte[] scrypt(String password, byte[] salt, int N, int r, int p, int outLen) {
+        return nativeScrypt(password.getBytes(), salt, N, r, p, outLen);
+    }
+
+    native long nativeSidFromPasswordHandle(byte[] handle);
+    native byte[] nativeScrypt(byte[] password, byte[] salt, int N, int r, int p, int outLen);
+
+    final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();
+    public static String bytesToHex(byte[] bytes) {
+        if (bytes == null) {
+            return "null";
+        }
+        char[] hexChars = new char[bytes.length * 2];
+        for ( int j = 0; j < bytes.length; j++ ) {
+            int v = bytes[j] & 0xFF;
+            hexChars[j * 2] = hexArray[v >>> 4];
+            hexChars[j * 2 + 1] = hexArray[v & 0x0F];
+        }
+        return new String(hexChars);
+    }
+}
diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java
index fbc4440..5c8024a 100644
--- a/services/core/java/com/android/server/accounts/AccountManagerService.java
+++ b/services/core/java/com/android/server/accounts/AccountManagerService.java
@@ -4087,9 +4087,10 @@
     }
 
     @Override
-    public void addSharedAccountsFromParentUser(int parentUserId, int userId) {
+    public void addSharedAccountsFromParentUser(int parentUserId, int userId,
+            String opPackageName) {
         checkManageOrCreateUsersPermission("addSharedAccountsFromParentUser");
-        Account[] accounts = getAccountsAsUser(null, parentUserId, mContext.getOpPackageName());
+        Account[] accounts = getAccountsAsUser(null, parentUserId, opPackageName);
         for (Account account : accounts) {
             addSharedAccountAsUser(account, userId);
         }
diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
index bf1018f..a95a627 100644
--- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
+++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
@@ -103,6 +103,10 @@
         final boolean change;
         synchronized(mPlayerLock) {
             final AudioPlaybackConfiguration apc = mPlayers.get(new Integer(piid));
+            // FIXME SoundPool not ready for state reporting
+            if (apc.getPlayerType() == AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL) {
+                return;
+            }
             if (checkConfigurationCaller(piid, apc, binderUid)) {
                 //TODO add generation counter to only update to the latest state
                 change = apc.handleStateEvent(event);
diff --git a/services/core/java/com/android/server/connectivity/Tethering.java b/services/core/java/com/android/server/connectivity/Tethering.java
index 9d63462..6c608a2 100644
--- a/services/core/java/com/android/server/connectivity/Tethering.java
+++ b/services/core/java/com/android/server/connectivity/Tethering.java
@@ -1008,10 +1008,9 @@
                 return false;
             }
 
-            protected boolean requestUpstreamMobileConnection() {
+            protected void requestUpstreamMobileConnection() {
                 mUpstreamNetworkMonitor.updateMobileRequiresDun(mConfig.isDunRequired);
                 mUpstreamNetworkMonitor.registerMobileNetworkRequest();
-                return true;
             }
 
             protected void unrequestUpstreamMobileConnection() {
@@ -1058,9 +1057,13 @@
             }
 
             protected void chooseUpstreamType(boolean tryCell) {
+                final int upstreamType = findPreferredUpstreamType(tryCell);
+                setUpstreamByType(upstreamType);
+            }
+
+            protected int findPreferredUpstreamType(boolean tryCell) {
                 final ConnectivityManager cm = getConnectivityManager();
                 int upType = ConnectivityManager.TYPE_NONE;
-                String iface = null;
 
                 updateConfiguration(); // TODO - remove?
 
@@ -1100,7 +1103,8 @@
                         requestUpstreamMobileConnection();
                         break;
                     case ConnectivityManager.TYPE_NONE:
-                        if (tryCell && requestUpstreamMobileConnection()) {
+                        if (tryCell) {
+                            requestUpstreamMobileConnection();
                             // We think mobile should be coming up; don't set a retry.
                         } else {
                             sendMessageDelayed(CMD_RETRY_UPSTREAM, UPSTREAM_SETTLE_TIME_MS);
@@ -1117,7 +1121,13 @@
                         break;
                 }
 
+                return upType;
+            }
+
+            protected void setUpstreamByType(int upType) {
+                final ConnectivityManager cm = getConnectivityManager();
                 Network network = null;
+                String iface = null;
                 if (upType != ConnectivityManager.TYPE_NONE) {
                     LinkProperties linkProperties = cm.getLinkProperties(upType);
                     if (linkProperties != null) {
@@ -1354,9 +1364,9 @@
                 simChange.startListening();
                 mUpstreamNetworkMonitor.start();
 
-                mTryCell = true;  // better try something first pass or crazy tests cases will fail
-                chooseUpstreamType(mTryCell);
-                mTryCell = !mTryCell;
+                // Better try something first pass or crazy tests cases will fail.
+                chooseUpstreamType(true);
+                mTryCell = false;
             }
 
             @Override
@@ -1407,10 +1417,9 @@
                         break;
                     }
                     case CMD_UPSTREAM_CHANGED:
-                        // need to try DUN immediately if Wifi goes down
-                        mTryCell = true;
-                        chooseUpstreamType(mTryCell);
-                        mTryCell = !mTryCell;
+                        // Need to try DUN immediately if Wi-Fi goes down.
+                        chooseUpstreamType(true);
+                        mTryCell = false;
                         break;
                     case CMD_RETRY_UPSTREAM:
                         chooseUpstreamType(mTryCell);
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 771ae9a..f2b5564 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -121,6 +121,7 @@
 import android.service.notification.SnoozeCriterion;
 import android.service.notification.StatusBarNotification;
 import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenModeProto;
 import android.telephony.PhoneStateListener;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
@@ -140,6 +141,7 @@
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.internal.statusbar.NotificationVisibility;
 import com.android.internal.util.FastXmlSerializer;
@@ -2812,8 +2814,26 @@
                     proto.write(NotificationRecordProto.STATE, NotificationServiceProto.ENQUEUED);
                 }
             }
+            List<NotificationRecord> snoozed = mSnoozeHelper.getSnoozed();
+            N = snoozed.size();
+            if (N > 0) {
+                for (int i = 0; i < N; i++) {
+                    final NotificationRecord nr = snoozed.get(i);
+                    if (filter.filtered && !filter.matches(nr.sbn)) continue;
+                    nr.dump(proto, filter.redact);
+                    proto.write(NotificationRecordProto.STATE, NotificationServiceProto.SNOOZED);
+                }
+            }
             proto.end(records);
         }
+
+        long zenLog = proto.start(NotificationServiceDumpProto.ZEN);
+        mZenModeHelper.dump(proto);
+        for (ComponentName suppressor : mEffectsSuppressors) {
+            proto.write(ZenModeProto.SUPPRESSORS, suppressor.toString());
+        }
+        proto.end(zenLog);
+
         proto.flush();
     }
 
@@ -2899,24 +2919,12 @@
                         }
                         pw.println("  ");
                     }
+
+                    mSnoozeHelper.dump(pw, filter);
                 }
             }
 
             if (!zenOnly) {
-                pw.println("\n  Usage Stats:");
-                mUsageStats.dump(pw, "    ", filter);
-            }
-
-            if (!filter.filtered || zenOnly) {
-                pw.println("\n  Zen Mode:");
-                pw.print("    mInterruptionFilter="); pw.println(mInterruptionFilter);
-                mZenModeHelper.dump(pw, "    ");
-
-                pw.println("\n  Zen Log:");
-                ZenLog.dump(pw, "    ");
-            }
-
-            if (!zenOnly) {
                 pw.println("\n  Ranking Config:");
                 mRankingHelper.dump(pw, "    ", filter);
 
@@ -2945,8 +2953,13 @@
                 mNotificationAssistants.dump(pw, filter);
             }
 
-            if (!zenOnly) {
-                mSnoozeHelper.dump(pw, filter);
+            if (!filter.filtered || zenOnly) {
+                pw.println("\n  Zen Mode:");
+                pw.print("    mInterruptionFilter="); pw.println(mInterruptionFilter);
+                mZenModeHelper.dump(pw, "    ");
+
+                pw.println("\n  Zen Log:");
+                ZenLog.dump(pw, "    ");
             }
 
             pw.println("\n  Policy access:");
@@ -2964,6 +2977,11 @@
                     r.dump(pw, "      ", getContext(), filter.redact);
                 }
             }
+
+            if (!zenOnly) {
+                pw.println("\n  Usage Stats:");
+                mUsageStats.dump(pw, "    ", filter);
+            }
         }
     }
 
@@ -3131,7 +3149,9 @@
 
         // snoozed apps
         if (mSnoozeHelper.isSnoozed(userId, pkg, r.getKey())) {
-            // TODO: log to event log
+            MetricsLogger.action(r.getLogMaker()
+                    .setType(MetricsProto.MetricsEvent.TYPE_UPDATE)
+                    .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED));
             if (DBG) {
                 Slog.d(TAG, "Ignored enqueue for snoozed notification " + r.getKey());
             }
@@ -3867,14 +3887,18 @@
     private void cancelNotificationLocked(NotificationRecord r, boolean sendDelete, int reason) {
         final String canceledKey = r.getKey();
 
-        // Remove from either list
-        boolean wasPosted;
-        if (mNotificationList.remove(r)) {
-            mNotificationsByKey.remove(r.sbn.getKey());
+        // Remove from both lists, either list could have a separate Record for what is effectively
+        // the same notification.
+        boolean wasPosted = false;
+        NotificationRecord recordInList = null;
+        if ((recordInList = findNotificationByListLocked(mNotificationList, r.getKey())) != null) {
+            mNotificationList.remove(recordInList);
+            mNotificationsByKey.remove(recordInList.sbn.getKey());
             wasPosted = true;
-        } else {
-            mEnqueuedNotifications.remove(r);
-            wasPosted = false;
+        }
+        if ((recordInList = findNotificationByListLocked(mEnqueuedNotifications, r.getKey()))
+                != null) {
+            mEnqueuedNotifications.remove(recordInList);
         }
 
         // Record caller.
@@ -4155,7 +4179,7 @@
         if (until < System.currentTimeMillis() && snoozeCriterionId == null) {
             return;
         }
-        // TODO: write to event log
+
         if (DBG) {
             Slog.d(TAG, String.format("snooze event(%s, %d, %s, %s)", key, until, snoozeCriterionId,
                     listenerName));
@@ -4167,6 +4191,11 @@
                 synchronized (mNotificationLock) {
                     final NotificationRecord r = findNotificationByKeyLocked(key);
                     if (r != null) {
+                        MetricsLogger.action(r.getLogMaker()
+                                .setCategory(MetricsEvent.NOTIFICATION_SNOOZED)
+                                .setType(MetricsEvent.TYPE_CLOSE)
+                                .addTaggedData(MetricsEvent.NOTIFICATION_SNOOZED_CRITERIA,
+                                        snoozeCriterionId == null ? false : true));
                         cancelNotificationLocked(r, false, REASON_SNOOZED);
                         updateLightsLocked();
                         if (snoozeCriterionId != null) {
@@ -4185,7 +4214,6 @@
 
     void unsnoozeNotificationInt(String key, ManagedServiceInfo listener) {
         String listenerName = listener == null ? null : listener.component.toShortString();
-        // TODO: write to event log
         if (DBG) {
             Slog.d(TAG, String.format("unsnooze event(%s, %s)", key, listenerName));
         }
@@ -4298,17 +4326,12 @@
     // TODO: need to combine a bunch of these getters with slightly different behavior.
     // TODO: Should enqueuing just add to mNotificationsByKey instead?
     private NotificationRecord findNotificationByKeyLocked(String key) {
-        final int N = mNotificationList.size();
-        for (int i = 0; i < N; i++) {
-            if (key.equals(mNotificationList.get(i).getKey())) {
-                return mNotificationList.get(i);
-            }
+        NotificationRecord r;
+        if ((r = findNotificationByListLocked(mNotificationList, key)) != null) {
+            return r;
         }
-        final int M = mEnqueuedNotifications.size();
-        for (int i = 0; i < M; i++) {
-            if (key.equals(mEnqueuedNotifications.get(i).getKey())) {
-                return mEnqueuedNotifications.get(i);
-            }
+        if ((r = findNotificationByListLocked(mEnqueuedNotifications, key)) != null) {
+            return r;
         }
         return null;
     }
@@ -4326,8 +4349,7 @@
     }
 
     private NotificationRecord findNotificationByListLocked(ArrayList<NotificationRecord> list,
-            String pkg, String tag, int id, int userId)
-    {
+            String pkg, String tag, int id, int userId) {
         final int len = list.size();
         for (int i = 0; i < len; i++) {
             NotificationRecord r = list.get(i);
@@ -4339,6 +4361,18 @@
         return null;
     }
 
+    private NotificationRecord findNotificationByListLocked(ArrayList<NotificationRecord> list,
+            String key)
+    {
+        final int N = list.size();
+        for (int i = 0; i < N; i++) {
+            if (key.equals(list.get(i).getKey())) {
+                return list.get(i);
+            }
+        }
+        return null;
+    }
+
     // lock on mNotificationList
     int indexOfNotificationLocked(String key) {
         final int N = mNotificationList.size();
diff --git a/services/core/java/com/android/server/notification/SnoozeHelper.java b/services/core/java/com/android/server/notification/SnoozeHelper.java
index f2aff11..0cd8cea 100644
--- a/services/core/java/com/android/server/notification/SnoozeHelper.java
+++ b/services/core/java/com/android/server/notification/SnoozeHelper.java
@@ -16,11 +16,14 @@
 package com.android.server.notification;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 import org.xmlpull.v1.XmlSerializer;
 
+import android.annotation.NonNull;
 import android.app.AlarmManager;
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
@@ -99,7 +102,7 @@
         return Collections.EMPTY_LIST;
     }
 
-    protected List<NotificationRecord> getSnoozed() {
+    protected @NonNull List<NotificationRecord> getSnoozed() {
         List<NotificationRecord> snoozedForUser = new ArrayList<>();
         int[] userIds = mUserProfiles.getCurrentProfileIds();
         final int N = userIds.length;
@@ -270,6 +273,9 @@
         final NotificationRecord record = pkgRecords.remove(key);
 
         if (record != null) {
+            MetricsLogger.action(record.getLogMaker()
+                    .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED)
+                    .setType(MetricsProto.MetricsEvent.TYPE_OPEN));
             mCallback.repost(userId, record);
         }
     }
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 66fb976..75190f3 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -50,14 +50,18 @@
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.provider.Settings.Global;
+import android.service.notification.Condition;
 import android.service.notification.ConditionProviderService;
+import android.service.notification.NotificationServiceDumpProto;
 import android.service.notification.ZenModeConfig;
 import android.service.notification.ZenModeConfig.EventInfo;
 import android.service.notification.ZenModeConfig.ScheduleInfo;
 import android.service.notification.ZenModeConfig.ZenRule;
+import android.service.notification.ZenModeProto;
 import android.util.AndroidRuntimeException;
 import android.util.Log;
 import android.util.SparseArray;
+import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.R;
 import com.android.internal.logging.MetricsLogger;
@@ -488,6 +492,24 @@
         }
     }
 
+    void dump(ProtoOutputStream proto) {
+
+        proto.write(ZenModeProto.ZEN_MODE, mZenMode);
+        synchronized (mConfig) {
+            if (mConfig.manualRule != null) {
+                proto.write(ZenModeProto.ENABLED_ACTIVE_CONDITIONS, mConfig.manualRule.toString());
+            }
+            for (ZenRule rule : mConfig.automaticRules.values()) {
+                if (rule.enabled && rule.condition.state == Condition.STATE_TRUE
+                        && !rule.snoozing) {
+                    proto.write(ZenModeProto.ENABLED_ACTIVE_CONDITIONS, rule.toString());
+                }
+            }
+            proto.write(ZenModeProto.POLICY, mConfig.toNotificationPolicy().toString());
+            proto.write(ZenModeProto.SUPPRESSED_EFFECTS, mSuppressedEffects);
+        }
+    }
+
     public void dump(PrintWriter pw, String prefix) {
         pw.print(prefix); pw.print("mZenMode=");
         pw.println(Global.zenModeToString(mZenMode));
diff --git a/services/core/java/com/android/server/pm/PackageDexOptimizer.java b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
index db712ae..b589057 100644
--- a/services/core/java/com/android/server/pm/PackageDexOptimizer.java
+++ b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
@@ -66,6 +66,9 @@
     public static final int DEX_OPT_PERFORMED = 1;
     public static final int DEX_OPT_FAILED = -1;
 
+    /** Special library name that skips shared libraries check during compilation. */
+    public static final String SKIP_SHARED_LIBRARY_CHECK = "&";
+
     private final Installer mInstaller;
     private final Object mInstallLock;
 
@@ -274,7 +277,7 @@
                 // TODO(calin): maybe add a separate call.
                 mInstaller.dexopt(path, info.uid, info.packageName, isa, /*dexoptNeeded*/ 0,
                         /*oatDir*/ null, dexoptFlags,
-                        compilerFilter, info.volumeUuid, /*sharedLibrariesPath*/ null);
+                        compilerFilter, info.volumeUuid, SKIP_SHARED_LIBRARY_CHECK);
             }
 
             return DEX_OPT_PERFORMED;
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index ebd0b34..f43e468 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -126,6 +126,7 @@
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.AppsQueryHelper;
+import android.content.pm.ChangedPackages;
 import android.content.pm.ComponentInfo;
 import android.content.pm.InstantAppInfo;
 import android.content.pm.EphemeralRequest;
@@ -544,9 +545,6 @@
 
     public static final int REASON_LAST = REASON_CORE_APP;
 
-    /** Special library name that skips shared libraries check during compilation. */
-    private static final String SKIP_SHARED_LIBRARY_CHECK = "&";
-
     /** All dangerous permission names in the same order as the events in MetricsEvent */
     private static final List<String> ALL_DANGEROUS_PERMISSIONS = Arrays.asList(
             Manifest.permission.READ_CALENDAR,
@@ -720,6 +718,21 @@
 
     private final InstantAppRegistry mInstantAppRegistry;
 
+    @GuardedBy("mPackages")
+    int mChangedPackagesSequenceNumber;
+    /**
+     * List of changed [installed, removed or updated] packages.
+     * mapping from user id -> sequence number -> package name
+     */
+    @GuardedBy("mPackages")
+    final SparseArray<SparseArray<String>> mChangedPackages = new SparseArray<>();
+    /**
+     * The sequence number of the last change to a package.
+     * mapping from user id -> package name -> sequence number
+     */
+    @GuardedBy("mPackages")
+    final SparseArray<Map<String, Integer>> mChangedPackagesSequenceNumbers = new SparseArray<>();
+
     public static final class SharedLibraryEntry {
         public final String path;
         public final String apk;
@@ -2141,6 +2154,7 @@
                     pkgSetting.setInstalled(install, UserHandle.USER_SYSTEM);
                 }
             }
+            scheduleWritePackageRestrictionsLocked(UserHandle.USER_SYSTEM);
         }
     }
 
@@ -2384,7 +2398,7 @@
                                             DEXOPT_PUBLIC,
                                             getCompilerFilterForReason(REASON_SHARED_APK),
                                             StorageManager.UUID_PRIVATE_INTERNAL,
-                                            SKIP_SHARED_LIBRARY_CHECK);
+                                            PackageDexOptimizer.SKIP_SHARED_LIBRARY_CHECK);
                                 }
                             } catch (FileNotFoundException e) {
                                 Slog.w(TAG, "Library not found: " + libPath);
@@ -4220,6 +4234,52 @@
         }
     }
 
+    private void updateSequenceNumberLP(String packageName, int[] userList) {
+        for (int i = userList.length - 1; i >= 0; --i) {
+            final int userId = userList[i];
+            SparseArray<String> changedPackages = mChangedPackages.get(userId);
+            if (changedPackages == null) {
+                changedPackages = new SparseArray<>();
+                mChangedPackages.put(userId, changedPackages);
+            }
+            Map<String, Integer> sequenceNumbers = mChangedPackagesSequenceNumbers.get(userId);
+            if (sequenceNumbers == null) {
+                sequenceNumbers = new HashMap<>();
+                mChangedPackagesSequenceNumbers.put(userId, sequenceNumbers);
+            }
+            final Integer sequenceNumber = sequenceNumbers.get(packageName);
+            if (sequenceNumber != null) {
+                changedPackages.remove(sequenceNumber);
+            }
+            changedPackages.put(mChangedPackagesSequenceNumber, packageName);
+            sequenceNumbers.put(packageName, mChangedPackagesSequenceNumber);
+        }
+        mChangedPackagesSequenceNumber++;
+    }
+
+    @Override
+    public ChangedPackages getChangedPackages(int sequenceNumber, int userId) {
+        synchronized (mPackages) {
+            if (sequenceNumber >= mChangedPackagesSequenceNumber) {
+                return null;
+            }
+            final SparseArray<String> changedPackages = mChangedPackages.get(userId);
+            if (changedPackages == null) {
+                return null;
+            }
+            final List<String> packageNames =
+                    new ArrayList<>(mChangedPackagesSequenceNumber - sequenceNumber);
+            for (int i = sequenceNumber; i < mChangedPackagesSequenceNumber; i++) {
+                final String packageName = changedPackages.get(i);
+                if (packageName != null) {
+                    packageNames.add(packageName);
+                }
+            }
+            return packageNames.isEmpty()
+                    ? null : new ChangedPackages(mChangedPackagesSequenceNumber, packageNames);
+        }
+    }
+
     @Override
     public @NonNull ParceledListSlice<FeatureInfo> getSystemAvailableFeatures() {
         ArrayList<FeatureInfo> res;
@@ -13251,6 +13311,7 @@
                     pkgSetting.setHidden(false, userId);
                     pkgSetting.setInstallReason(installReason, userId);
                     mSettings.writePackageRestrictionsLPr(userId);
+                    mSettings.writeKernelMappingLPr(pkgSetting);
                     installed = true;
                 }
             }
@@ -13263,6 +13324,9 @@
                     }
                 }
                 sendPackageAddedForUser(packageName, pkgSetting, userId);
+                synchronized (mPackages) {
+                    updateSequenceNumberLP(packageName, new int[]{ userId });
+                }
             }
         } finally {
             Binder.restoreCallingIdentity(callingId);
@@ -16345,6 +16409,7 @@
             //note that the new package setting would have already been
             //added to mPackages. It hasn't been persisted yet.
             mSettings.setInstallStatus(pkgName, PackageSettingBase.PKG_INSTALL_INCOMPLETE);
+            // TODO: Remove this write? It's also written at the end of this method
             Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "writeSettings");
             mSettings.writeLPr();
             Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
@@ -16418,6 +16483,7 @@
                 } else if (!previousUserIds.contains(userId)) {
                     ps.setInstallReason(installReason, userId);
                 }
+                mSettings.writeKernelMappingLPr(ps);
             }
             res.name = pkgName;
             res.uid = newPackage.applicationInfo.uid;
@@ -16847,6 +16913,10 @@
                             sUserManager.getUserIds(), true);
                 }
             }
+
+            if (res.returnCode == PackageManager.INSTALL_SUCCEEDED) {
+                updateSequenceNumberLP(pkgName, res.newUsers);
+            }
         }
     }
 
@@ -17429,6 +17499,7 @@
                 if (res) {
                     mInstantAppRegistry.onPackageUninstalledLPw(uninstalledPs.pkg,
                             info.removedUsers);
+                    updateSequenceNumberLP(packageName, info.removedUsers);
                 }
             }
         }
@@ -17597,6 +17668,7 @@
 
         // writer
         synchronized (mPackages) {
+            boolean installedStateChanged = false;
             if (deletedPs != null) {
                 if ((flags&PackageManager.DELETE_KEEP_DATA) == 0) {
                     clearIntentFilterVerificationsLPw(deletedPs.name, UserHandle.USER_ALL);
@@ -17644,6 +17716,9 @@
                         if (DEBUG_REMOVE) {
                             Slog.d(TAG, "    user " + userId + " => " + installed);
                         }
+                        if (installed != ps.getInstalled(userId)) {
+                            installedStateChanged = true;
+                        }
                         ps.setInstalled(installed, userId);
                     }
                 }
@@ -17653,6 +17728,9 @@
                 // Save settings now
                 mSettings.writeLPr();
             }
+            if (installedStateChanged) {
+                mSettings.writeKernelMappingLPr(ps);
+            }
         }
         if (removedAppId != -1) {
             // A user ID was deleted here. Go through all users and remove it
@@ -17795,6 +17873,7 @@
                     UPDATE_PERMISSIONS_ALL | UPDATE_PERMISSIONS_REPLACE_PKG);
 
             if (applyUserRestrictions) {
+                boolean installedStateChanged = false;
                 if (DEBUG_REMOVE) {
                     Slog.d(TAG, "Propagating install state across reinstall");
                 }
@@ -17803,6 +17882,9 @@
                     if (DEBUG_REMOVE) {
                         Slog.d(TAG, "    user " + userId + " => " + installed);
                     }
+                    if (installed != ps.getInstalled(userId)) {
+                        installedStateChanged = true;
+                    }
                     ps.setInstalled(installed, userId);
 
                     mSettings.writeRuntimePermissionsForUserLPr(userId, false);
@@ -17810,6 +17892,9 @@
                 // Regardless of writeSettings we need to ensure that this restriction
                 // state propagation is persisted
                 mSettings.writeAllUsersPackageRestrictionsLPr();
+                if (installedStateChanged) {
+                    mSettings.writeKernelMappingLPr(ps);
+                }
             }
             // can downgrade to reader here
             if (writeSettings) {
@@ -18013,6 +18098,7 @@
                     // broadcasts will be sent correctly.
                     if (DEBUG_REMOVE) Slog.d(TAG, "Not installed by other users, full delete");
                     ps.setInstalled(true, user.getIdentifier());
+                    mSettings.writeKernelMappingLPr(ps);
                 }
             } else {
                 // This is a system app, so we assume that the
@@ -18122,6 +18208,7 @@
                     ps.readUserState(nextUserId).domainVerificationStatus, 0,
                     PackageManager.INSTALL_REASON_UNKNOWN);
         }
+        mSettings.writeKernelMappingLPr(ps);
     }
 
     private boolean clearPackageStateForUserLIF(PackageSetting ps, int userId,
@@ -19725,6 +19812,7 @@
                 }
             }
             scheduleWritePackageRestrictionsLocked(userId);
+            updateSequenceNumberLP(packageName, new int[] { userId });
             components = mPendingBroadcasts.get(userId, packageName);
             final boolean newPackage = components == null;
             if (newPackage) {
diff --git a/services/core/java/com/android/server/pm/PackageSettingBase.java b/services/core/java/com/android/server/pm/PackageSettingBase.java
index b63edfd..0e11b0c 100644
--- a/services/core/java/com/android/server/pm/PackageSettingBase.java
+++ b/services/core/java/com/android/server/pm/PackageSettingBase.java
@@ -40,6 +40,9 @@
  * Settings base class for pending and resolved classes.
  */
 abstract class PackageSettingBase extends SettingBase {
+
+    private static final int[] EMPTY_INT_ARRAY = new int[0];
+
     /**
      * Indicates the state of installation. Used by PackageManager to figure out
      * incomplete installations. Say a package is being installed (the state is
@@ -502,6 +505,25 @@
         userState.delete(userId);
     }
 
+    public int[] getNotInstalledUserIds() {
+        int count = 0;
+        int userStateCount = userState.size();
+        for (int i = 0; i < userStateCount; i++) {
+            if (userState.valueAt(i).installed == false) {
+                count++;
+            }
+        }
+        if (count == 0) return EMPTY_INT_ARRAY;
+        int[] excludedUserIds = new int[count];
+        int idx = 0;
+        for (int i = 0; i < userStateCount; i++) {
+            if (userState.valueAt(i).installed == false) {
+                excludedUserIds[idx++] = userState.keyAt(i);
+            }
+        }
+        return excludedUserIds;
+    }
+
     IntentFilterVerificationInfo getIntentFilterVerificationInfo() {
         return verificationInfo;
     }
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 281e445..6156802 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -252,6 +252,7 @@
     private final File mPackageListFilename;
     private final File mStoppedPackagesFilename;
     private final File mBackupStoppedPackagesFilename;
+    /** The top level directory in configfs for sdcardfs to push the package->uid,userId mappings */
     private final File mKernelMappingFilename;
 
     /** Map from package name to settings */
@@ -260,8 +261,8 @@
     /** List of packages that installed other packages */
     final ArraySet<String> mInstallerPackages = new ArraySet<>();
 
-    /** Map from package name to appId */
-    private final ArrayMap<String, Integer> mKernelMapping = new ArrayMap<>();
+    /** Map from package name to appId and excluded userids */
+    private final ArrayMap<String, KernelPackageState> mKernelMapping = new ArrayMap<>();
 
     // List of replaced system applications
     private final ArrayMap<String, PackageSetting> mDisabledSysPackages =
@@ -271,6 +272,11 @@
     private final ArrayMap<String, IntentFilterVerificationInfo> mRestoredIntentFilterVerifications =
             new ArrayMap<String, IntentFilterVerificationInfo>();
 
+    private static final class KernelPackageState {
+        int appId;
+        int[] excludedUserIds;
+    }
+
     // Bookkeeping for restored user permission grants
     final class RestoredPermissionGrant {
         String permissionName;
@@ -2512,6 +2518,15 @@
         //Debug.stopMethodTracing();
     }
 
+    private void writeKernelRemoveUserLPr(int userId) {
+        if (mKernelMappingFilename == null) return;
+
+        File removeUserIdFile = new File(mKernelMappingFilename, "remove_userid");
+        if (DEBUG_KERNEL) Slog.d(TAG, "Writing " + userId + " to " + removeUserIdFile
+                .getAbsolutePath());
+        writeIntToFile(removeUserIdFile, userId);
+    }
+
     void writeKernelMappingLPr() {
         if (mKernelMappingFilename == null) return;
 
@@ -2538,27 +2553,63 @@
     }
 
     void writeKernelMappingLPr(PackageSetting ps) {
-        if (mKernelMappingFilename == null) return;
+        if (mKernelMappingFilename == null || ps == null || ps.name == null) return;
 
-        final Integer cur = mKernelMapping.get(ps.name);
-        if (cur != null && cur.intValue() == ps.appId) {
-            // Ignore when mapping already matches
-            return;
+        KernelPackageState cur = mKernelMapping.get(ps.name);
+        final boolean firstTime = cur == null;
+        int[] excludedUserIds = ps.getNotInstalledUserIds();
+        final boolean userIdsChanged = firstTime
+                || !Arrays.equals(excludedUserIds, cur.excludedUserIds);
+
+        // Package directory
+        final File dir = new File(mKernelMappingFilename, ps.name);
+
+        if (firstTime) {
+            dir.mkdir();
+            // Create a new mapping state
+            cur = new KernelPackageState();
+            mKernelMapping.put(ps.name, cur);
         }
 
-        if (DEBUG_KERNEL) Slog.d(TAG, "Mapping " + ps.name + " to " + ps.appId);
+        // If mapping is incorrect or non-existent, write the appid file
+        if (cur.appId != ps.appId) {
+            final File appIdFile = new File(dir, "appid");
+            writeIntToFile(appIdFile, ps.appId);
+            if (DEBUG_KERNEL) Slog.d(TAG, "Mapping " + ps.name + " to " + ps.appId);
+        }
 
-        final File dir = new File(mKernelMappingFilename, ps.name);
-        dir.mkdir();
+        if (userIdsChanged) {
+            // Build the exclusion list -- the ids to add to the exclusion list
+            for (int i = 0; i < excludedUserIds.length; i++) {
+                if (cur.excludedUserIds == null || !ArrayUtils.contains(cur.excludedUserIds,
+                        excludedUserIds[i])) {
+                    writeIntToFile(new File(dir, "excluded_userids"), excludedUserIds[i]);
+                    if (DEBUG_KERNEL) Slog.d(TAG, "Writing " + excludedUserIds[i] + " to "
+                            + ps.name + "/excluded_userids");
+                }
+            }
+            // Build the inclusion list -- the ids to remove from the exclusion list
+            if (cur.excludedUserIds != null) {
+                for (int i = 0; i < cur.excludedUserIds.length; i++) {
+                    if (!ArrayUtils.contains(excludedUserIds, cur.excludedUserIds[i])) {
+                        writeIntToFile(new File(dir, "clear_userid"),
+                                cur.excludedUserIds[i]);
+                        if (DEBUG_KERNEL) Slog.d(TAG, "Writing " + cur.excludedUserIds[i] + " to "
+                                + ps.name + "/clear_userid");
 
-        final File file = new File(dir, "appid");
+                    }
+                }
+            }
+            cur.excludedUserIds = excludedUserIds;
+        }
+    }
+
+    private void writeIntToFile(File file, int value) {
         try {
-            // Note that the use of US_ASCII here is safe, we're only writing a decimal
-            // number to the file.
             FileUtils.bytesToFile(file.getAbsolutePath(),
-                    Integer.toString(ps.appId).getBytes(StandardCharsets.US_ASCII));
-            mKernelMapping.put(ps.name, ps.appId);
+                    Integer.toString(value).getBytes(StandardCharsets.US_ASCII));
         } catch (IOException ignored) {
+            Slog.w(TAG, "Couldn't write " + value + " to " + file.getAbsolutePath());
         }
     }
 
@@ -4081,6 +4132,9 @@
                         !ArrayUtils.contains(disallowedPackages, ps.name);
                 // Only system apps are initially installed.
                 ps.setInstalled(shouldInstall, userHandle);
+                if (!shouldInstall) {
+                    writeKernelMappingLPr(ps);
+                }
                 // Need to create a data directory for all apps under this user. Accumulate all
                 // required args and call the installer after mPackages lock has been released
                 volumeUuids[i] = ps.volumeUuid;
@@ -4123,6 +4177,10 @@
         mRuntimePermissionsPersistence.onUserRemovedLPw(userId);
 
         writePackageListLPr();
+
+        // Inform kernel that the user was removed, so that packages are marked uninstalled
+        // for sdcardfs
+        writeKernelRemoveUserLPr(userId);
     }
 
     void removeCrossProfileIntentFiltersLPw(int userId) {
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index 3085c9c..570259b 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -1581,6 +1581,11 @@
                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
                         + " still has an icon");
             }
+            if (si.hasMaskableBitmap() && !si.hasIconFile()) {
+                failed = true;
+                Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
+                    + " has maskable bitmap but was not saved to a file.");
+            }
             if (si.hasIconFile() && si.hasIconResource()) {
                 failed = true;
                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index d8857b7..057e781 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -1216,7 +1216,8 @@
         // he XML we'd lose the icon.  We just remove all dangling files after saving the XML.
         shortcut.setIconResourceId(0);
         shortcut.setIconResName(null);
-        shortcut.clearFlags(ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_HAS_ICON_RES);
+        shortcut.clearFlags(ShortcutInfo.FLAG_HAS_ICON_FILE |
+            ShortcutInfo.FLAG_MASKABLE_BITMAP | ShortcutInfo.FLAG_HAS_ICON_RES);
     }
 
     public void cleanupBitmapsForPackage(@UserIdInt int userId, String packageName) {
@@ -1351,7 +1352,8 @@
                         shortcut.addFlags(ShortcutInfo.FLAG_HAS_ICON_RES);
                         return;
                     }
-                    case Icon.TYPE_BITMAP: {
+                    case Icon.TYPE_BITMAP:
+                    case Icon.TYPE_BITMAP_MASKABLE: {
                         bitmap = icon.getBitmap(); // Don't recycle in this case.
                         break;
                     }
@@ -1382,6 +1384,9 @@
 
                         shortcut.setBitmapPath(out.getFile().getAbsolutePath());
                         shortcut.addFlags(ShortcutInfo.FLAG_HAS_ICON_FILE);
+                        if (icon.getType() == Icon.TYPE_BITMAP_MASKABLE) {
+                            shortcut.addFlags(ShortcutInfo.FLAG_MASKABLE_BITMAP);
+                        }
                     } finally {
                         IoUtils.closeQuietly(out);
                     }
diff --git a/services/core/java/com/android/server/wm/PinnedStackController.java b/services/core/java/com/android/server/wm/PinnedStackController.java
index a872ea4..6a8417dc 100644
--- a/services/core/java/com/android/server/wm/PinnedStackController.java
+++ b/services/core/java/com/android/server/wm/PinnedStackController.java
@@ -105,6 +105,7 @@
     private final DisplayMetrics mTmpMetrics = new DisplayMetrics();
     private final Rect mTmpInsets = new Rect();
     private final Rect mTmpRect = new Rect();
+    private final Point mTmpDisplaySize = new Point();
 
     /**
      * The callback object passed to listeners for them to notify the controller of state changes.
@@ -209,6 +210,9 @@
         final int top = (int) (stackBounds.centerY() - height / 2f);
         stackBounds.set(left, top, left + width, top + height);
         mSnapAlgorithm.applySnapFraction(stackBounds, getMovementBounds(stackBounds), snapFraction);
+        if (mIsMinimized) {
+            applyMinimizedOffset(stackBounds, getMovementBounds(stackBounds));
+        }
         return stackBounds;
     }
 
@@ -269,11 +273,7 @@
             mSnapAlgorithm.applySnapFraction(postChangeStackBounds, postChangeMovementBounds,
                     snapFraction);
             if (mIsMinimized) {
-                final Point displaySize = new Point(mDisplayInfo.logicalWidth,
-                        mDisplayInfo.logicalHeight);
-                mService.getStableInsetsLocked(displayContent.getDisplayId(), mStableInsets);
-                mSnapAlgorithm.applyMinimizedOffset(postChangeStackBounds, postChangeMovementBounds,
-                        displaySize, mStableInsets);
+                applyMinimizedOffset(postChangeStackBounds, postChangeMovementBounds);
             }
             notifyMovementBoundsChanged(false /* fromImeAdjustment */);
         }
@@ -387,6 +387,16 @@
     }
 
     /**
+     * Applies the minimized offsets to the given stack bounds.
+     */
+    private void applyMinimizedOffset(Rect stackBounds, Rect movementBounds) {
+        mTmpDisplaySize.set(mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight);
+        mService.getStableInsetsLocked(mDisplayContent.getDisplayId(), mStableInsets);
+        mSnapAlgorithm.applyMinimizedOffset(stackBounds, movementBounds, mTmpDisplaySize,
+                mStableInsets);
+    }
+
+    /**
      * @return the pixels for a given dp value.
      */
     private int dpToPx(float dpValue, DisplayMetrics dm) {
diff --git a/services/core/java/com/android/server/wm/TaskStack.java b/services/core/java/com/android/server/wm/TaskStack.java
index 544d1e3..b09d699 100644
--- a/services/core/java/com/android/server/wm/TaskStack.java
+++ b/services/core/java/com/android/server/wm/TaskStack.java
@@ -423,8 +423,12 @@
         mBoundsAfterRotation.setEmpty();
         final DockedStackDividerController controller = getDisplayContent()
                 .mDividerControllerLocked;
-        if (controller.isMinimizedDock() && mStackId == DOCKED_STACK_ID) {
-            outTempBounds.set(controller.getMiddlePositionDockedStackRect());
+        if (mStackId == DOCKED_STACK_ID) {
+            final Rect dockedStackRect = controller.getMiddlePositionDockedStackRect();
+
+            if (dockedStackRect != null) {
+                outTempBounds.set(dockedStackRect);
+            }
         }
     }
 
diff --git a/services/core/jni/Android.mk b/services/core/jni/Android.mk
index eab5d8a..2c3cda5 100644
--- a/services/core/jni/Android.mk
+++ b/services/core/jni/Android.mk
@@ -19,6 +19,7 @@
     $(LOCAL_REL_DIR)/com_android_server_location_GnssLocationProvider.cpp \
     $(LOCAL_REL_DIR)/com_android_server_power_PowerManagerService.cpp \
     $(LOCAL_REL_DIR)/com_android_server_SerialService.cpp \
+    $(LOCAL_REL_DIR)/com_android_server_SyntheticPasswordManager.cpp \
     $(LOCAL_REL_DIR)/com_android_server_storage_AppFuseBridge.cpp \
     $(LOCAL_REL_DIR)/com_android_server_SystemServer.cpp \
     $(LOCAL_REL_DIR)/com_android_server_tv_TvUinputBridge.cpp \
@@ -33,12 +34,14 @@
 
 LOCAL_C_INCLUDES += \
     $(JNI_H_INCLUDE) \
+    external/scrypt/lib/crypto \
     frameworks/base/services \
     frameworks/base/libs \
     frameworks/base/libs/hwui \
     frameworks/base/core/jni \
     frameworks/native/services \
     system/core/libappfuse/include \
+    system/gatekeeper/include \
     system/security/keystore/include \
     $(call include-path-for, libhardware)/hardware \
     $(call include-path-for, libhardware_legacy)/hardware_legacy \
@@ -50,6 +53,7 @@
     libappfuse \
     libbinder \
     libcutils \
+    libcrypto \
     liblog \
     libhardware \
     libhardware_legacy \
@@ -83,3 +87,5 @@
     android.hardware.tv.input@1.0 \
     android.hardware.vibrator@1.0 \
     android.hardware.vr@1.0 \
+
+LOCAL_STATIC_LIBRARIES += libscrypt_static
\ No newline at end of file
diff --git a/services/core/jni/com_android_server_SyntheticPasswordManager.cpp b/services/core/jni/com_android_server_SyntheticPasswordManager.cpp
new file mode 100644
index 0000000..a9f7b9f
--- /dev/null
+++ b/services/core/jni/com_android_server_SyntheticPasswordManager.cpp
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "SyntheticPasswordManager"
+
+#include "JNIHelp.h"
+#include "jni.h"
+
+#include <android_runtime/Log.h>
+#include <utils/Timers.h>
+#include <utils/misc.h>
+#include <utils/String8.h>
+#include <utils/Log.h>
+#include <gatekeeper/password_handle.h>
+
+
+extern "C" {
+#include "crypto_scrypt.h"
+}
+
+namespace android {
+
+static jlong android_server_SyntheticPasswordManager_nativeSidFromPasswordHandle(JNIEnv* env, jobject, jbyteArray handleArray) {
+
+    jbyte* data = (jbyte*)env->GetPrimitiveArrayCritical(handleArray, NULL);
+
+    if (data != NULL) {
+        const gatekeeper::password_handle_t *handle =
+                reinterpret_cast<const gatekeeper::password_handle_t *>(data);
+        jlong sid = handle->user_id;
+        env->ReleasePrimitiveArrayCritical(handleArray, data, JNI_ABORT);
+        return sid;
+    } else {
+        return 0;
+    }
+}
+
+static jbyteArray android_server_SyntheticPasswordManager_nativeScrypt(JNIEnv* env, jobject, jbyteArray password, jbyteArray salt, jint N, jint r, jint p, jint outLen) {
+    if (!password || !salt) {
+        return NULL;
+    }
+
+    int passwordLen = env->GetArrayLength(password);
+    int saltLen = env->GetArrayLength(salt);
+    jbyteArray ret = env->NewByteArray(outLen);
+
+    jbyte* passwordPtr = (jbyte*)env->GetByteArrayElements(password, NULL);
+    jbyte* saltPtr = (jbyte*)env->GetByteArrayElements(salt, NULL);
+    jbyte* retPtr = (jbyte*)env->GetByteArrayElements(ret, NULL);
+
+    int rc = crypto_scrypt((const uint8_t *)passwordPtr, passwordLen,
+                       (const uint8_t *)saltPtr, saltLen, N, r, p, (uint8_t *)retPtr,
+                       outLen);
+    env->ReleaseByteArrayElements(password, passwordPtr, JNI_ABORT);
+    env->ReleaseByteArrayElements(salt, saltPtr, JNI_ABORT);
+    env->ReleaseByteArrayElements(ret, retPtr, 0);
+
+    if (!rc) {
+        return ret;
+    } else {
+        SLOGE("scrypt failed");
+        return NULL;
+    }
+}
+
+static const JNINativeMethod sMethods[] = {
+     /* name, signature, funcPtr */
+    {"nativeSidFromPasswordHandle", "([B)J", (void*)android_server_SyntheticPasswordManager_nativeSidFromPasswordHandle},
+    {"nativeScrypt", "([B[BIIII)[B", (void*)android_server_SyntheticPasswordManager_nativeScrypt},
+};
+
+int register_android_server_SyntheticPasswordManager(JNIEnv* env) {
+    return jniRegisterNativeMethods(env, "com/android/server/SyntheticPasswordManager",
+                                    sMethods, NELEM(sMethods));
+}
+
+} /* namespace android */
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index 6f505d5..899640e 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -45,6 +45,7 @@
 int register_android_server_PersistentDataBlockService(JNIEnv* env);
 int register_android_server_Watchdog(JNIEnv* env);
 int register_android_server_HardwarePropertiesManagerService(JNIEnv* env);
+int register_android_server_SyntheticPasswordManager(JNIEnv* env);
 };
 
 using namespace android;
@@ -85,6 +86,7 @@
     register_android_server_Watchdog(env);
     register_android_server_HardwarePropertiesManagerService(env);
     register_android_server_storage_AppFuse(env);
+    register_android_server_SyntheticPasswordManager(env);
 
     return JNI_VERSION_1_4;
 }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index fb28bcf..2b2bb48 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -7211,6 +7211,7 @@
             long id = mInjector.binderClearCallingIdentity();
             try {
                 mIPackageManager.addPersistentPreferredActivity(filter, activity, userHandle);
+                mIPackageManager.flushPackageRestrictionsAsUser(userHandle);
             } catch (RemoteException re) {
                 // Shouldn't happen
             } finally {
@@ -7229,6 +7230,7 @@
             long id = mInjector.binderClearCallingIdentity();
             try {
                 mIPackageManager.clearPackagePersistentPreferredActivities(packageName, userHandle);
+                mIPackageManager.flushPackageRestrictionsAsUser(userHandle);
             } catch (RemoteException re) {
                 // Shouldn't happen
             } finally {
diff --git a/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java
index db010b8..88f1a53 100644
--- a/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -280,6 +280,21 @@
 
     @Test
     @UiThreadTest
+    public void testCancelNotificationWhilePostedAndEnqueued() throws Exception {
+        mBinderService.enqueueNotificationWithTag(mContext.getPackageName(), "opPkg", "tag", 0,
+                generateNotificationRecord(null).getNotification(), new int[1], 0);
+        waitForIdle();
+        mBinderService.enqueueNotificationWithTag(mContext.getPackageName(), "opPkg", "tag", 0,
+                generateNotificationRecord(null).getNotification(), new int[1], 0);
+        mBinderService.cancelNotificationWithTag(mContext.getPackageName(), "tag", 0, 0);
+        waitForIdle();
+        StatusBarNotification[] notifs =
+                mBinderService.getActiveNotifications(mContext.getPackageName());
+        assertEquals(0, notifs.length);
+    }
+
+    @Test
+    @UiThreadTest
     public void testCancelNotificationsFromListenerImmediatelyAfterEnqueue() throws Exception {
         final StatusBarNotification sbn = generateNotificationRecord(null).sbn;
         mBinderService.enqueueNotificationWithTag(sbn.getPackageName(), "opPkg", "tag",
diff --git a/services/tests/servicestests/src/com/android/server/BaseLockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/BaseLockSettingsServiceTests.java
index c89d35c..c6265bc 100644
--- a/services/tests/servicestests/src/com/android/server/BaseLockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/BaseLockSettingsServiceTests.java
@@ -134,5 +134,13 @@
         File storageDir = mStorage.mStorageDir;
         assertTrue(FileUtils.deleteContents(storageDir));
     }
+
+    protected static void assertArrayEquals(byte[] expected, byte[] actual) {
+        assertTrue(Arrays.equals(expected, actual));
+    }
+
+    protected static void assertArrayNotSame(byte[] expected, byte[] actual) {
+        assertFalse(Arrays.equals(expected, actual));
+    }
 }
 
diff --git a/services/tests/servicestests/src/com/android/server/LockSettingsServiceTestable.java b/services/tests/servicestests/src/com/android/server/LockSettingsServiceTestable.java
index 613ec0b..cfdb5b1 100644
--- a/services/tests/servicestests/src/com/android/server/LockSettingsServiceTestable.java
+++ b/services/tests/servicestests/src/com/android/server/LockSettingsServiceTestable.java
@@ -21,9 +21,11 @@
 import android.app.IActivityManager;
 import android.content.Context;
 import android.os.Handler;
+import android.os.Process;
+import android.os.RemoteException;
 import android.os.storage.IStorageManager;
 import android.security.KeyStore;
-import android.service.gatekeeper.IGateKeeperService;
+import android.security.keystore.KeyPermanentlyInvalidatedException;
 
 import com.android.internal.widget.LockPatternUtils;
 
@@ -38,16 +40,18 @@
         private IActivityManager mActivityManager;
         private LockPatternUtils mLockPatternUtils;
         private IStorageManager mStorageManager;
+        private MockGateKeeperService mGatekeeper;
 
         public MockInjector(Context context, LockSettingsStorage storage, KeyStore keyStore,
                 IActivityManager activityManager, LockPatternUtils lockPatternUtils,
-                IStorageManager storageManager) {
+                IStorageManager storageManager, MockGateKeeperService gatekeeper) {
             super(context);
             mLockSettingsStorage = storage;
             mKeyStore = keyStore;
             mActivityManager = activityManager;
             mLockPatternUtils = lockPatternUtils;
             mStorageManager = storageManager;
+            mGatekeeper = gatekeeper;
         }
 
         @Override
@@ -89,13 +93,25 @@
         public IStorageManager getStorageManager() {
             return mStorageManager;
         }
+
+        @Override
+        public SyntheticPasswordManager getSyntheticPasswordManager(LockSettingsStorage storage) {
+            return new MockSyntheticPasswordManager(storage, mGatekeeper);
+        }
+
+        @Override
+        public int binderGetCallingUid() {
+            return Process.SYSTEM_UID;
+        }
+
+
     }
 
     protected LockSettingsServiceTestable(Context context, LockPatternUtils lockPatternUtils,
-            LockSettingsStorage storage, IGateKeeperService gatekeeper, KeyStore keystore,
+            LockSettingsStorage storage, MockGateKeeperService gatekeeper, KeyStore keystore,
             IStorageManager storageManager, IActivityManager mActivityManager) {
         super(new MockInjector(context, storage, keystore, mActivityManager, lockPatternUtils,
-                storageManager));
+                storageManager, gatekeeper));
         mGateKeeperService = gatekeeper;
     }
 
@@ -105,12 +121,18 @@
     }
 
     @Override
-    protected String getDecryptedPasswordForTiedProfile(int userId) throws FileNotFoundException {
+    protected String getDecryptedPasswordForTiedProfile(int userId) throws FileNotFoundException, KeyPermanentlyInvalidatedException {
         byte[] storedData = mStorage.readChildProfileLock(userId);
         if (storedData == null) {
             throw new FileNotFoundException("Child profile lock file not found");
         }
+        try {
+            if (mGateKeeperService.getSecureUserId(userId) == 0) {
+                throw new KeyPermanentlyInvalidatedException();
+            }
+        } catch (RemoteException e) {
+            // shouldn't happen.
+        }
         return new String(storedData);
     }
-
 }
diff --git a/services/tests/servicestests/src/com/android/server/LockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/LockSettingsServiceTests.java
index 4c2e171..ae9762a 100644
--- a/services/tests/servicestests/src/com/android/server/LockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/LockSettingsServiceTests.java
@@ -123,6 +123,12 @@
                 UnifiedPassword, PRIMARY_USER_ID);
         mStorageManager.setIgnoreBadUnlock(false);
         assertEquals(profileSid, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID));
+
+        //Clear unified challenge
+        mService.setLockCredential(null, LockPatternUtils.CREDENTIAL_TYPE_NONE, UnifiedPassword,
+                PRIMARY_USER_ID);
+        assertEquals(0, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
+        assertEquals(0, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID));
     }
 
     public void testManagedProfileSeparateChallenge() throws RemoteException {
diff --git a/services/tests/servicestests/src/com/android/server/LockSettingsStorageTestable.java b/services/tests/servicestests/src/com/android/server/LockSettingsStorageTestable.java
index e81b02f..18da1a5 100644
--- a/services/tests/servicestests/src/com/android/server/LockSettingsStorageTestable.java
+++ b/services/tests/servicestests/src/com/android/server/LockSettingsStorageTestable.java
@@ -31,19 +31,36 @@
 
     @Override
     String getLockPatternFilename(int userId) {
-        return new File(mStorageDir,
-                super.getLockPatternFilename(userId).replace('/', '-')).getAbsolutePath();
+        return makeDirs(mStorageDir,
+                super.getLockPatternFilename(userId)).getAbsolutePath();
     }
 
     @Override
     String getLockPasswordFilename(int userId) {
-        return new File(mStorageDir,
-                super.getLockPasswordFilename(userId).replace('/', '-')).getAbsolutePath();
+        return makeDirs(mStorageDir,
+                super.getLockPasswordFilename(userId)).getAbsolutePath();
     }
 
     @Override
     String getChildProfileLockFile(int userId) {
-        return new File(mStorageDir,
-                super.getChildProfileLockFile(userId).replace('/', '-')).getAbsolutePath();
+        return makeDirs(mStorageDir,
+                super.getChildProfileLockFile(userId)).getAbsolutePath();
+    }
+
+    @Override
+    protected File getSyntheticPasswordDirectoryForUser(int userId) {
+        return makeDirs(mStorageDir, super.getSyntheticPasswordDirectoryForUser(
+                userId).getAbsolutePath());
+    }
+
+    private File makeDirs(File baseDir, String filePath) {
+        File path = new File(filePath);
+        if (path.getParent() == null) {
+            return new File(baseDir, filePath);
+        } else {
+            File mappedDir = new File(baseDir, path.getParent());
+            mappedDir.mkdirs();
+            return new File(mappedDir, path.getName());
+        }
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/LockSettingsStorageTests.java b/services/tests/servicestests/src/com/android/server/LockSettingsStorageTests.java
index d110fea..c68fbdc 100644
--- a/services/tests/servicestests/src/com/android/server/LockSettingsStorageTests.java
+++ b/services/tests/servicestests/src/com/android/server/LockSettingsStorageTests.java
@@ -329,6 +329,16 @@
         assertEquals("/data/system/users/3/gatekeeper.password.key", storage.getLockPasswordFilename(3));
     }
 
+    public void testSyntheticPasswordState() {
+        final byte[] data = {1,2,3,4};
+        mStorage.writeSyntheticPasswordState(10, 1234L, "state", data);
+        assertArrayEquals(data, mStorage.readSyntheticPasswordState(10, 1234L, "state"));
+        assertEquals(null, mStorage.readSyntheticPasswordState(0, 1234L, "state"));
+
+        mStorage.deleteSyntheticPasswordState(10, 1234L, "state", true);
+        assertEquals(null, mStorage.readSyntheticPasswordState(10, 1234L, "state"));
+    }
+
     private static void assertArrayEquals(byte[] expected, byte[] actual) {
         if (!Arrays.equals(expected, actual)) {
             fail("expected:<" + Arrays.toString(expected) +
diff --git a/services/tests/servicestests/src/com/android/server/MockGateKeeperService.java b/services/tests/servicestests/src/com/android/server/MockGateKeeperService.java
index 15983ca..bc93341 100644
--- a/services/tests/servicestests/src/com/android/server/MockGateKeeperService.java
+++ b/services/tests/servicestests/src/com/android/server/MockGateKeeperService.java
@@ -149,6 +149,15 @@
         return authTokenMap.get(uid);
     }
 
+    public AuthToken getAuthTokenForSid(long sid) {
+        for(AuthToken token : authTokenMap.values()) {
+            if (token.sid == sid) {
+                return token;
+            }
+        }
+        return null;
+    }
+
     public void clearAuthToken(int uid) {
         authTokenMap.remove(uid);
     }
diff --git a/services/tests/servicestests/src/com/android/server/MockSyntheticPasswordManager.java b/services/tests/servicestests/src/com/android/server/MockSyntheticPasswordManager.java
new file mode 100644
index 0000000..93e3fc6
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/MockSyntheticPasswordManager.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server;
+
+import android.util.ArrayMap;
+
+import junit.framework.AssertionFailedError;
+
+import java.nio.ByteBuffer;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Arrays;
+
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+
+public class MockSyntheticPasswordManager extends SyntheticPasswordManager {
+
+    private MockGateKeeperService mGateKeeper;
+
+    public MockSyntheticPasswordManager(LockSettingsStorage storage,
+            MockGateKeeperService gatekeeper) {
+        super(storage);
+        mGateKeeper = gatekeeper;
+    }
+
+    private ArrayMap<String, byte[]> mBlobs = new ArrayMap<>();
+
+    @Override
+    protected byte[] decryptSPBlob(String blobKeyName, byte[] blob, byte[] applicationId) {
+        if (mBlobs.containsKey(blobKeyName) && !Arrays.equals(mBlobs.get(blobKeyName), blob)) {
+            throw new AssertionFailedError("blobKeyName content is overwritten: " + blobKeyName);
+        }
+        ByteBuffer buffer = ByteBuffer.allocate(blob.length);
+        buffer.put(blob, 0, blob.length);
+        buffer.flip();
+        int len;
+        len = buffer.getInt();
+        byte[] data = new byte[len];
+        buffer.get(data);
+        len = buffer.getInt();
+        byte[] appId = new byte[len];
+        buffer.get(appId);
+        long sid = buffer.getLong();
+        if (!Arrays.equals(appId, applicationId)) {
+            throw new AssertionFailedError("Invalid application id");
+        }
+        if (sid != 0 && mGateKeeper.getAuthTokenForSid(sid) == null) {
+            throw new AssertionFailedError("No valid auth token");
+        }
+        return data;
+    }
+
+    @Override
+    protected byte[] createSPBlob(String blobKeyName, byte[] data, byte[] applicationId, long sid) {
+        ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES + data.length + Integer.BYTES
+                + applicationId.length + Long.BYTES);
+        buffer.putInt(data.length);
+        buffer.put(data);
+        buffer.putInt(applicationId.length);
+        buffer.put(applicationId);
+        buffer.putLong(sid);
+        byte[] result = buffer.array();
+        mBlobs.put(blobKeyName, result);
+        return result;
+    }
+
+    @Override
+    protected void destroySPBlobKey(String keyAlias) {
+    }
+
+    @Override
+    protected long sidFromPasswordHandle(byte[] handle) {
+        return new MockGateKeeperService.VerifyHandle(handle).sid;
+    }
+
+    @Override
+    protected byte[] scrypt(String password, byte[] salt, int N, int r, int p, int outLen) {
+        try {
+            PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 10, outLen * 8);
+            SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
+            return f.generateSecret(spec).getEncoded();
+        } catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+}
diff --git a/services/tests/servicestests/src/com/android/server/NetworkScoreServiceTest.java b/services/tests/servicestests/src/com/android/server/NetworkScoreServiceTest.java
index 3a88e9c..c0b79be 100644
--- a/services/tests/servicestests/src/com/android/server/NetworkScoreServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/NetworkScoreServiceTest.java
@@ -117,6 +117,7 @@
     private static final String SSID = "ssid";
     private static final String SSID_2 = "ssid_2";
     private static final String SSID_3 = "ssid_3";
+    private static final String INVALID_BSSID = "invalid_bssid";
     private static final ComponentName RECOMMENDATION_SERVICE_COMP =
             new ComponentName("newPackageName", "newScoringServiceClass");
     private static final ScoredNetwork SCORED_NETWORK =
@@ -778,6 +779,54 @@
     }
 
     @Test
+    public void testCurrentNetworkScoreCacheFilter_invalidWifiInfo_nullSsid() throws Exception {
+        when(mWifiInfo.getSSID()).thenReturn(null);
+        NetworkScoreService.CurrentNetworkScoreCacheFilter cacheFilter =
+                new NetworkScoreService.CurrentNetworkScoreCacheFilter(() -> mWifiInfo);
+
+        List<ScoredNetwork> actualList =
+                cacheFilter.apply(Lists.newArrayList(SCORED_NETWORK, SCORED_NETWORK_2));
+
+        assertTrue(actualList.isEmpty());
+    }
+
+    @Test
+    public void testCurrentNetworkScoreCacheFilter_invalidWifiInfo_noneSsid() throws Exception {
+        when(mWifiInfo.getSSID()).thenReturn(WifiSsid.NONE);
+        NetworkScoreService.CurrentNetworkScoreCacheFilter cacheFilter =
+                new NetworkScoreService.CurrentNetworkScoreCacheFilter(() -> mWifiInfo);
+
+        List<ScoredNetwork> actualList =
+                cacheFilter.apply(Lists.newArrayList(SCORED_NETWORK, SCORED_NETWORK_2));
+
+        assertTrue(actualList.isEmpty());
+    }
+
+    @Test
+    public void testCurrentNetworkScoreCacheFilter_invalidWifiInfo_emptySsid() throws Exception {
+        when(mWifiInfo.getSSID()).thenReturn("");
+        NetworkScoreService.CurrentNetworkScoreCacheFilter cacheFilter =
+                new NetworkScoreService.CurrentNetworkScoreCacheFilter(() -> mWifiInfo);
+
+        List<ScoredNetwork> actualList =
+                cacheFilter.apply(Lists.newArrayList(SCORED_NETWORK, SCORED_NETWORK_2));
+
+        assertTrue(actualList.isEmpty());
+    }
+
+    @Test
+    public void testCurrentNetworkScoreCacheFilter_invalidWifiInfo_invalidBssid() throws Exception {
+        when(mWifiInfo.getBSSID()).thenReturn(INVALID_BSSID);
+        NetworkScoreService.CurrentNetworkScoreCacheFilter cacheFilter =
+                new NetworkScoreService.CurrentNetworkScoreCacheFilter(() -> mWifiInfo);
+
+        List<ScoredNetwork> actualList =
+                cacheFilter.apply(Lists.newArrayList(SCORED_NETWORK, SCORED_NETWORK_2));
+
+        assertTrue(actualList.isEmpty());
+    }
+
+    @Test
     public void testCurrentNetworkScoreCacheFilter_scoreFiltered() throws Exception {
         NetworkScoreService.CurrentNetworkScoreCacheFilter cacheFilter =
                 new NetworkScoreService.CurrentNetworkScoreCacheFilter(() -> mWifiInfo);
@@ -813,6 +862,24 @@
     }
 
     @Test
+    public void testScanResultsScoreCacheFilter_invalidScanResults() throws Exception {
+        List<ScanResult> invalidScanResults = Lists.newArrayList(
+                new ScanResult(),
+                createScanResult("", SCORED_NETWORK.networkKey.wifiKey.bssid),
+                createScanResult(WifiSsid.NONE, SCORED_NETWORK.networkKey.wifiKey.bssid),
+                createScanResult(SSID, null),
+                createScanResult(SSID, INVALID_BSSID)
+        );
+        NetworkScoreService.ScanResultsScoreCacheFilter cacheFilter =
+                new NetworkScoreService.ScanResultsScoreCacheFilter(() -> invalidScanResults);
+
+        List<ScoredNetwork> actualList =
+                cacheFilter.apply(Lists.newArrayList(SCORED_NETWORK, SCORED_NETWORK_2));
+
+        assertTrue(actualList.isEmpty());
+    }
+
+    @Test
     public void testScanResultsScoreCacheFilter_scoresFiltered() throws Exception {
         NetworkScoreService.ScanResultsScoreCacheFilter cacheFilter =
                 new NetworkScoreService.ScanResultsScoreCacheFilter(() -> mScanResults);
diff --git a/services/tests/servicestests/src/com/android/server/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/SyntheticPasswordTests.java
new file mode 100644
index 0000000..6e5ade1
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/SyntheticPasswordTests.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server;
+
+import static com.android.internal.widget.LockPatternUtils.SYNTHETIC_PASSWORD_ENABLED_KEY;
+import static com.android.internal.widget.LockPatternUtils.SYNTHETIC_PASSWORD_HANDLE_KEY;
+
+import android.os.RemoteException;
+import android.os.UserHandle;
+
+import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.widget.VerifyCredentialResponse;
+import com.android.server.SyntheticPasswordManager.AuthenticationResult;
+import com.android.server.SyntheticPasswordManager.AuthenticationToken;
+
+
+/**
+ * runtest frameworks-services -c com.android.server.SyntheticPasswordTests
+ */
+public class SyntheticPasswordTests extends BaseLockSettingsServiceTests {
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    public void testPasswordBasedSyntheticPassword() throws RemoteException {
+        final int USER_ID = 10;
+        final String PASSWORD = "user-password";
+        final String BADPASSWORD = "bad-password";
+        MockSyntheticPasswordManager manager = new MockSyntheticPasswordManager(mStorage, mGateKeeperService);
+        AuthenticationToken authToken = manager.newSyntheticPasswordAndSid(mGateKeeperService, null,
+                null, USER_ID);
+        long handle = manager.createPasswordBasedSyntheticPassword(mGateKeeperService, PASSWORD,
+                LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, authToken, USER_ID);
+
+        AuthenticationResult result = manager.unwrapPasswordBasedSyntheticPassword(mGateKeeperService, handle, PASSWORD, USER_ID);
+        assertEquals(result.authToken.deriveKeyStorePassword(), authToken.deriveKeyStorePassword());
+
+        result = manager.unwrapPasswordBasedSyntheticPassword(mGateKeeperService, handle, BADPASSWORD, USER_ID);
+        assertNull(result.authToken);
+    }
+
+    private void disableSyntheticPassword(int userId) throws RemoteException {
+        mService.setLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 0, UserHandle.USER_SYSTEM);
+    }
+
+    private void enableSyntheticPassword(int userId) throws RemoteException {
+        mService.setLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 1, UserHandle.USER_SYSTEM);
+    }
+
+    private boolean hasSyntheticPassword(int userId) throws RemoteException {
+        return mService.getLong(SYNTHETIC_PASSWORD_HANDLE_KEY, 0, userId) != 0;
+    }
+
+    public void testPasswordMigration() throws RemoteException {
+        final String PASSWORD = "testPasswordMigration-password";
+
+        disableSyntheticPassword(PRIMARY_USER_ID);
+        mService.setLockCredential(PASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, PRIMARY_USER_ID);
+        long sid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
+        final byte[] primaryStorageKey = mStorageManager.getUserUnlockToken(PRIMARY_USER_ID);
+        enableSyntheticPassword(PRIMARY_USER_ID);
+        // Performs migration
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(PASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
+        assertEquals(sid, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
+        assertTrue(hasSyntheticPassword(PRIMARY_USER_ID));
+
+        // SP-based verification
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(PASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
+        assertArrayNotSame(primaryStorageKey, mStorageManager.getUserUnlockToken(PRIMARY_USER_ID));
+    }
+
+    private void initializeCredentialUnderSP(String password, int userId) throws RemoteException {
+        enableSyntheticPassword(userId);
+        mService.setLockCredential(password, password != null ? LockPatternUtils.CREDENTIAL_TYPE_PASSWORD : LockPatternUtils.CREDENTIAL_TYPE_NONE, null, userId);
+    }
+
+    public void testSyntheticPasswordChangeCredential() throws RemoteException {
+        final String PASSWORD = "testSyntheticPasswordChangeCredential-password";
+        final String NEWPASSWORD = "testSyntheticPasswordChangeCredential-newpassword";
+
+        initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID);
+        long sid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
+        mService.setLockCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, PASSWORD, PRIMARY_USER_ID);
+        mGateKeeperService.clearSecureUserId(PRIMARY_USER_ID);
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
+        assertEquals(sid, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
+    }
+
+    public void testSyntheticPasswordVerifyCredential() throws RemoteException {
+        final String PASSWORD = "testSyntheticPasswordVerifyCredential-password";
+        final String BADPASSWORD = "testSyntheticPasswordVerifyCredential-badpassword";
+
+        initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID);
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(PASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
+
+        assertEquals(VerifyCredentialResponse.RESPONSE_ERROR,
+                mService.verifyCredential(BADPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
+    }
+
+    public void testSyntheticPasswordClearCredential() throws RemoteException {
+        final String PASSWORD = "testSyntheticPasswordClearCredential-password";
+        final String NEWPASSWORD = "testSyntheticPasswordClearCredential-newpassword";
+
+        initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID);
+        long sid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
+        // clear password
+        mService.setLockCredential(null, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, PASSWORD, PRIMARY_USER_ID);
+        assertEquals(0 ,mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
+
+        // set a new password
+        mService.setLockCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, PRIMARY_USER_ID);
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
+        assertNotSame(sid, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
+    }
+
+    public void testSyntheticPasswordClearCredentialUntrusted() throws RemoteException {
+        final String PASSWORD = "testSyntheticPasswordClearCredential-password";
+        final String NEWPASSWORD = "testSyntheticPasswordClearCredential-newpassword";
+
+        initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID);
+        long sid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
+        // clear password
+        mService.setLockCredential(null, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, PRIMARY_USER_ID);
+        assertEquals(0 ,mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
+
+        // set a new password
+        mService.setLockCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, PRIMARY_USER_ID);
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
+        assertNotSame(sid, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
+    }
+
+    public void testSyntheticPasswordChangeCredentialUntrusted() throws RemoteException {
+        final String PASSWORD = "testSyntheticPasswordClearCredential-password";
+        final String NEWPASSWORD = "testSyntheticPasswordClearCredential-newpassword";
+
+        initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID);
+        long sid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
+        // Untrusted change password
+        mService.setLockCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, PRIMARY_USER_ID);
+        assertNotSame(0 ,mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
+        assertNotSame(sid ,mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
+
+        // Verify the password
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
+    }
+
+
+    public void testManagedProfileUnifiedChallengeMigration() throws RemoteException {
+        final String UnifiedPassword = "testManagedProfileUnifiedChallengeMigration-pwd";
+        disableSyntheticPassword(PRIMARY_USER_ID);
+        disableSyntheticPassword(MANAGED_PROFILE_USER_ID);
+        mService.setLockCredential(UnifiedPassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, PRIMARY_USER_ID);
+        mService.setSeparateProfileChallengeEnabled(MANAGED_PROFILE_USER_ID, false, null);
+        final long primarySid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
+        final long profileSid = mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID);
+        final byte[] primaryStorageKey = mStorageManager.getUserUnlockToken(PRIMARY_USER_ID);
+        final byte[] profileStorageKey = mStorageManager.getUserUnlockToken(MANAGED_PROFILE_USER_ID);
+        assertTrue(primarySid != 0);
+        assertTrue(profileSid != 0);
+        assertTrue(profileSid != primarySid);
+
+        // do migration
+        enableSyntheticPassword(PRIMARY_USER_ID);
+        enableSyntheticPassword(MANAGED_PROFILE_USER_ID);
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(UnifiedPassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
+
+        // verify
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(UnifiedPassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
+        assertEquals(primarySid, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
+        assertEquals(profileSid, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID));
+        assertArrayNotSame(primaryStorageKey, mStorageManager.getUserUnlockToken(PRIMARY_USER_ID));
+        assertArrayNotSame(profileStorageKey, mStorageManager.getUserUnlockToken(MANAGED_PROFILE_USER_ID));
+        assertTrue(hasSyntheticPassword(PRIMARY_USER_ID));
+        assertTrue(hasSyntheticPassword(MANAGED_PROFILE_USER_ID));
+    }
+
+    public void testManagedProfileSeparateChallengeMigration() throws RemoteException {
+        final String primaryPassword = "testManagedProfileSeparateChallengeMigration-primary";
+        final String profilePassword = "testManagedProfileSeparateChallengeMigration-profile";
+        mService.setLockCredential(primaryPassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, PRIMARY_USER_ID);
+        mService.setLockCredential(profilePassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, MANAGED_PROFILE_USER_ID);
+        final long primarySid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
+        final long profileSid = mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID);
+        final byte[] primaryStorageKey = mStorageManager.getUserUnlockToken(PRIMARY_USER_ID);
+        final byte[] profileStorageKey = mStorageManager.getUserUnlockToken(MANAGED_PROFILE_USER_ID);
+        assertTrue(primarySid != 0);
+        assertTrue(profileSid != 0);
+        assertTrue(profileSid != primarySid);
+
+        // do migration
+        enableSyntheticPassword(PRIMARY_USER_ID);
+        enableSyntheticPassword(MANAGED_PROFILE_USER_ID);
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(primaryPassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(profilePassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, MANAGED_PROFILE_USER_ID).getResponseCode());
+
+        // verify
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(primaryPassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(profilePassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, MANAGED_PROFILE_USER_ID).getResponseCode());
+        assertEquals(primarySid, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
+        assertEquals(profileSid, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID));
+        assertArrayNotSame(primaryStorageKey, mStorageManager.getUserUnlockToken(PRIMARY_USER_ID));
+        assertArrayNotSame(profileStorageKey, mStorageManager.getUserUnlockToken(MANAGED_PROFILE_USER_ID));
+        assertTrue(hasSyntheticPassword(PRIMARY_USER_ID));
+        assertTrue(hasSyntheticPassword(MANAGED_PROFILE_USER_ID));
+    }
+
+    public void testTokenBasedResetPassword() throws RemoteException {
+        final String PASSWORD = "password";
+        final String PATTERN = "123654";
+        final String TOKEN = "some-high-entropy-secure-token";
+        initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID);
+        final byte[] storageKey = mStorageManager.getUserUnlockToken(PRIMARY_USER_ID);
+
+        long handle = mService.addEscrowToken(TOKEN.getBytes(), PRIMARY_USER_ID);
+        assertFalse(mService.isEscrowTokenActive(handle, PRIMARY_USER_ID));
+
+        mService.verifyCredential(PASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode();
+        assertTrue(mService.isEscrowTokenActive(handle, PRIMARY_USER_ID));
+
+        mService.setLockCredentialWithToken(PATTERN, LockPatternUtils.CREDENTIAL_TYPE_PATTERN, handle, TOKEN.getBytes(), PRIMARY_USER_ID);
+
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(PATTERN, LockPatternUtils.CREDENTIAL_TYPE_PATTERN, 0, PRIMARY_USER_ID).getResponseCode());
+        assertArrayEquals(storageKey, mStorageManager.getUserUnlockToken(PRIMARY_USER_ID));
+    }
+
+    public void testTokenBasedClearPassword() throws RemoteException {
+        final String PASSWORD = "password";
+        final String PATTERN = "123654";
+        final String TOKEN = "some-high-entropy-secure-token";
+        initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID);
+        final byte[] storageKey = mStorageManager.getUserUnlockToken(PRIMARY_USER_ID);
+
+        long handle = mService.addEscrowToken(TOKEN.getBytes(), PRIMARY_USER_ID);
+        assertFalse(mService.isEscrowTokenActive(handle, PRIMARY_USER_ID));
+
+        mService.verifyCredential(PASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode();
+        assertTrue(mService.isEscrowTokenActive(handle, PRIMARY_USER_ID));
+
+        mService.setLockCredentialWithToken(null, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, handle, TOKEN.getBytes(), PRIMARY_USER_ID);
+        mService.setLockCredentialWithToken(PATTERN, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, handle, TOKEN.getBytes(), PRIMARY_USER_ID);
+
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(PATTERN, LockPatternUtils.CREDENTIAL_TYPE_PATTERN, 0, PRIMARY_USER_ID).getResponseCode());
+        assertArrayEquals(storageKey, mStorageManager.getUserUnlockToken(PRIMARY_USER_ID));
+    }
+
+    public void testTokenBasedResetPasswordAfterCredentialChanges() throws RemoteException {
+        final String PASSWORD = "password";
+        final String PATTERN = "123654";
+        final String NEWPASSWORD = "password";
+        final String TOKEN = "some-high-entropy-secure-token";
+        initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID);
+        final byte[] storageKey = mStorageManager.getUserUnlockToken(PRIMARY_USER_ID);
+
+        long handle = mService.addEscrowToken(TOKEN.getBytes(), PRIMARY_USER_ID);
+        assertFalse(mService.isEscrowTokenActive(handle, PRIMARY_USER_ID));
+
+        mService.verifyCredential(PASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode();
+        assertTrue(mService.isEscrowTokenActive(handle, PRIMARY_USER_ID));
+
+        mService.setLockCredential(PATTERN, LockPatternUtils.CREDENTIAL_TYPE_PATTERN, PASSWORD, PRIMARY_USER_ID);
+
+        mService.setLockCredentialWithToken(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, handle, TOKEN.getBytes(), PRIMARY_USER_ID);
+
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
+        assertArrayEquals(storageKey, mStorageManager.getUserUnlockToken(PRIMARY_USER_ID));
+    }
+
+    public void testEscrowTokenActivatedImmediatelyIfNoUserPasswordNeedsMigration() throws RemoteException {
+        final String TOKEN = "some-high-entropy-secure-token";
+        enableSyntheticPassword(PRIMARY_USER_ID);
+        long handle = mService.addEscrowToken(TOKEN.getBytes(), PRIMARY_USER_ID);
+        assertTrue(mService.isEscrowTokenActive(handle, PRIMARY_USER_ID));
+        assertEquals(0, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
+        assertTrue(hasSyntheticPassword(PRIMARY_USER_ID));
+    }
+
+    public void testEscrowTokenActivatedImmediatelyIfNoUserPasswordNoMigration() throws RemoteException {
+        final String TOKEN = "some-high-entropy-secure-token";
+        initializeCredentialUnderSP(null, PRIMARY_USER_ID);
+        long handle = mService.addEscrowToken(TOKEN.getBytes(), PRIMARY_USER_ID);
+        assertTrue(mService.isEscrowTokenActive(handle, PRIMARY_USER_ID));
+        assertEquals(0, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
+        assertTrue(hasSyntheticPassword(PRIMARY_USER_ID));
+    }
+
+    // b/34600579
+    //TODO: add non-migration work profile case, and unify/un-unify transition.
+    //TODO: test token after user resets password
+    //TODO: test token based reset after unified work challenge
+    //TODO: test clear password after unified work challenge
+}
+
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
index 980aa2d..0c53167 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
@@ -75,7 +75,9 @@
 import android.graphics.Bitmap;
 import android.graphics.Bitmap.CompressFormat;
 import android.graphics.BitmapFactory;
+import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
+import android.graphics.drawable.MaskableIconDrawable;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
@@ -244,6 +246,8 @@
         final Icon icon1 = Icon.createWithResource(getTestContext(), R.drawable.icon1);
         final Icon icon2 = Icon.createWithBitmap(BitmapFactory.decodeResource(
                 getTestContext().getResources(), R.drawable.icon2));
+        final Icon icon3 = Icon.createWithMaskableBitmap(BitmapFactory.decodeResource(
+            getTestContext().getResources(), R.drawable.icon2));
 
         final ShortcutInfo si1 = makeShortcut(
                 "shortcut1",
@@ -261,12 +265,18 @@
                 icon2,
                 makeIntent(Intent.ACTION_ASSIST, ShortcutActivity3.class),
                 /* weight */ 12);
-        final ShortcutInfo si3 = makeShortcut("shortcut3");
+        final ShortcutInfo si3 = makeShortcut(
+                "shortcut3",
+                "Title 3",
+                /* activity */ null,
+                icon3,
+                makeIntent(Intent.ACTION_ASSIST, ShortcutActivity3.class),
+                /* weight */ 13);
 
-        assertTrue(mManager.setDynamicShortcuts(list(si1, si2)));
+        assertTrue(mManager.setDynamicShortcuts(list(si1, si2, si3)));
         assertShortcutIds(assertAllNotKeyFieldsOnly(
                 mManager.getDynamicShortcuts()),
-                "shortcut1", "shortcut2");
+                "shortcut1", "shortcut2", "shortcut3");
         assertEquals(2, mManager.getRemainingCallCount());
 
         // TODO: Check fields
@@ -550,7 +560,7 @@
 
         final Icon bmp32x32 = Icon.createWithBitmap(BitmapFactory.decodeResource(
                 getTestContext().getResources(), R.drawable.black_32x32));
-        final Icon bmp64x64 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+        final Icon bmp64x64_maskable = Icon.createWithMaskableBitmap(BitmapFactory.decodeResource(
                 getTestContext().getResources(), R.drawable.black_64x64));
         final Icon bmp512x512 = Icon.createWithBitmap(BitmapFactory.decodeResource(
                 getTestContext().getResources(), R.drawable.black_512x512));
@@ -561,7 +571,7 @@
                 makeShortcutWithIcon("res32x32", res32x32),
                 makeShortcutWithIcon("res64x64", res64x64),
                 makeShortcutWithIcon("bmp32x32", bmp32x32),
-                makeShortcutWithIcon("bmp64x64", bmp64x64),
+                makeShortcutWithIcon("bmp64x64", bmp64x64_maskable),
                 makeShortcutWithIcon("bmp512x512", bmp512x512),
                 makeShortcut("none")
         )));
@@ -691,6 +701,15 @@
         bmp = pfdToBitmap(
                 mLauncherApps.getShortcutIconFd(CALLING_PACKAGE_1, "bmp32x32", HANDLE_USER_P0));
         assertBitmapSize(128, 128, bmp);
+
+        Drawable dr = mLauncherApps.getShortcutIconDrawable(
+            makeShortcutWithIcon("bmp64x64", bmp64x64_maskable), 0);
+        assertTrue(dr instanceof MaskableIconDrawable);
+        float viewportPercentage = 1 / (1 + 2 * MaskableIconDrawable.getExtraInsetPercentage());
+        assertEquals((int) (bmp64x64_maskable.getBitmap().getWidth() * viewportPercentage),
+            dr.getIntrinsicWidth());
+        assertEquals((int) (bmp64x64_maskable.getBitmap().getHeight() * viewportPercentage),
+            dr.getIntrinsicHeight());
     }
 
     public void testCleanupDanglingBitmaps() throws Exception {
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
index 562de414..28ec4fd 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
@@ -932,6 +932,74 @@
         dumpUserFile(USER_10);
     }
 
+    public void testShortcutInfoSaveAndLoad_maskableBitmap() throws InterruptedException {
+        mRunningUsers.put(USER_10, true);
+
+        setCaller(CALLING_PACKAGE_1, USER_10);
+
+        final Icon bmp32x32 = Icon.createWithMaskableBitmap(BitmapFactory.decodeResource(
+            getTestContext().getResources(), R.drawable.black_32x32));
+
+        PersistableBundle pb = new PersistableBundle();
+        pb.putInt("k", 1);
+        ShortcutInfo sorig = new ShortcutInfo.Builder(mClientContext)
+            .setId("id")
+            .setActivity(new ComponentName(mClientContext, ShortcutActivity2.class))
+            .setIcon(bmp32x32)
+            .setTitle("title")
+            .setText("text")
+            .setDisabledMessage("dismes")
+            .setCategories(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"))
+            .setIntent(makeIntent("action", ShortcutActivity.class, "key", "val"))
+            .setRank(123)
+            .setExtras(pb)
+            .build();
+        sorig.setTimestamp(mInjectedCurrentTimeMillis);
+
+        mManager.addDynamicShortcuts(list(sorig));
+
+        mInjectedCurrentTimeMillis += 1;
+        final long now = mInjectedCurrentTimeMillis;
+        mInjectedCurrentTimeMillis += 1;
+
+        dumpsysOnLogcat("before save");
+
+        // Save and load.
+        mService.saveDirtyInfo();
+        initService();
+        mService.handleUnlockUser(USER_10);
+
+        dumpUserFile(USER_10);
+        dumpsysOnLogcat("after load");
+
+        ShortcutInfo si;
+        si = mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "id", USER_10);
+
+        assertEquals(USER_10, si.getUserId());
+        assertEquals(HANDLE_USER_10, si.getUserHandle());
+        assertEquals(CALLING_PACKAGE_1, si.getPackage());
+        assertEquals("id", si.getId());
+        assertEquals(ShortcutActivity2.class.getName(), si.getActivity().getClassName());
+        assertEquals(null, si.getIcon());
+        assertEquals("title", si.getTitle());
+        assertEquals("text", si.getText());
+        assertEquals("dismes", si.getDisabledMessage());
+        assertEquals(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"), si.getCategories());
+        assertEquals("action", si.getIntent().getAction());
+        assertEquals("val", si.getIntent().getStringExtra("key"));
+        assertEquals(0, si.getRank());
+        assertEquals(1, si.getExtras().getInt("k"));
+
+        assertEquals(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_HAS_ICON_FILE
+            | ShortcutInfo.FLAG_STRINGS_RESOLVED | ShortcutInfo.FLAG_MASKABLE_BITMAP,
+            si.getFlags());
+        assertNotNull(si.getBitmapPath()); // Something should be set.
+        assertEquals(0, si.getIconResourceId());
+        assertTrue(si.getLastChangedTimestamp() < now);
+
+        dumpUserFile(USER_10);
+    }
+
     public void testShortcutInfoSaveAndLoad_resId() throws InterruptedException {
         mRunningUsers.put(USER_10, true);
 
diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java
index 96070b8..7964cf2 100644
--- a/telecomm/java/android/telecom/TelecomManager.java
+++ b/telecomm/java/android/telecom/TelecomManager.java
@@ -317,6 +317,15 @@
     public static final String EXTRA_CALL_BACK_NUMBER = "android.telecom.extra.CALL_BACK_NUMBER";
 
     /**
+     * The number of milliseconds that Telecom should wait after disconnecting a call via the
+     * ACTION_NEW_OUTGOING_CALL broadcast, in order to wait for the app which cancelled the call
+     * to make a new one.
+     * @hide
+     */
+    public static final String EXTRA_NEW_OUTGOING_CALL_CANCEL_TIMEOUT =
+            "android.telecom.extra.NEW_OUTGOING_CALL_CANCEL_TIMEOUT";
+
+    /**
      * A boolean meta-data value indicating whether an {@link InCallService} implements an
      * in-call user interface. Dialer implementations (see {@link #getDefaultDialerPackage()}) which
      * would also like to replace the in-call interface should set this meta-data to {@code true} in
diff --git a/test-runner/src/android/test/mock/MockPackageManager.java b/test-runner/src/android/test/mock/MockPackageManager.java
index 0dcd0f1..d51d75e 100644
--- a/test-runner/src/android/test/mock/MockPackageManager.java
+++ b/test-runner/src/android/test/mock/MockPackageManager.java
@@ -24,6 +24,7 @@
 import android.content.IntentSender;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.ChangedPackages;
 import android.content.pm.InstantAppInfo;
 import android.content.pm.FeatureInfo;
 import android.content.pm.IPackageDataObserver;
@@ -374,6 +375,12 @@
         throw new UnsupportedOperationException();
     }
 
+    /** @hide */
+    @Override
+    public ChangedPackages getChangedPackages(int sequenceNumber) {
+        throw new UnsupportedOperationException();
+    }
+
     @Override
     public ResolveInfo resolveActivity(Intent intent, int flags) {
         throw new UnsupportedOperationException();
diff --git a/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java b/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java
index 35cf903..9bc8e18 100644
--- a/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java
+++ b/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java
@@ -31,6 +31,7 @@
 import android.annotation.Nullable;
 import android.content.res.Resources.NotFoundException;
 import android.content.res.Resources.Theme;
+import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
 import android.util.DisplayMetrics;
 import android.util.TypedValue;
@@ -697,6 +698,22 @@
 
 
     /**
+     * Retrieve the Typeface for the attribute at <var>index</var>.
+     * @param index Index of attribute to retrieve.
+     *
+     * @return Typeface for the attribute, or null if not defined.
+     */
+    @Override
+    public Typeface getFont(int index) {
+        if (!hasValue(index)) {
+            return null;
+        }
+
+        ResourceValue value = mResourceData[index];
+        return ResourceHelper.getFont(value, mContext, mTheme);
+    }
+
+    /**
      * Retrieve the CharSequence[] for the attribute at <var>index</var>.
      * This gets the resource ID of the selected attribute, and uses
      * {@link Resources#getTextArray Resources.getTextArray} of the owning
diff --git a/tools/layoutlib/bridge/src/android/content/res/Resources_Delegate.java b/tools/layoutlib/bridge/src/android/content/res/Resources_Delegate.java
index e0f8e1c..d71cc6f 100644
--- a/tools/layoutlib/bridge/src/android/content/res/Resources_Delegate.java
+++ b/tools/layoutlib/bridge/src/android/content/res/Resources_Delegate.java
@@ -43,6 +43,7 @@
 import android.annotation.Nullable;
 import android.content.res.Resources.NotFoundException;
 import android.content.res.Resources.Theme;
+import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
 import android.icu.text.PluralRules;
 import android.util.AttributeSet;
@@ -779,6 +780,35 @@
     }
 
     @LayoutlibDelegate
+    static Typeface getFont(Resources resources, int id) throws
+            NotFoundException {
+        Pair<String, ResourceValue> value = getResourceValue(resources, id, mPlatformResourceFlag);
+        if (value != null) {
+            return ResourceHelper.getFont(value.getSecond(), resources.mContext, null);
+        }
+
+        throwException(resources, id);
+
+        // this is not used since the method above always throws
+        return null;
+    }
+
+    @LayoutlibDelegate
+    static Typeface getFont(Resources resources, TypedValue outValue, int id) throws
+            NotFoundException {
+        Resources_Delegate.getValue(resources, id, outValue, true);
+        if (outValue.string != null) {
+            return ResourceHelper.getFont(outValue.string.toString(), resources.mContext, null,
+                    mPlatformResourceFlag[0]);
+        }
+
+        throwException(resources, id);
+
+        // this is not used since the method above always throws
+        return null;
+    }
+
+    @LayoutlibDelegate
     static void getValue(Resources resources, int id, TypedValue outValue, boolean resolveRefs)
             throws NotFoundException {
         Pair<String, ResourceValue> value = getResourceValue(resources, id, mPlatformResourceFlag);
diff --git a/tools/layoutlib/bridge/src/android/graphics/FontFamily_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/FontFamily_Delegate.java
index a43e545..fb24c01 100644
--- a/tools/layoutlib/bridge/src/android/graphics/FontFamily_Delegate.java
+++ b/tools/layoutlib/bridge/src/android/graphics/FontFamily_Delegate.java
@@ -344,7 +344,9 @@
                     ffd.addFont(fontInfo);
                     return true;
                 }
-                fontStream = assetRepository.openAsset(path, AssetManager.ACCESS_STREAMING);
+                fontStream = isAsset ?
+                        assetRepository.openAsset(path, AssetManager.ACCESS_STREAMING) :
+                        assetRepository.openNonAsset(cookie, path, AssetManager.ACCESS_STREAMING);
                 if (fontStream == null) {
                     Bridge.getLog().error(LayoutLog.TAG_MISSING_ASSET, "Asset not found: " + path,
                             path);
diff --git a/tools/layoutlib/bridge/src/android/graphics/Typeface_Accessor.java b/tools/layoutlib/bridge/src/android/graphics/Typeface_Accessor.java
new file mode 100644
index 0000000..ce669cb
--- /dev/null
+++ b/tools/layoutlib/bridge/src/android/graphics/Typeface_Accessor.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.graphics;
+
+import android.annotation.NonNull;
+
+/**
+ * Class allowing access to package-protected methods/fields.
+ */
+public class Typeface_Accessor {
+    public static boolean isSystemFont(@NonNull String fontName) {
+        return Typeface.sSystemFontMap.containsKey(fontName);
+    }
+}
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 00799a1..0039476 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
@@ -24,6 +24,7 @@
 import android.content.IntentSender;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.ChangedPackages;
 import android.content.pm.InstantAppInfo;
 import android.content.pm.FeatureInfo;
 import android.content.pm.IPackageDataObserver;
@@ -859,6 +860,11 @@
     }
 
     @Override
+    public ChangedPackages getChangedPackages(int sequenceNumber) {
+        return null;
+    }
+
+    @Override
     public boolean isUpgrade() {
         return false;
     }
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ResourceHelper.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ResourceHelper.java
index c197e40..b3a2d3e 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ResourceHelper.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ResourceHelper.java
@@ -38,16 +38,20 @@
 import android.content.res.ColorStateList;
 import android.content.res.ComplexColor;
 import android.content.res.ComplexColor_Accessor;
+import android.content.res.FontResourcesParser;
 import android.content.res.GradientColor;
 import android.content.res.Resources.Theme;
 import android.graphics.Bitmap;
 import android.graphics.Bitmap_Delegate;
 import android.graphics.NinePatch_Delegate;
 import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.graphics.Typeface_Accessor;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.NinePatchDrawable;
+import android.text.FontConfig;
 import android.util.TypedValue;
 
 import java.io.File;
@@ -367,6 +371,89 @@
         return null;
     }
 
+    /**
+     * Returns a {@link Typeface} given a font name. The font name, can be a system font family
+     * (like sans-serif) or a full path if the font is to be loaded from resources.
+     */
+    public static Typeface getFont(String fontName, BridgeContext context, Theme theme, boolean
+            isFramework) {
+        if (fontName == null) {
+            return null;
+        }
+
+        if (Typeface_Accessor.isSystemFont(fontName)) {
+            // Shortcut for the case where we are asking for a system font name. Those are not
+            // loaded using external resources.
+            return null;
+        }
+
+        // Check if this is an asset that we've already loaded dynamically
+        Typeface typeface = Typeface.findFromCache(context.getAssets(), fontName);
+        if (typeface != null) {
+            return typeface;
+        }
+
+        String lowerCaseValue = fontName.toLowerCase();
+        if (lowerCaseValue.endsWith(".xml")) {
+            // create a block parser for the file
+            Boolean psiParserSupport = context.getLayoutlibCallback().getFlag(
+                    RenderParamsFlags.FLAG_KEY_XML_FILE_PARSER_SUPPORT);
+            XmlPullParser parser = null;
+            if (psiParserSupport != null && psiParserSupport) {
+                parser = context.getLayoutlibCallback().getXmlFileParser(fontName);
+            }
+            else {
+                File f = new File(fontName);
+                if (f.isFile()) {
+                    try {
+                        parser = ParserFactory.create(f);
+                    } catch (XmlPullParserException | FileNotFoundException e) {
+                        // this is an error and not warning since the file existence is checked before
+                        // attempting to parse it.
+                        Bridge.getLog().error(null, "Failed to parse file " + fontName,
+                                e, null /*data*/);
+                    }
+                }
+            }
+
+            if (parser != null) {
+                BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser(
+                        parser, context, isFramework);
+                try {
+                    FontConfig config = FontResourcesParser.parse(blockParser, context
+                            .getResources());
+                    typeface = Typeface.createFromResources(config, context.getAssets(),
+                            fontName);
+                } catch (XmlPullParserException | IOException e) {
+                    Bridge.getLog().error(null, "Failed to parse file " + fontName,
+                            e, null /*data*/);
+                } finally {
+                    blockParser.ensurePopped();
+                }
+            } else {
+                Bridge.getLog().error(LayoutLog.TAG_BROKEN,
+                        String.format("File %s does not exist (or is not a file)", fontName),
+                        null /*data*/);
+            }
+        } else {
+            typeface = Typeface.createFromResources(context.getAssets(), fontName, 0);
+        }
+
+        return typeface;
+    }
+
+    /**
+     * Returns a {@link Typeface} given a font name. The font name, can be a system font family
+     * (like sans-serif) or a full path if the font is to be loaded from resources.
+     */
+    public static Typeface getFont(ResourceValue value, BridgeContext context, Theme theme) {
+        if (value == null) {
+            return null;
+        }
+
+        return getFont(value.getValue(), context, theme, value.isFramework());
+    }
+
     private static Drawable getNinePatchDrawable(InputStream inputStream, Density density,
             boolean isFramework, String cacheKey, BridgeContext context) throws IOException {
         // see if we still have both the chunk and the bitmap in the caches
diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/font_test.png b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/font_test.png
new file mode 100644
index 0000000..b2baa98
--- /dev/null
+++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/font_test.png
Binary files differ
diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/font/testfamily.xml b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/font/testfamily.xml
new file mode 100644
index 0000000..b1e9206
--- /dev/null
+++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/font/testfamily.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<font-family xmlns:android="http://schemas.android.com/apk/res/android">
+    <font android:fontStyle="normal" android:fontWeight="400" android:font="@font/testfont" />
+    <font android:fontStyle="italic" android:fontWeight="400" android:font="@font/testfont2" />
+</font-family>
\ No newline at end of file
diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/font/testfont.ttf b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/font/testfont.ttf
new file mode 100644
index 0000000..2852302
--- /dev/null
+++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/font/testfont.ttf
Binary files differ
diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/font/testfont2.ttf b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/font/testfont2.ttf
new file mode 100644
index 0000000..b7bf5b4
--- /dev/null
+++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/font/testfont2.ttf
Binary files differ
diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/layout/fonts_test.xml b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/layout/fonts_test.xml
new file mode 100644
index 0000000..c63b211
--- /dev/null
+++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/layout/fonts_test.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical" android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="CONDENSED"
+        android:textSize="50sp"
+        android:fontFamily="sans-serif-condensed"
+        />
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="CONDENSED ITALIC"
+        android:textSize="30sp"
+        android:fontFamily="sans-serif-condensed"
+        android:textStyle="italic"
+        />
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="MONOSPACE"
+        android:textSize="50sp"
+        android:fontFamily="monospace"/>
+
+    <Space
+        android:layout_width="wrap_content"
+        android:layout_height="30dp" />
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="Custom"
+        android:textSize="20sp"
+        android:fontFamily="@font/testfont"/>
+
+    <Space
+        android:layout_width="wrap_content"
+        android:layout_height="30dp" />
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="Custom family"
+        android:textSize="20sp"
+        android:fontFamily="@font/testfamily"/>
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="Custom family"
+        android:textSize="20sp"
+        android:fontFamily="@font/testfamily"
+        android:textStyle="italic"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/RenderTestBase.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/RenderTestBase.java
index 3e5f9e0..67b42a7 100644
--- a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/RenderTestBase.java
+++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/RenderTestBase.java
@@ -35,6 +35,7 @@
 import com.android.layoutlib.bridge.intensive.setup.LayoutPullParser;
 import com.android.layoutlib.bridge.intensive.util.ImageUtils;
 import com.android.layoutlib.bridge.intensive.util.ModuleClassLoader;
+import com.android.layoutlib.bridge.intensive.util.TestAssetRepository;
 import com.android.layoutlib.bridge.intensive.util.TestUtils;
 import com.android.tools.layoutlib.java.System_Delegate;
 import com.android.utils.ILogger;
@@ -537,6 +538,7 @@
                         configGenerator.getHardwareConfig(), resourceResolver, layoutLibCallback, 0,
                         targetSdk, getLayoutLog());
         sessionParams.setFlag(RenderParamsFlags.FLAG_DO_NOT_RENDER_ON_CREATE, true);
+        sessionParams.setAssetRepository(new TestAssetRepository());
         return sessionParams;
     }
 }
diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/RenderTests.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/RenderTests.java
index 73e51ec..913519c 100644
--- a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/RenderTests.java
+++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/RenderTests.java
@@ -384,4 +384,10 @@
                 strings);
         assertTrue(sRenderMessages.isEmpty());
     }
+
+    @Test
+    public void testFonts() throws ClassNotFoundException {
+        // TODO: styles seem to be broken in TextView
+        renderAndVerify("fonts_test.xml", "font_test.png");
+    }
 }
diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/TestAssetRepository.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/TestAssetRepository.java
new file mode 100644
index 0000000..0856ac9
--- /dev/null
+++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/TestAssetRepository.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2017 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.layoutlib.bridge.intensive.util;
+
+import com.android.ide.common.rendering.api.AssetRepository;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * {@link AssetRepository} used for render tests.
+ */
+public class TestAssetRepository extends AssetRepository {
+    private static InputStream open(String path) throws FileNotFoundException {
+        File asset = new File(path);
+        if (asset.isFile()) {
+            return new FileInputStream(asset);
+        }
+
+        return null;
+    }
+
+    @Override
+    public boolean isSupported() {
+        return true;
+    }
+
+    @Override
+    public InputStream openAsset(String path, int mode) throws IOException {
+        return open(path);
+    }
+
+    @Override
+    public InputStream openNonAsset(int cookie, String path, int mode) throws IOException {
+        return open(path);
+    }
+}
diff --git a/tools/layoutlib/create/Android.mk b/tools/layoutlib/create/Android.mk
index c7f2c41..7611cde 100644
--- a/tools/layoutlib/create/Android.mk
+++ b/tools/layoutlib/create/Android.mk
@@ -20,7 +20,7 @@
 
 LOCAL_JAR_MANIFEST := manifest.txt
 LOCAL_STATIC_JAVA_LIBRARIES := \
-	asm-5.0
+	asm-5.2
 
 LOCAL_MODULE := layoutlib_create
 
diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java
index 94302d3..a8582c6 100644
--- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java
+++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java
@@ -156,6 +156,7 @@
         "android.content.res.Resources#getDimensionPixelOffset",
         "android.content.res.Resources#getDimensionPixelSize",
         "android.content.res.Resources#getDrawable",
+        "android.content.res.Resources#getFont",
         "android.content.res.Resources#getIntArray",
         "android.content.res.Resources#getInteger",
         "android.content.res.Resources#getLayout",
diff --git a/tools/layoutlib/create/tests/Android.mk b/tools/layoutlib/create/tests/Android.mk
index 61e381d..488d7d6 100644
--- a/tools/layoutlib/create/tests/Android.mk
+++ b/tools/layoutlib/create/tests/Android.mk
@@ -24,7 +24,7 @@
 LOCAL_MODULE_TAGS := optional
 
 LOCAL_JAVA_LIBRARIES := layoutlib_create junit-host
-LOCAL_STATIC_JAVA_LIBRARIES := asm-5.0
+LOCAL_STATIC_JAVA_LIBRARIES := asm-5.2
 
 include $(BUILD_HOST_JAVA_LIBRARY)
 
diff --git a/tools/preload2/src/com/android/preload/DeviceUtils.java b/tools/preload2/src/com/android/preload/DeviceUtils.java
index 803a7f1..18cab7b 100644
--- a/tools/preload2/src/com/android/preload/DeviceUtils.java
+++ b/tools/preload2/src/com/android/preload/DeviceUtils.java
@@ -16,13 +16,18 @@
 
 package com.android.preload;
 
+import com.android.ddmlib.AdbCommandRejectedException;
 import com.android.ddmlib.AndroidDebugBridge;
 import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
 import com.android.preload.classdataretrieval.hprof.Hprof;
 import com.android.ddmlib.DdmPreferences;
 import com.android.ddmlib.IDevice;
 import com.android.ddmlib.IShellOutputReceiver;
+import com.android.ddmlib.SyncException;
+import com.android.ddmlib.TimeoutException;
 
+import java.io.File;
+import java.io.IOException;
 import java.util.Date;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
@@ -32,6 +37,18 @@
  */
 public class DeviceUtils {
 
+  // Locations
+  private static final String PRELOADED_CLASSES_FILE = "/etc/preloaded-classes";
+  // Shell commands
+  private static final String CREATE_EMPTY_PRELOADED_CMD = "touch " + PRELOADED_CLASSES_FILE;
+  private static final String DELETE_CACHE_CMD = "rm /data/dalvik-cache/*/*boot.art";
+  private static final String DELETE_PRELOADED_CMD = "rm " + PRELOADED_CLASSES_FILE;
+  private static final String READ_PRELOADED_CMD = "cat " + PRELOADED_CLASSES_FILE;
+  private static final String START_SHELL_CMD = "start";
+  private static final String STOP_SHELL_CMD = "stop";
+  private static final String REMOUNT_SYSTEM_CMD = "mount -o rw,remount /system";
+  private static final String UNSET_BOOTCOMPLETE_CMD = "setprop dev.bootcomplete \"0\"";
+
   public static void init(int debugPort) {
     DdmPreferences.setSelectedDebugPort(debugPort);
 
@@ -119,43 +136,56 @@
     return !ret.contains("No such file or directory");
   }
 
-  /**
-   * Remove files involved in a standard build that interfere with collecting data. This will
-   * remove /etc/preloaded-classes, which determines which classes are allocated already in the
-   * boot image. It also deletes any compiled boot image on the device. Then it restarts the
-   * device.
-   *
-   * This is a potentially long-running operation, as the boot after the deletion may take a while.
-   * The method will abort after the given timeout.
-   */
-  public static boolean removePreloaded(IDevice device, long preloadedWaitTimeInSeconds) {
-    String oldContent =
-        DeviceUtils.doShellReturnString(device, "cat /etc/preloaded-classes", 1, TimeUnit.SECONDS);
-    if (oldContent.trim().equals("")) {
-      System.out.println("Preloaded-classes already empty.");
-      return true;
-    }
+    /**
+     * Write over the preloaded-classes file with an empty or existing file and regenerate the boot
+     * image as necessary.
+     *
+     * @param device
+     * @param pcFile
+     * @param bootTimeout
+     * @throws AdbCommandRejectedException
+     * @throws IOException
+     * @throws TimeoutException
+     * @throws SyncException
+     * @return true if successfully overwritten, false otherwise
+     */
+    public static boolean overwritePreloaded(IDevice device, File pcFile, long bootTimeout)
+            throws AdbCommandRejectedException, IOException, TimeoutException, SyncException {
+        boolean writeEmpty = (pcFile == null);
+        if (writeEmpty) {
+            // Check if the preloaded-classes file is already empty.
+            String oldContent =
+                    doShellReturnString(device, READ_PRELOADED_CMD, 1, TimeUnit.SECONDS);
+            if (oldContent.trim().equals("")) {
+                System.out.println("Preloaded-classes already empty.");
+                return true;
+            }
+        }
 
-    // Stop the system server etc.
-    doShell(device, "stop", 100, TimeUnit.MILLISECONDS);
+        // Stop the system server etc.
+        doShell(device, STOP_SHELL_CMD, 1, TimeUnit.SECONDS);
+        // Remount the read-only system partition
+        doShell(device, REMOUNT_SYSTEM_CMD, 1, TimeUnit.SECONDS);
+        // Delete the preloaded-classes file
+        doShell(device, DELETE_PRELOADED_CMD, 1, TimeUnit.SECONDS);
+        // Delete the dalvik cache files
+        doShell(device, DELETE_CACHE_CMD, 1, TimeUnit.SECONDS);
+        if (writeEmpty) {
+            // Write an empty preloaded-classes file
+            doShell(device, CREATE_EMPTY_PRELOADED_CMD, 500, TimeUnit.MILLISECONDS);
+        } else {
+            // Push the new preloaded-classes file
+            device.pushFile(pcFile.getAbsolutePath(), PRELOADED_CLASSES_FILE);
+        }
+        // Manually reset the boot complete flag
+        doShell(device, UNSET_BOOTCOMPLETE_CMD, 1, TimeUnit.SECONDS);
+        // Restart system server on the device
+        doShell(device, START_SHELL_CMD, 1, TimeUnit.SECONDS);
+        // Wait for the boot complete flag and return the outcome.
+        return waitForBootComplete(device, bootTimeout);
+  }
 
-    // Remount /system, delete /etc/preloaded-classes. It would be nice to use "adb remount,"
-    // but AndroidDebugBridge doesn't expose it.
-    doShell(device, "mount -o remount,rw /system", 500, TimeUnit.MILLISECONDS);
-    doShell(device, "rm /etc/preloaded-classes", 100, TimeUnit.MILLISECONDS);
-    // We do need an empty file.
-    doShell(device, "touch /etc/preloaded-classes", 100, TimeUnit.MILLISECONDS);
-
-    // Delete the files in the dalvik cache.
-    doShell(device, "rm /data/dalvik-cache/*/*boot.art", 500, TimeUnit.MILLISECONDS);
-
-    // We'll try to use dev.bootcomplete to know when the system server is back up. But stop
-    // doesn't reset it, so do it manually.
-    doShell(device, "setprop dev.bootcomplete \"0\"", 500, TimeUnit.MILLISECONDS);
-
-    // Start the system server.
-    doShell(device, "start", 100, TimeUnit.MILLISECONDS);
-
+  private static boolean waitForBootComplete(IDevice device, long timeout) {
     // Do a loop checking each second whether bootcomplete. Wait for at most the given
     // threshold.
     Date startDate = new Date();
@@ -178,7 +208,7 @@
       Date endDate = new Date();
       long seconds =
           TimeUnit.SECONDS.convert(endDate.getTime() - startDate.getTime(), TimeUnit.MILLISECONDS);
-      if (seconds > preloadedWaitTimeInSeconds) {
+      if (seconds > timeout) {
         return false;
       }
     }
diff --git a/tools/preload2/src/com/android/preload/Main.java b/tools/preload2/src/com/android/preload/Main.java
index c42a19b..2265e95 100644
--- a/tools/preload2/src/com/android/preload/Main.java
+++ b/tools/preload2/src/com/android/preload/Main.java
@@ -29,6 +29,7 @@
 import com.android.preload.actions.ScanAllPackagesAction;
 import com.android.preload.actions.ScanPackageAction;
 import com.android.preload.actions.ShowDataAction;
+import com.android.preload.actions.WritePreloadedClassesAction;
 import com.android.preload.classdataretrieval.ClassDataRetriever;
 import com.android.preload.classdataretrieval.hprof.Hprof;
 import com.android.preload.classdataretrieval.jdwp.JDWPClassDataRetriever;
@@ -96,6 +97,7 @@
     public final static String COMPUTE_FILE_CMD = "comp";
     public final static String EXPORT_CMD = "export";
     public final static String IMPORT_CMD = "import";
+    public final static String WRITE_CMD = "write";
 
     /**
      * @param args
@@ -132,6 +134,7 @@
                 null));
         actions.add(new ComputeThresholdXAction("Compute(X)", dataTableModel,
                 CLASS_PRELOAD_BLACKLIST));
+        actions.add(new WritePreloadedClassesAction(clientUtils, null, dataTableModel));
         actions.add(new ShowDataAction(dataTableModel));
         actions.add(new ImportAction(dataTableModel));
         actions.add(new ExportAction(dataTableModel));
@@ -200,6 +203,11 @@
                     ui.input(it.next());
                     ui.confirmYes();
                     ui.output(new File(it.next()));
+                // Operation: Write preloaded classes from a specific file
+                } else if (WRITE_CMD.equals(op)) {
+                    System.out.println("Writing preloaded classes.");
+                    ui.action(WritePreloadedClassesAction.class);
+                    ui.input(new File(it.next()));
                 }
             }
         } catch (NoSuchElementException e) {
@@ -305,8 +313,16 @@
 
             Main.getUI().showMessageDialog("The device will reboot. This will potentially take a "
                     + "long time. Please be patient.");
-            if (!DeviceUtils.removePreloaded(device, 15 * 60) /* 15m timeout */) {
-                Main.getUI().showMessageDialog("Removing preloaded-classes failed unexpectedly!");
+            boolean success = false;
+            try {
+                success = DeviceUtils.overwritePreloaded(device, null, 15 * 60);
+            } catch (Exception e) {
+                System.err.println(e);
+            } finally {
+                if (!success) {
+                    Main.getUI().showMessageDialog(
+                            "Removing preloaded-classes failed unexpectedly!");
+                }
             }
         }
     }
diff --git a/tools/preload2/src/com/android/preload/actions/WritePreloadedClassesAction.java b/tools/preload2/src/com/android/preload/actions/WritePreloadedClassesAction.java
new file mode 100644
index 0000000..9b97f11
--- /dev/null
+++ b/tools/preload2/src/com/android/preload/actions/WritePreloadedClassesAction.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.preload.actions;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.IDevice;
+import com.android.preload.ClientUtils;
+import com.android.preload.DeviceUtils;
+import com.android.preload.DumpData;
+import com.android.preload.DumpTableModel;
+import com.android.preload.Main;
+
+import java.awt.event.ActionEvent;
+import java.io.File;
+import java.util.Date;
+import java.util.Map;
+
+public class WritePreloadedClassesAction extends AbstractThreadedDeviceSpecificAction {
+    private File preloadedClassFile;
+
+    public WritePreloadedClassesAction(ClientUtils utils, IDevice device, DumpTableModel dataTableModel) {
+        super("Write preloaded classes action", device);
+    }
+
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        File[] files = Main.getUI().showOpenDialog(true);
+        if (files != null && files.length > 0) {
+            preloadedClassFile = files[0];
+            super.actionPerformed(e);
+        }
+    }
+
+    @Override
+    public void run() {
+        Main.getUI().showWaitDialog();
+        try {
+            // Write the new file with a 5-minute timeout
+            DeviceUtils.overwritePreloaded(device, preloadedClassFile, 5 * 60);
+        } catch (Exception e) {
+            System.err.println(e);
+        } finally {
+            Main.getUI().hideWaitDialog();
+        }
+    }
+}