Merge "Revert "Add Build.isAtLeastO""
diff --git a/Android.mk b/Android.mk
index bd04214..466a7d9 100644
--- a/Android.mk
+++ b/Android.mk
@@ -487,7 +487,7 @@
 	../../system/update_engine/binder_bindings/android/os/IUpdateEngine.aidl \
 	../../system/update_engine/binder_bindings/android/os/IUpdateEngineCallback.aidl \
 
-LOCAL_SRC_FILES +=  \
+LOCAL_SRC_FILES += \
 	../../system/netd/server/binder/android/net/INetd.aidl \
 
 LOCAL_AIDL_INCLUDES += system/update_engine/binder_bindings
@@ -519,6 +519,7 @@
 LOCAL_STATIC_JAVA_LIBRARIES :=                          \
     framework-protos                                    \
     android.hardware.thermal@1.0-java-constants         \
+    android.hardware.health@1.0-java-constants          \
 
 LOCAL_MODULE := framework
 
diff --git a/api/current.txt b/api/current.txt
index a267ae5..873957b 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -4954,9 +4954,9 @@
     ctor public Notification(android.os.Parcel);
     method public android.app.Notification clone();
     method public int describeContents();
+    method public java.lang.String getChannel();
     method public java.lang.String getGroup();
     method public android.graphics.drawable.Icon getLargeIcon();
-    method public java.lang.String getNotificationChannel();
     method public android.graphics.drawable.Icon getSmallIcon();
     method public java.lang.String getSortKey();
     method public void writeToParcel(android.os.Parcel, int);
@@ -36730,6 +36730,7 @@
     field public static final int CAPABILITY_CONNECTION_MANAGER = 1; // 0x1
     field public static final int CAPABILITY_PLACE_EMERGENCY_CALLS = 16; // 0x10
     field public static final int CAPABILITY_SIM_SUBSCRIPTION = 4; // 0x4
+    field public static final int CAPABILITY_SUPPORTS_VIDEO_CALLING = 1024; // 0x400
     field public static final int CAPABILITY_VIDEO_CALLING = 8; // 0x8
     field public static final int CAPABILITY_VIDEO_CALLING_RELIES_ON_PRESENCE = 256; // 0x100
     field public static final android.os.Parcelable.Creator<android.telecom.PhoneAccount> CREATOR;
@@ -37251,8 +37252,12 @@
     method public int describeContents();
     method public boolean equals(java.lang.Object);
     method public int getAsuLevel();
+    method public int getCqi();
     method public int getDbm();
     method public int getLevel();
+    method public int getRsrp();
+    method public int getRsrq();
+    method public int getRssnr();
     method public int getTimingAdvance();
     method public int hashCode();
     method public void writeToParcel(android.os.Parcel, int);
@@ -45094,6 +45099,7 @@
     field public static final int IME_FLAG_NO_ENTER_ACTION = 1073741824; // 0x40000000
     field public static final int IME_FLAG_NO_EXTRACT_UI = 268435456; // 0x10000000
     field public static final int IME_FLAG_NO_FULLSCREEN = 33554432; // 0x2000000
+    field public static final int IME_FLAG_NO_PERSONALIZED_LEARNING = 16777216; // 0x1000000
     field public static final int IME_MASK_ACTION = 255; // 0xff
     field public static final int IME_NULL = 0; // 0x0
     field public int actionId;
diff --git a/api/system-current.txt b/api/system-current.txt
index 4f58ed0..88f25a8 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -5107,9 +5107,9 @@
     ctor public Notification(android.os.Parcel);
     method public android.app.Notification clone();
     method public int describeContents();
+    method public java.lang.String getChannel();
     method public java.lang.String getGroup();
     method public android.graphics.drawable.Icon getLargeIcon();
-    method public java.lang.String getNotificationChannel();
     method public android.graphics.drawable.Icon getSmallIcon();
     method public java.lang.String getSortKey();
     method public void writeToParcel(android.os.Parcel, int);
@@ -39742,6 +39742,7 @@
     field public static final int CAPABILITY_MULTI_USER = 32; // 0x20
     field public static final int CAPABILITY_PLACE_EMERGENCY_CALLS = 16; // 0x10
     field public static final int CAPABILITY_SIM_SUBSCRIPTION = 4; // 0x4
+    field public static final int CAPABILITY_SUPPORTS_VIDEO_CALLING = 1024; // 0x400
     field public static final int CAPABILITY_VIDEO_CALLING = 8; // 0x8
     field public static final int CAPABILITY_VIDEO_CALLING_RELIES_ON_PRESENCE = 256; // 0x100
     field public static final android.os.Parcelable.Creator<android.telecom.PhoneAccount> CREATOR;
@@ -40329,8 +40330,12 @@
     method public int describeContents();
     method public boolean equals(java.lang.Object);
     method public int getAsuLevel();
+    method public int getCqi();
     method public int getDbm();
     method public int getLevel();
+    method public int getRsrp();
+    method public int getRsrq();
+    method public int getRssnr();
     method public int getTimingAdvance();
     method public int hashCode();
     method public void writeToParcel(android.os.Parcel, int);
@@ -48264,6 +48269,7 @@
     field public static final int IME_FLAG_NO_ENTER_ACTION = 1073741824; // 0x40000000
     field public static final int IME_FLAG_NO_EXTRACT_UI = 268435456; // 0x10000000
     field public static final int IME_FLAG_NO_FULLSCREEN = 33554432; // 0x2000000
+    field public static final int IME_FLAG_NO_PERSONALIZED_LEARNING = 16777216; // 0x1000000
     field public static final int IME_MASK_ACTION = 255; // 0xff
     field public static final int IME_NULL = 0; // 0x0
     field public int actionId;
diff --git a/api/test-current.txt b/api/test-current.txt
index ad56e8f4b0..5d08832 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -4964,9 +4964,9 @@
     ctor public Notification(android.os.Parcel);
     method public android.app.Notification clone();
     method public int describeContents();
+    method public java.lang.String getChannel();
     method public java.lang.String getGroup();
     method public android.graphics.drawable.Icon getLargeIcon();
-    method public java.lang.String getNotificationChannel();
     method public android.graphics.drawable.Icon getSmallIcon();
     method public java.lang.String getSortKey();
     method public void writeToParcel(android.os.Parcel, int);
@@ -36820,6 +36820,7 @@
     field public static final int CAPABILITY_CONNECTION_MANAGER = 1; // 0x1
     field public static final int CAPABILITY_PLACE_EMERGENCY_CALLS = 16; // 0x10
     field public static final int CAPABILITY_SIM_SUBSCRIPTION = 4; // 0x4
+    field public static final int CAPABILITY_SUPPORTS_VIDEO_CALLING = 1024; // 0x400
     field public static final int CAPABILITY_VIDEO_CALLING = 8; // 0x8
     field public static final int CAPABILITY_VIDEO_CALLING_RELIES_ON_PRESENCE = 256; // 0x100
     field public static final android.os.Parcelable.Creator<android.telecom.PhoneAccount> CREATOR;
@@ -37341,8 +37342,12 @@
     method public int describeContents();
     method public boolean equals(java.lang.Object);
     method public int getAsuLevel();
+    method public int getCqi();
     method public int getDbm();
     method public int getLevel();
+    method public int getRsrp();
+    method public int getRsrq();
+    method public int getRssnr();
     method public int getTimingAdvance();
     method public int hashCode();
     method public void writeToParcel(android.os.Parcel, int);
@@ -45341,6 +45346,7 @@
     field public static final int IME_FLAG_NO_ENTER_ACTION = 1073741824; // 0x40000000
     field public static final int IME_FLAG_NO_EXTRACT_UI = 268435456; // 0x10000000
     field public static final int IME_FLAG_NO_FULLSCREEN = 33554432; // 0x2000000
+    field public static final int IME_FLAG_NO_PERSONALIZED_LEARNING = 16777216; // 0x1000000
     field public static final int IME_MASK_ACTION = 255; // 0xff
     field public static final int IME_NULL = 0; // 0x0
     field public int actionId;
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index a3414f4..78204a0 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -5839,6 +5839,7 @@
                     case BlockedNumberContract.AUTHORITY:
                     case CalendarContract.AUTHORITY:
                     case Downloads.Impl.AUTHORITY:
+                    case "telephony":
                         Binder.allowBlocking(provider.asBinder());
                 }
             }
diff --git a/core/java/android/app/IUiModeManager.aidl b/core/java/android/app/IUiModeManager.aidl
index cae54b6..848464c 100644
--- a/core/java/android/app/IUiModeManager.aidl
+++ b/core/java/android/app/IUiModeManager.aidl
@@ -53,6 +53,16 @@
     int getNightMode();
 
     /**
+     * Sets whith theme overlays to use within /vendor/overlay.
+     */
+    void setTheme(String theme);
+
+    /**
+     * Gets whith theme overlays to use within /vendor/overlay.
+     */
+    String getTheme();
+
+    /**
      * Tells if UI mode is locked or not.
      */
     boolean isUiModeLocked();
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index bdb2685..261fde2 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -2239,7 +2239,7 @@
     /**
      * Returns the id of the channel this notification posts to.
      */
-    public String getNotificationChannel() {
+    public String getChannel() {
         return mChannelId;
     }
 
diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java
index 2d15beb..45831a3 100644
--- a/core/java/android/app/ResourcesManager.java
+++ b/core/java/android/app/ResourcesManager.java
@@ -826,7 +826,8 @@
 
             for (int i = mResourceImpls.size() - 1; i >= 0; i--) {
                 ResourcesKey key = mResourceImpls.keyAt(i);
-                ResourcesImpl r = mResourceImpls.valueAt(i).get();
+                WeakReference<ResourcesImpl> weakImplRef = mResourceImpls.valueAt(i);
+                ResourcesImpl r = weakImplRef != null ? weakImplRef.get() : null;
                 if (r != null) {
                     if (DEBUG || DEBUG_CONFIGURATION) Slog.v(TAG, "Changing resources "
                             + r + " config to: " + config);
@@ -890,8 +891,9 @@
 
             final int implCount = mResourceImpls.size();
             for (int i = 0; i < implCount; i++) {
-                final ResourcesImpl impl = mResourceImpls.valueAt(i).get();
                 final ResourcesKey key = mResourceImpls.keyAt(i);
+                final WeakReference<ResourcesImpl> weakImplRef = mResourceImpls.valueAt(i);
+                final ResourcesImpl impl = weakImplRef != null ? weakImplRef.get() : null;
                 if (impl != null && key.mResDir.equals(assetPath)) {
                     if (!ArrayUtils.contains(key.mLibDirs, libAsset)) {
                         final int newLibAssetCount = 1 +
diff --git a/core/java/android/app/UiModeManager.java b/core/java/android/app/UiModeManager.java
index 0046b0e..2e21729 100644
--- a/core/java/android/app/UiModeManager.java
+++ b/core/java/android/app/UiModeManager.java
@@ -241,6 +241,35 @@
     }
 
     /**
+     * Sets the vendor theme overlay property, then triggers a reboot.
+     * @hide
+     */
+    public void setTheme(String theme) {
+        if (mService != null) {
+            try {
+                mService.setTheme(theme);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /**
+     * Gets the vendor theme overlay property.
+     * @hide
+     */
+    public String getTheme() {
+        if (mService != null) {
+            try {
+                return mService.getTheme();
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+        return null;
+    }
+
+    /**
      * Returns the currently configured night mode.
      * <p>
      * May be one of:
diff --git a/core/java/android/app/admin/DeviceAdminReceiver.java b/core/java/android/app/admin/DeviceAdminReceiver.java
index 360087c..cbd5a6d 100644
--- a/core/java/android/app/admin/DeviceAdminReceiver.java
+++ b/core/java/android/app/admin/DeviceAdminReceiver.java
@@ -285,6 +285,27 @@
             = "android.app.action.NETWORK_LOGS_AVAILABLE";
 
     /**
+     * A {@code long} containing a token of the current batch of network logs, that has to be used
+     * to retrieve the batch of logs by the device owner.
+     *
+     * @see #ACTION_NETWORK_LOGS_AVAILABLE
+     * @see DevicePolicyManager#retrieveNetworkLogs
+     * @hide
+     */
+    public static final String EXTRA_NETWORK_LOGS_TOKEN =
+            "android.app.extra.EXTRA_NETWORK_LOGS_TOKEN";
+
+    /**
+     * An {@code int} count representing a total count of network logs inside the current batch of
+     * network logs.
+     *
+     * @see #ACTION_NETWORK_LOGS_AVAILABLE
+     * @hide
+     */
+    public static final String EXTRA_NETWORK_LOGS_COUNT =
+            "android.app.extra.EXTRA_NETWORK_LOGS_COUNT";
+
+    /**
      * A string containing the SHA-256 hash of the bugreport file.
      *
      * @see #ACTION_BUGREPORT_SHARE
@@ -644,19 +665,22 @@
     }
 
     /**
-     * Called when a new batch of network logs can be retrieved. This callback method will only ever
-     * be called when network logging is enabled. The logs can only be retrieved while network
+     * Called each time a new batch of network logs can be retrieved. This callback method will only
+     * ever be called when network logging is enabled. The logs can only be retrieved while network
      * logging is enabled.
      *
      * <p>This callback is only applicable to device owners.
      *
      * @param context The running context as per {@link #onReceive}.
      * @param intent The received intent as per {@link #onReceive}.
+     * @param batchToken The token representing the current batch of network logs.
+     * @param networkLogsCount The total count of events in the current batch of network logs.
      * @see DevicePolicyManager#retrieveNetworkLogs(ComponentName)
      *
      * @hide
      */
-    public void onNetworkLogsAvailable(Context context, Intent intent) {
+    public void onNetworkLogsAvailable(Context context, Intent intent, long batchToken,
+            int networkLogsCount) {
     }
 
     /**
@@ -714,7 +738,9 @@
         } else if (ACTION_SECURITY_LOGS_AVAILABLE.equals(action)) {
             onSecurityLogsAvailable(context, intent);
         } else if (ACTION_NETWORK_LOGS_AVAILABLE.equals(action)) {
-            onNetworkLogsAvailable(context, intent);
+            long batchToken = intent.getLongExtra(EXTRA_NETWORK_LOGS_TOKEN, -1);
+            int networkLogsCount = intent.getIntExtra(EXTRA_NETWORK_LOGS_COUNT, 0);
+            onNetworkLogsAvailable(context, intent, batchToken, networkLogsCount);
         }
     }
 }
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 538e52b..1ab809d 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -6649,8 +6649,6 @@
      * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
      * @param enabled whether network logging should be enabled or not.
      * @throws {@link SecurityException} if {@code admin} is not a device owner.
-     * @throws {@link RemoteException} if network logging could not be enabled or disabled due to
-     *         the logging service not being available
      * @see #retrieveNetworkLogs
      *
      * @hide
@@ -6683,7 +6681,10 @@
     }
 
     /**
-     * Called by device owner to retrieve a new batch of network logging events.
+     * Called by device owner to retrieve the most recent batch of network logging events.
+     * A device owner has to provide a batchToken provided as part of
+     * {@link DeviceAdminReceiver#onNetworkLogsAvailable} callback. If the token doesn't match the
+     * token of the most recent available batch of logs, {@code null} will be returned.
      *
      * <p> {@link NetworkEvent} can be one of {@link DnsEvent} or {@link ConnectEvent}.
      *
@@ -6694,16 +6695,20 @@
      * {@link DeviceAdminReceiver#onNetworkLogsAvailable}.
      *
      * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+     * @param batchToken A token of the batch to retrieve
      * @return A new batch of network logs which is a list of {@link NetworkEvent}. Returns
-     * {@code null} if there's no batch currently awaiting for retrieval or if logging is disabled.
+     *        {@code null} if the batch represented by batchToken is no longer available or if
+     *        logging is disabled.
      * @throws {@link SecurityException} if {@code admin} is not a device owner.
+     * @see DeviceAdminReceiver#onNetworkLogsAvailable
      *
      * @hide
      */
-    public List<NetworkEvent> retrieveNetworkLogs(@NonNull ComponentName admin) {
+    public @Nullable List<NetworkEvent> retrieveNetworkLogs(@NonNull ComponentName admin,
+            long batchToken) {
         throwIfParentInstance("retrieveNetworkLogs");
         try {
-            return mService.retrieveNetworkLogs(admin);
+            return mService.retrieveNetworkLogs(admin, batchToken);
         } catch (RemoteException re) {
             throw re.rethrowFromSystemServer();
         }
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index b0aec8c..729d12b 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -318,5 +318,5 @@
 
     void setNetworkLoggingEnabled(in ComponentName admin, boolean enabled);
     boolean isNetworkLoggingEnabled(in ComponentName admin);
-    List<NetworkEvent> retrieveNetworkLogs(in ComponentName admin);
+    List<NetworkEvent> retrieveNetworkLogs(in ComponentName admin, long batchToken);
 }
diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java
index bb344a6..b0e27a4 100644
--- a/core/java/android/bluetooth/BluetoothAdapter.java
+++ b/core/java/android/bluetooth/BluetoothAdapter.java
@@ -604,10 +604,6 @@
      */
     public BluetoothLeAdvertiser getBluetoothLeAdvertiser() {
         if (!getLeAccess()) return null;
-        if (!isMultipleAdvertisementSupported() && !isPeripheralModeSupported()) {
-            Log.e(TAG, "Bluetooth LE advertising not supported");
-            return null;
-        }
         synchronized(mLock) {
             if (sBluetoothLeAdvertiser == null) {
                 sBluetoothLeAdvertiser = new BluetoothLeAdvertiser(mManagerService);
@@ -1357,24 +1353,6 @@
     }
 
     /**
-     * Returns whether peripheral mode is supported.
-     *
-     * @hide
-     */
-    public boolean isPeripheralModeSupported() {
-        if (getState() != STATE_ON) return false;
-        try {
-            mServiceLock.readLock().lock();
-            if (mService != null) return mService.isPeripheralModeSupported();
-        } catch (RemoteException e) {
-            Log.e(TAG, "failed to get peripheral mode capability: ", e);
-        } finally {
-            mServiceLock.readLock().unlock();
-        }
-        return false;
-    }
-
-    /**
      * Return true if offloaded filters are supported
      *
      * @return true if chipset supports on-chip filtering
diff --git a/core/java/android/bluetooth/IBluetooth.aidl b/core/java/android/bluetooth/IBluetooth.aidl
index 96a1ae8..7c5458b 100644
--- a/core/java/android/bluetooth/IBluetooth.aidl
+++ b/core/java/android/bluetooth/IBluetooth.aidl
@@ -100,7 +100,6 @@
     boolean factoryReset();
 
     boolean isMultiAdvertisementSupported();
-    boolean isPeripheralModeSupported();
     boolean isOffloadedFilteringSupported();
     boolean isOffloadedScanBatchingSupported();
     boolean isActivityAndEnergyReportingSupported();
diff --git a/core/java/android/bluetooth/le/BluetoothLeAdvertiser.java b/core/java/android/bluetooth/le/BluetoothLeAdvertiser.java
index 26f2dea..5d27662 100644
--- a/core/java/android/bluetooth/le/BluetoothLeAdvertiser.java
+++ b/core/java/android/bluetooth/le/BluetoothLeAdvertiser.java
@@ -111,12 +111,6 @@
             if (callback == null) {
                 throw new IllegalArgumentException("callback cannot be null");
             }
-            if (!mBluetoothAdapter.isMultipleAdvertisementSupported() &&
-                    !mBluetoothAdapter.isPeripheralModeSupported()) {
-                postStartFailure(callback,
-                        AdvertiseCallback.ADVERTISE_FAILED_FEATURE_UNSUPPORTED);
-                return;
-            }
             boolean isConnectable = settings.isConnectable();
             if (totalBytes(advertiseData, isConnectable) > MAX_ADVERTISING_DATA_BYTES ||
                     totalBytes(scanResponse, false) > MAX_ADVERTISING_DATA_BYTES) {
@@ -236,9 +230,9 @@
         private final AdvertiseSettings mSettings;
         private final IBluetoothGatt mBluetoothGatt;
 
-        // mAdvertiserId 0: not registered
-        // -1: advertise stopped or registration timeout
-        // >0: registered and advertising started
+        // mAdvertiserId -1: not registered
+        // -2: advertise stopped or registration timeout
+        // >=0: registered and advertising started
         private int mAdvertiserId;
         private boolean mIsAdvertising = false;
 
@@ -251,12 +245,12 @@
             mScanResponse = scanResponse;
             mSettings = settings;
             mBluetoothGatt = bluetoothGatt;
-            mAdvertiserId = 0;
+            mAdvertiserId = -1;
         }
 
         public void startRegisteration() {
             synchronized (this) {
-                if (mAdvertiserId == -1) return;
+                if (mAdvertiserId == -2) return;
 
                 try {
                     mBluetoothGatt.registerAdvertiser(this);
@@ -264,13 +258,13 @@
                 } catch (InterruptedException | RemoteException e) {
                     Log.e(TAG, "Failed to start registeration", e);
                 }
-                if (mAdvertiserId > 0 && mIsAdvertising) {
+                if (mAdvertiserId >= 0 && mIsAdvertising) {
                     mLeAdvertisers.put(mAdvertiseCallback, this);
-                } else if (mAdvertiserId <= 0) {
+                } else if (mAdvertiserId < 0) {
 
                     // Registration timeout, reset mClientIf to -1 so no subsequent operations can
                     // proceed.
-                    if (mAdvertiserId == 0) mAdvertiserId = -1;
+                    if (mAdvertiserId == 0) mAdvertiserId = -2;
                     // Post internal error if registration failed.
                     postStartFailure(mAdvertiseCallback,
                             AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR);
@@ -278,7 +272,7 @@
                     // Unregister application if it's already registered but advertise failed.
                     try {
                         mBluetoothGatt.unregisterAdvertiser(mAdvertiserId);
-                        mAdvertiserId = -1;
+                        mAdvertiserId = -2;
                     } catch (RemoteException e) {
                         Log.e(TAG, "remote exception when unregistering", e);
                     }
@@ -312,7 +306,7 @@
             synchronized (this) {
                 if (status == BluetoothGatt.GATT_SUCCESS) {
                     try {
-                        if (mAdvertiserId == -1) {
+                        if (mAdvertiserId == -2) {
                             // Registration succeeds after timeout, unregister advertiser.
                             mBluetoothGatt.unregisterAdvertiser(advertiserId);
                         } else {
@@ -326,7 +320,7 @@
                     }
                 }
                 // Registration failed.
-                mAdvertiserId = -1;
+                mAdvertiserId = -2;
                 notifyAll();
             }
         }
@@ -348,7 +342,7 @@
                     // unregister advertiser for stop.
                     try {
                         mBluetoothGatt.unregisterAdvertiser(mAdvertiserId);
-                        mAdvertiserId = -1;
+                        mAdvertiserId = -2;
                         mIsAdvertising = false;
                         mLeAdvertisers.remove(mAdvertiseCallback);
                     } catch (RemoteException e) {
diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java
index 1e6424e..49b5853 100644
--- a/core/java/android/content/ContentProvider.java
+++ b/core/java/android/content/ContentProvider.java
@@ -42,6 +42,7 @@
 import android.os.OperationCanceledException;
 import android.os.ParcelFileDescriptor;
 import android.os.Process;
+import android.os.RemoteException;
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.Log;
@@ -463,6 +464,23 @@
             }
         }
 
+        @Override
+        public boolean refresh(String callingPkg, Uri uri, Bundle args,
+                ICancellationSignal cancellationSignal) throws RemoteException {
+            validateIncomingUri(uri);
+            uri = getUriWithoutUserId(uri);
+            if (enforceReadPermission(callingPkg, uri, null) != AppOpsManager.MODE_ALLOWED) {
+                return false;
+            }
+            final String original = setCallingPackage(callingPkg);
+            try {
+                return ContentProvider.this.refresh(uri, args,
+                        CancellationSignal.fromTransport(cancellationSignal));
+            } finally {
+                setCallingPackage(original);
+            }
+        }
+
         private void enforceFilePermission(String callingPkg, Uri uri, String mode,
                 IBinder callerToken) throws FileNotFoundException, SecurityException {
             if (mode != null && mode.indexOf('w') != -1) {
@@ -1093,6 +1111,34 @@
     }
 
     /**
+     * Implement this to support refresh of content identified by {@code uri}. By default, this
+     * method returns false; providers who wish to implement this should return true to signal the
+     * client that the provider has tried refreshing with its own implementation.
+     * <p>
+     * This allows clients to request an explicit refresh of content identified by {@code uri}.
+     * <p>
+     * Client code should only invoke this method when there is a strong indication (such as a user
+     * initiated pull to refresh gesture) that the content is stale.
+     * <p>
+     * Remember to send {@link ContentResolver#notifyChange(Uri, android.database.ContentObserver)}
+     * notifications when content changes.
+     *
+     * @param uri The Uri identifying the data to refresh.
+     * @param args Additional options from the client. The definitions of these are specific to the
+     *            content provider being called.
+     * @param cancellationSignal A signal to cancel the operation in progress, or {@code null} if
+     *            none. For example, if you called refresh on a particular uri, you should call
+     *            {@link CancellationSignal#throwIfCanceled()} to check whether the client has
+     *            canceled the refresh request.
+     * @return true if the provider actually tried refreshing.
+     * @hide
+     */
+    public boolean refresh(Uri uri, @Nullable Bundle args,
+            @Nullable CancellationSignal cancellationSignal) {
+        return false;
+    }
+
+    /**
      * @hide
      * Implementation when a caller has performed an insert on the content
      * provider, but that call has been rejected for the operation given
diff --git a/core/java/android/content/ContentProviderClient.java b/core/java/android/content/ContentProviderClient.java
index 9221fbb..857610d 100644
--- a/core/java/android/content/ContentProviderClient.java
+++ b/core/java/android/content/ContentProviderClient.java
@@ -234,6 +234,30 @@
         }
     }
 
+    /** @hide */
+    public boolean refresh(Uri url, @Nullable Bundle args,
+            @Nullable CancellationSignal cancellationSignal) throws RemoteException {
+        Preconditions.checkNotNull(url, "url");
+
+        beforeRemote();
+        try {
+            ICancellationSignal remoteCancellationSignal = null;
+            if (cancellationSignal != null) {
+                cancellationSignal.throwIfCanceled();
+                remoteCancellationSignal = mContentProvider.createCancellationSignal();
+                cancellationSignal.setRemote(remoteCancellationSignal);
+            }
+            return mContentProvider.refresh(mPackageName, url, args, remoteCancellationSignal);
+        } catch (DeadObjectException e) {
+            if (!mStable) {
+                mContentResolver.unstableProviderDied(mContentProvider);
+            }
+            throw e;
+        } finally {
+            afterRemote();
+        }
+    }
+
     /** See {@link ContentProvider#insert ContentProvider.insert} */
     public @Nullable Uri insert(@NonNull Uri url, @Nullable ContentValues initialValues)
             throws RemoteException {
diff --git a/core/java/android/content/ContentProviderNative.java b/core/java/android/content/ContentProviderNative.java
index 439e1ff..eadc013 100644
--- a/core/java/android/content/ContentProviderNative.java
+++ b/core/java/android/content/ContentProviderNative.java
@@ -355,6 +355,20 @@
                     Uri.writeToParcel(reply, out);
                     return true;
                 }
+
+                case REFRESH_TRANSACTION: {
+                    data.enforceInterface(IContentProvider.descriptor);
+                    String callingPkg = data.readString();
+                    Uri url = Uri.CREATOR.createFromParcel(data);
+                    Bundle args = data.readBundle();
+                    ICancellationSignal signal = ICancellationSignal.Stub.asInterface(
+                            data.readStrongBinder());
+
+                    boolean out = refresh(callingPkg, url, args, signal);
+                    reply.writeNoException();
+                    reply.writeInt(out ? 0 : -1);
+                    return true;
+                }
             }
         } catch (Exception e) {
             DatabaseUtils.writeExceptionToParcel(reply, e);
@@ -761,5 +775,28 @@
         }
     }
 
+    public boolean refresh(String callingPkg, Uri url, Bundle args, ICancellationSignal signal)
+            throws RemoteException {
+        Parcel data = Parcel.obtain();
+        Parcel reply = Parcel.obtain();
+        try {
+            data.writeInterfaceToken(IContentProvider.descriptor);
+
+            data.writeString(callingPkg);
+            url.writeToParcel(data, 0);
+            data.writeBundle(args);
+            data.writeStrongBinder(signal != null ? signal.asBinder() : null);
+
+            mRemote.transact(IContentProvider.REFRESH_TRANSACTION, data, reply, 0);
+
+            DatabaseUtils.readExceptionFromParcel(reply);
+            int success = reply.readInt();
+            return (success == 0);
+        } finally {
+            data.recycle();
+            reply.recycle();
+        }
+    }
+
     private IBinder mRemote;
 }
diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java
index daa1b93..705c091 100644
--- a/core/java/android/content/ContentResolver.java
+++ b/core/java/android/content/ContentResolver.java
@@ -194,6 +194,15 @@
     public static final String EXTRA_SIZE = "android.content.extra.SIZE";
 
     /**
+     * An extra boolean describing whether a particular provider supports refresh
+     * or not. If a provider supports refresh, it should include this key in its
+     * returned Cursor as part of its query call.
+     *
+     * @hide
+     */
+    public static final String EXTRA_REFRESH_SUPPORTED = "android.content.extra.REFRESH_SUPPORTED";
+
+    /**
      * This is the Android platform's base MIME type for a content: URI
      * containing a Cursor of a single item.  Applications should use this
      * as the base type along with their own sub-type of their content: URIs
@@ -664,6 +673,54 @@
     }
 
     /**
+     * Implement this to support refresh of content identified by {@code uri}. By default, this
+     * method returns false; providers who wish to implement this should return true to signal the
+     * client that the provider has tried refreshing with its own implementation.
+     * <p>
+     * This allows clients to request an explicit refresh of content identified by {@code uri}.
+     * <p>
+     * Client code should only invoke this method when there is a strong indication (such as a user
+     * initiated pull to refresh gesture) that the content is stale.
+     * <p>
+     * Remember to send {@link ContentResolver#notifyChange(Uri, android.database.ContentObserver)}
+     * notifications when content changes.
+     *
+     * @param uri The Uri identifying the data to refresh.
+     * @param args Additional options from the client. The definitions of these are specific to the
+     *            content provider being called.
+     * @param cancellationSignal A signal to cancel the operation in progress, or {@code null} if
+     *            none. For example, if you called refresh on a particular uri, you should call
+     *            {@link CancellationSignal#throwIfCanceled()} to check whether the client has
+     *            canceled the refresh request.
+     * @return true if the provider actually tried refreshing.
+     * @hide
+     */
+    public final boolean refresh(@NonNull Uri url, @Nullable Bundle args,
+            @Nullable CancellationSignal cancellationSignal) {
+        Preconditions.checkNotNull(url, "url");
+        IContentProvider provider = acquireProvider(url);
+        if (provider == null) {
+            return false;
+        }
+
+        try {
+            ICancellationSignal remoteCancellationSignal = null;
+            if (cancellationSignal != null) {
+                cancellationSignal.throwIfCanceled();
+                remoteCancellationSignal = provider.createCancellationSignal();
+                cancellationSignal.setRemote(remoteCancellationSignal);
+            }
+            return provider.refresh(mPackageName, url, args, remoteCancellationSignal);
+        } catch (RemoteException e) {
+            // Arbitrary and not worth documenting, as Activity
+            // Manager will kill this process shortly anyway.
+            return false;
+        } finally {
+            releaseProvider(provider);
+        }
+    }
+
+    /**
      * Open a stream on to the content associated with a content URI.  If there
      * is no data associated with the URI, FileNotFoundException is thrown.
      *
diff --git a/core/java/android/content/IContentProvider.java b/core/java/android/content/IContentProvider.java
index 4afe38b..ee8a22f 100644
--- a/core/java/android/content/IContentProvider.java
+++ b/core/java/android/content/IContentProvider.java
@@ -65,6 +65,9 @@
     public Uri canonicalize(String callingPkg, Uri uri) throws RemoteException;
     public Uri uncanonicalize(String callingPkg, Uri uri) throws RemoteException;
 
+    public boolean refresh(String callingPkg, Uri url, @Nullable Bundle args,
+            ICancellationSignal cancellationSignal) throws RemoteException;
+
     // Data interchange.
     public String[] getStreamTypes(Uri url, String mimeTypeFilter) throws RemoteException;
     public AssetFileDescriptor openTypedAssetFile(String callingPkg, Uri url, String mimeType,
@@ -88,4 +91,5 @@
     static final int CREATE_CANCELATION_SIGNAL_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 23;
     static final int CANONICALIZE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 24;
     static final int UNCANONICALIZE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 25;
+    static final int REFRESH_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 26;
 }
diff --git a/core/java/android/content/pm/ShortcutManager.java b/core/java/android/content/pm/ShortcutManager.java
index a93870e..f7c4d59 100644
--- a/core/java/android/content/pm/ShortcutManager.java
+++ b/core/java/android/content/pm/ShortcutManager.java
@@ -193,7 +193,11 @@
  * The following list includes descriptions for the different attributes within a static shortcut:
  * <dl>
  *   <dt>{@code android:shortcutId}</dt>
- *   <dd>Mandatory shortcut ID</dd>
+ *   <dd>Mandatory shortcut ID.
+ *   <p>
+ *   This must be a string literal.
+ *   A resource string, such as <code>@string/foo</code>, cannot be used.
+ *   </dd>
  *
  *   <dt>{@code android:enabled}</dt>
  *   <dd>Default is {@code true}.  Can be set to {@code false} in order
@@ -206,15 +210,24 @@
  *
  *   <dt>{@code android:shortcutShortLabel}</dt>
  *   <dd>Mandatory shortcut short label.
- *   See {@link ShortcutInfo.Builder#setShortLabel(CharSequence)}.</dd>
+ *   See {@link ShortcutInfo.Builder#setShortLabel(CharSequence)}.
+ *   <p>
+ *   This must be a resource string, such as <code>@string/shortcut_label</code>.
+ *   </dd>
  *
  *   <dt>{@code android:shortcutLongLabel}</dt>
  *   <dd>Shortcut long label.
- *   See {@link ShortcutInfo.Builder#setLongLabel(CharSequence)}.</dd>
+ *   See {@link ShortcutInfo.Builder#setLongLabel(CharSequence)}.
+ *   <p>
+ *   This must be a resource string, such as <code>@string/shortcut_long_label</code>.
+ *   </dd>
  *
  *   <dt>{@code android:shortcutDisabledMessage}</dt>
  *   <dd>When {@code android:enabled} is set to
- *   {@code false}, this attribute is used to display a custom disabled message.</dd>
+ *   {@code false}, this attribute is used to display a custom disabled message.
+ *   <p>
+ *   This must be a resource string, such as <code>@string/shortcut_disabled_message</code>.
+ *   </dd>
  *
  *   <dt>{@code intent}</dt>
  *   <dd>Intent to launch when the user selects the shortcut.
diff --git a/core/java/android/hardware/location/IFusedLocationHardwareSink.aidl b/core/java/android/hardware/location/IFusedLocationHardwareSink.aidl
index c99cb0c..a7dd035 100644
--- a/core/java/android/hardware/location/IFusedLocationHardwareSink.aidl
+++ b/core/java/android/hardware/location/IFusedLocationHardwareSink.aidl
@@ -24,7 +24,7 @@
  *
  * @hide
  */
-interface IFusedLocationHardwareSink {
+oneway interface IFusedLocationHardwareSink {
     /**
      * Event generated when a batch of location information is available.
      *
@@ -50,4 +50,4 @@
      * changes (location is successful/unsuccessful).
      */
     void onStatusChanged(int status) = 3;
-}
\ No newline at end of file
+}
diff --git a/core/java/android/os/BatteryManager.java b/core/java/android/os/BatteryManager.java
index 252385f..263750a 100644
--- a/core/java/android/os/BatteryManager.java
+++ b/core/java/android/os/BatteryManager.java
@@ -16,6 +16,7 @@
 
 package android.os;
 
+import android.hardware.health.V1_0.Constants;
 import com.android.internal.app.IBatteryStats;
 
 /**
@@ -118,20 +119,20 @@
      public static final String EXTRA_CHARGE_COUNTER = "charge_counter";
 
     // values for "status" field in the ACTION_BATTERY_CHANGED Intent
-    public static final int BATTERY_STATUS_UNKNOWN = 1;
-    public static final int BATTERY_STATUS_CHARGING = 2;
-    public static final int BATTERY_STATUS_DISCHARGING = 3;
-    public static final int BATTERY_STATUS_NOT_CHARGING = 4;
-    public static final int BATTERY_STATUS_FULL = 5;
+    public static final int BATTERY_STATUS_UNKNOWN = Constants.BATTERY_STATUS_UNKNOWN;
+    public static final int BATTERY_STATUS_CHARGING = Constants.BATTERY_STATUS_CHARGING;
+    public static final int BATTERY_STATUS_DISCHARGING = Constants.BATTERY_STATUS_DISCHARGING;
+    public static final int BATTERY_STATUS_NOT_CHARGING = Constants.BATTERY_STATUS_NOT_CHARGING;
+    public static final int BATTERY_STATUS_FULL = Constants.BATTERY_STATUS_FULL;
 
     // values for "health" field in the ACTION_BATTERY_CHANGED Intent
-    public static final int BATTERY_HEALTH_UNKNOWN = 1;
-    public static final int BATTERY_HEALTH_GOOD = 2;
-    public static final int BATTERY_HEALTH_OVERHEAT = 3;
-    public static final int BATTERY_HEALTH_DEAD = 4;
-    public static final int BATTERY_HEALTH_OVER_VOLTAGE = 5;
-    public static final int BATTERY_HEALTH_UNSPECIFIED_FAILURE = 6;
-    public static final int BATTERY_HEALTH_COLD = 7;
+    public static final int BATTERY_HEALTH_UNKNOWN = Constants.BATTERY_HEALTH_UNKNOWN;
+    public static final int BATTERY_HEALTH_GOOD = Constants.BATTERY_HEALTH_GOOD;
+    public static final int BATTERY_HEALTH_OVERHEAT = Constants.BATTERY_HEALTH_OVERHEAT;
+    public static final int BATTERY_HEALTH_DEAD = Constants.BATTERY_HEALTH_DEAD;
+    public static final int BATTERY_HEALTH_OVER_VOLTAGE = Constants.BATTERY_HEALTH_OVER_VOLTAGE;
+    public static final int BATTERY_HEALTH_UNSPECIFIED_FAILURE = Constants.BATTERY_HEALTH_UNSPECIFIED_FAILURE;
+    public static final int BATTERY_HEALTH_COLD = Constants.BATTERY_HEALTH_COLD;
 
     // values of the "plugged" field in the ACTION_BATTERY_CHANGED intent.
     // These must be powers of 2.
diff --git a/core/java/android/text/Editable.java b/core/java/android/text/Editable.java
index b3f2c2a..c0948a6 100644
--- a/core/java/android/text/Editable.java
+++ b/core/java/android/text/Editable.java
@@ -121,8 +121,10 @@
     public InputFilter[] getFilters();
 
     /**
-     * Factory used by TextView to create new Editables.  You can subclass
-     * it to provide something other than SpannableStringBuilder.
+     * Factory used by TextView to create new {@link Editable Editables}. You can subclass
+     * it to provide something other than {@link SpannableStringBuilder}.
+     *
+     * @see android.widget.TextView#setEditableFactory(Factory)
      */
     public static class Factory {
         private static Editable.Factory sInstance = new Editable.Factory();
diff --git a/core/java/android/text/Spannable.java b/core/java/android/text/Spannable.java
index ae5d356..39b78eb 100644
--- a/core/java/android/text/Spannable.java
+++ b/core/java/android/text/Spannable.java
@@ -46,15 +46,17 @@
     public void removeSpan(Object what);
 
     /**
-     * Factory used by TextView to create new Spannables.  You can subclass
-     * it to provide something other than SpannableString.
+     * Factory used by TextView to create new {@link Spannable Spannables}. You can subclass
+     * it to provide something other than {@link SpannableString}.
+     *
+     * @see android.widget.TextView#setSpannableFactory(Factory)
      */
     public static class Factory {
         private static Spannable.Factory sInstance = new Spannable.Factory();
 
         /**
          * Returns the standard Spannable Factory.
-         */ 
+         */
         public static Spannable.Factory getInstance() {
             return sInstance;
         }
diff --git a/core/java/android/view/IPinnedStackController.aidl b/core/java/android/view/IPinnedStackController.aidl
index a81eef8..d59be02 100644
--- a/core/java/android/view/IPinnedStackController.aidl
+++ b/core/java/android/view/IPinnedStackController.aidl
@@ -32,6 +32,11 @@
     oneway void setInInteractiveMode(boolean inInteractiveMode);
 
     /**
+     * Notifies the controller that the PIP is currently minimized.
+     */
+    oneway void setIsMinimized(boolean isMinimized);
+
+    /**
      * Notifies the controller that the desired snap mode is to the closest edge.
      */
     oneway void setSnapToEdge(boolean snapToEdge);
diff --git a/core/java/android/view/inputmethod/EditorInfo.java b/core/java/android/view/inputmethod/EditorInfo.java
index 8038089..2f12e9b 100644
--- a/core/java/android/view/inputmethod/EditorInfo.java
+++ b/core/java/android/view/inputmethod/EditorInfo.java
@@ -34,6 +34,54 @@
  */
 public class EditorInfo implements InputType, Parcelable {
     /**
+     * Masks for {@link inputType}
+     *
+     * <pre>
+     * |-------|-------|-------|-------|
+     *                              1111 TYPE_MASK_CLASS
+     *                      11111111     TYPE_MASK_VARIATION
+     *          111111111111             TYPE_MASK_FLAGS
+     * |-------|-------|-------|-------|
+     *                                   TYPE_NULL
+     * |-------|-------|-------|-------|
+     *                                 1 TYPE_CLASS_TEXT
+     *                             1     TYPE_TEXT_VARIATION_URI
+     *                            1      TYPE_TEXT_VARIATION_EMAIL_ADDRESS
+     *                            11     TYPE_TEXT_VARIATION_EMAIL_SUBJECT
+     *                           1       TYPE_TEXT_VARIATION_SHORT_MESSAGE
+     *                           1 1     TYPE_TEXT_VARIATION_LONG_MESSAGE
+     *                           11      TYPE_TEXT_VARIATION_PERSON_NAME
+     *                           111     TYPE_TEXT_VARIATION_POSTAL_ADDRESS
+     *                          1        TYPE_TEXT_VARIATION_PASSWORD
+     *                          1  1     TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
+     *                          1 1      TYPE_TEXT_VARIATION_WEB_EDIT_TEXT
+     *                          1 11     TYPE_TEXT_VARIATION_FILTER
+     *                          11       TYPE_TEXT_VARIATION_PHONETIC
+     *                          11 1     TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS
+     *                          111      TYPE_TEXT_VARIATION_WEB_PASSWORD
+     *                     1             TYPE_TEXT_FLAG_CAP_CHARACTERS
+     *                    1              TYPE_TEXT_FLAG_CAP_WORDS
+     *                   1               TYPE_TEXT_FLAG_CAP_SENTENCES
+     *                  1                TYPE_TEXT_FLAG_AUTO_CORRECT
+     *                 1                 TYPE_TEXT_FLAG_AUTO_COMPLETE
+     *                1                  TYPE_TEXT_FLAG_MULTI_LINE
+     *               1                   TYPE_TEXT_FLAG_IME_MULTI_LINE
+     *              1                    TYPE_TEXT_FLAG_NO_SUGGESTIONS
+     * |-------|-------|-------|-------|
+     *                                1  TYPE_CLASS_NUMBER
+     *                             1     TYPE_NUMBER_VARIATION_PASSWORD
+     *                     1             TYPE_NUMBER_FLAG_SIGNED
+     *                    1              TYPE_NUMBER_FLAG_DECIMAL
+     * |-------|-------|-------|-------|
+     *                                11 TYPE_CLASS_PHONE
+     * |-------|-------|-------|-------|
+     *                               1   TYPE_CLASS_DATETIME
+     *                             1     TYPE_DATETIME_VARIATION_DATE
+     *                            1      TYPE_DATETIME_VARIATION_TIME
+     * |-------|-------|-------|-------|</pre>
+     */
+
+    /**
      * The content type of the text box, whose bits are defined by
      * {@link InputType}.
      *
@@ -107,6 +155,26 @@
     public static final int IME_ACTION_PREVIOUS = 0x00000007;
 
     /**
+     * Flag of {@link #imeOptions}: used to request that the IME does not update any personalized
+     * data such as typing history and personalized language model based on what the user typed on
+     * this text editing object.  Typical use cases are:
+     * <ul>
+     *     <li>When the application is in a special mode, where user's activities are expected to be
+     *     not recorded in the application's history.  Some web browsers and chat applications may
+     *     have this kind of modes.</li>
+     *     <li>When storing typing history does not make much sense.  Specifying this flag in typing
+     *     games may help to avoid typing history from being filled up with words that the user is
+     *     less likely to type in their daily life.  Another example is that when the application
+     *     already knows that the expected input is not a valid word (e.g. a promotion code that is
+     *     not a valid word in any natural language).</li>
+     * </ul>
+     *
+     * <p>Applications need to be aware that the flag is not a guarantee, and some IMEs may not
+     * respect it.</p>
+     */
+    public static final int IME_FLAG_NO_PERSONALIZED_LEARNING = 0x1000000;
+
+    /**
      * Flag of {@link #imeOptions}: used to request that the IME never go
      * into fullscreen mode.
      * By default, IMEs may go into full screen mode when they think
@@ -208,6 +276,32 @@
     public static final int IME_NULL = 0x00000000;
 
     /**
+     * Masks for {@link imeOptions}
+     *
+     * <pre>
+     * |-------|-------|-------|-------|
+     *                              1111 IME_MASK_ACTION
+     * |-------|-------|-------|-------|
+     *                                   IME_ACTION_UNSPECIFIED
+     *                                 1 IME_ACTION_NONE
+     *                                1  IME_ACTION_GO
+     *                                11 IME_ACTION_SEARCH
+     *                               1   IME_ACTION_SEND
+     *                               1 1 IME_ACTION_NEXT
+     *                               11  IME_ACTION_DONE
+     *                               111 IME_ACTION_PREVIOUS
+     *         1                         IME_FLAG_NO_PERSONALIZED_LEARNING
+     *        1                          IME_FLAG_NO_FULLSCREEN
+     *       1                           IME_FLAG_NAVIGATE_PREVIOUS
+     *      1                            IME_FLAG_NAVIGATE_NEXT
+     *     1                             IME_FLAG_NO_EXTRACT_UI
+     *    1                              IME_FLAG_NO_ACCESSORY_ACTION
+     *   1                               IME_FLAG_NO_ENTER_ACTION
+     *  1                                IME_FLAG_FORCE_ASCII
+     * |-------|-------|-------|-------|</pre>
+     */
+
+    /**
      * Extended type information for the editor, to help the IME better
      * integrate with it.
      */
diff --git a/core/java/android/view/inputmethod/InputContentInfo.java b/core/java/android/view/inputmethod/InputContentInfo.java
index b39705e..7104a28 100644
--- a/core/java/android/view/inputmethod/InputContentInfo.java
+++ b/core/java/android/view/inputmethod/InputContentInfo.java
@@ -18,11 +18,14 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.UserIdInt;
 import android.content.ClipDescription;
+import android.content.ContentProvider;
 import android.net.Uri;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.RemoteException;
+import android.os.UserHandle;
 
 import com.android.internal.inputmethod.IInputContentUriToken;
 
@@ -33,8 +36,24 @@
  */
 public final class InputContentInfo implements Parcelable {
 
+    /**
+     * The content URI that may or may not have a user ID embedded by
+     * {@link ContentProvider#maybeAddUserId(Uri, int)}.  This always preserves the exact value
+     * specified to a constructor.  In other words, if it had user ID embedded when it was passed
+     * to the constructor, it still has the same user ID no matter if it is valid or not.
+     */
     @NonNull
     private final Uri mContentUri;
+    /**
+     * The user ID to which {@link #mContentUri} belongs to.  If {@link #mContentUri} already
+     * embedded the user ID when it was specified then this fields has the same user ID.  Otherwise
+     * the user ID is determined based on the process ID when the constructor is called.
+     *
+     * <p>CAUTION: If you received {@link InputContentInfo} from a different process, there is no
+     * guarantee that this value is correct and valid.  Never use this for any security purpose</p>
+     */
+    @UserIdInt
+    private final int mContentUriOwnerUserId;
     @NonNull
     private final ClipDescription mDescription;
     @Nullable
@@ -73,6 +92,8 @@
             @Nullable Uri linkUri) {
         validateInternal(contentUri, description, linkUri, true /* throwException */);
         mContentUri = contentUri;
+        mContentUriOwnerUserId =
+                ContentProvider.getUserIdFromUri(mContentUri, UserHandle.myUserId());
         mDescription = description;
         mLinkUri = linkUri;
     }
@@ -139,7 +160,14 @@
      * @return Content URI with which the content can be obtained.
      */
     @NonNull
-    public Uri getContentUri() { return mContentUri; }
+    public Uri getContentUri() {
+        // Fix up the content URI when and only when the caller's user ID does not match the owner's
+        // user ID.
+        if (mContentUriOwnerUserId != UserHandle.myUserId()) {
+            return ContentProvider.maybeAddUserId(mContentUri, mContentUriOwnerUserId);
+        }
+        return mContentUri;
+    }
 
     /**
      * @return {@link ClipDescription} object that contains the metadata of {@code #getContentUri()}
@@ -203,6 +231,7 @@
     @Override
     public void writeToParcel(Parcel dest, int flags) {
         Uri.writeToParcel(dest, mContentUri);
+        dest.writeInt(mContentUriOwnerUserId);
         mDescription.writeToParcel(dest, flags);
         Uri.writeToParcel(dest, mLinkUri);
         if (mUriToken != null) {
@@ -215,6 +244,7 @@
 
     private InputContentInfo(@NonNull Parcel source) {
         mContentUri = Uri.CREATOR.createFromParcel(source);
+        mContentUriOwnerUserId = source.readInt();
         mDescription = ClipDescription.CREATOR.createFromParcel(source);
         mLinkUri = Uri.CREATOR.createFromParcel(source);
         if (source.readInt() == 1) {
diff --git a/core/java/android/widget/ArrayAdapter.java b/core/java/android/widget/ArrayAdapter.java
index 9d228cf..bbc50da 100644
--- a/core/java/android/widget/ArrayAdapter.java
+++ b/core/java/android/widget/ArrayAdapter.java
@@ -312,10 +312,10 @@
     }
 
     /**
-     * Control whether methods that change the list ({@link #add},
-     * {@link #insert}, {@link #remove}, {@link #clear}) automatically call
-     * {@link #notifyDataSetChanged}.  If set to false, caller must
-     * manually call notifyDataSetChanged() to have the changes
+     * Control whether methods that change the list ({@link #add}, {@link #addAll(Collection)},
+     * {@link #addAll(Object[])}, {@link #insert}, {@link #remove}, {@link #clear},
+     * {@link #sort(Comparator)}) automatically call {@link #notifyDataSetChanged}.  If set to
+     * false, caller must manually call notifyDataSetChanged() to have the changes
      * reflected in the attached view.
      *
      * The default is true, and calling notifyDataSetChanged()
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 73af755..5426a37 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -1683,12 +1683,13 @@
     }
 
     /**
-     * Return the text the TextView is displaying. If setText() was called with
-     * an argument of BufferType.SPANNABLE or BufferType.EDITABLE, you can cast
+     * Return the text that TextView is displaying. If {@link #setText(CharSequence)} was called
+     * with an argument of {@link android.widget.TextView.BufferType#SPANNABLE BufferType.SPANNABLE}
+     * or {@link android.widget.TextView.BufferType#EDITABLE BufferType.EDITABLE}, you can cast
      * the return value from this method to Spannable or Editable, respectively.
-     *
-     * Note: The content of the return value should not be modified. If you want
-     * a modifiable one, you should make your own copy first.
+     * <p/>
+     * The content of the return value should not be modified. If you want a modifiable one, you
+     * should make your own copy first.
      *
      * @attr ref android.R.styleable#TextView_text
      */
@@ -1705,8 +1706,8 @@
     }
 
     /**
-     * Return the text the TextView is displaying as an Editable object.  If
-     * the text is not editable, null is returned.
+     * Return the text that TextView is displaying as an Editable object. If the text is not
+     * editable, null is returned.
      *
      * @see #getText
      */
@@ -4148,18 +4149,26 @@
     }
 
     /**
-     * Convenience method: Append the specified text to the TextView's
-     * display buffer, upgrading it to BufferType.EDITABLE if it was
-     * not already editable.
+     * Convenience method to append the specified text to the TextView's
+     * display buffer, upgrading it to {@link android.widget.TextView.BufferType#EDITABLE}
+     * if it was not already editable.
+     *
+     * @param text text to be appended to the already displayed text
      */
     public final void append(CharSequence text) {
         append(text, 0, text.length());
     }
 
     /**
-     * Convenience method: Append the specified text slice to the TextView's
-     * display buffer, upgrading it to BufferType.EDITABLE if it was
-     * not already editable.
+     * Convenience method to append the specified text slice to the TextView's
+     * display buffer, upgrading it to {@link android.widget.TextView.BufferType#EDITABLE}
+     * if it was not already editable.
+     *
+     * @param text text to be appended to the already displayed text
+     * @param start the index of the first character in the {@code text}
+     * @param end the index of the character following the last character in the {@code text}
+     *
+     * @see Appendable#append(CharSequence, int, int)
      */
     public void append(CharSequence text, int start, int end) {
         if (!(mText instanceof Editable)) {
@@ -4403,7 +4412,12 @@
     ///////////////////////////////////////////////////////////////////////////
 
     /**
-     * Sets the Factory used to create new Editables.
+     * Sets the Factory used to create new {@link Editable Editables}.
+     *
+     * @param factory {@link android.text.Editable.Factory Editable.Factory} to be used
+     *
+     * @see android.text.Editable.Factory
+     * @see android.widget.TextView.BufferType#EDITABLE
      */
     public final void setEditableFactory(Editable.Factory factory) {
         mEditableFactory = factory;
@@ -4411,7 +4425,12 @@
     }
 
     /**
-     * Sets the Factory used to create new Spannables.
+     * Sets the Factory used to create new {@link Spannable Spannables}.
+     *
+     * @param factory {@link android.text.Spannable.Factory Spannable.Factory} to be used
+     *
+     * @see android.text.Spannable.Factory
+     * @see android.widget.TextView.BufferType#SPANNABLE
      */
     public final void setSpannableFactory(Spannable.Factory factory) {
         mSpannableFactory = factory;
@@ -4419,13 +4438,20 @@
     }
 
     /**
-     * Sets the string value of the TextView. TextView <em>does not</em> accept
+     * Sets the text to be displayed. TextView <em>does not</em> accept
      * HTML-like formatting, which you can do with text strings in XML resource files.
      * To style your strings, attach android.text.style.* objects to a
-     * {@link android.text.SpannableString SpannableString}, or see the
+     * {@link android.text.SpannableString}, or see the
      * <a href="{@docRoot}guide/topics/resources/available-resources.html#stringresources">
      * Available Resource Types</a> documentation for an example of setting
      * formatted text in the XML resource file.
+     * <p/>
+     * When required, TextView will use {@link android.text.Spannable.Factory} to create final or
+     * intermediate {@link Spannable Spannables}. Likewise it will use
+     * {@link android.text.Editable.Factory} to create final or intermediate
+     * {@link Editable Editables}.
+     *
+     * @param text text to be displayed
      *
      * @attr ref android.R.styleable#TextView_text
      */
@@ -4435,10 +4461,16 @@
     }
 
     /**
-     * Like {@link #setText(CharSequence)},
-     * except that the cursor position (if any) is retained in the new text.
+     * Sets the text to be displayed but retains the cursor position. Same as
+     * {@link #setText(CharSequence)} except that the cursor position (if any) is retained in the
+     * new text.
+     * <p/>
+     * When required, TextView will use {@link android.text.Spannable.Factory} to create final or
+     * intermediate {@link Spannable Spannables}. Likewise it will use
+     * {@link android.text.Editable.Factory} to create final or intermediate
+     * {@link Editable Editables}.
      *
-     * @param text The new text to place in the text view.
+     * @param text text to be displayed
      *
      * @see #setText(CharSequence)
      */
@@ -4448,9 +4480,21 @@
     }
 
     /**
-     * Sets the text that this TextView is to display (see
-     * {@link #setText(CharSequence)}) and also sets whether it is stored
-     * in a styleable/spannable buffer and whether it is editable.
+     * Sets the text to be displayed and the {@link android.widget.TextView.BufferType}.
+     * <p/>
+     * When required, TextView will use {@link android.text.Spannable.Factory} to create final or
+     * intermediate {@link Spannable Spannables}. Likewise it will use
+     * {@link android.text.Editable.Factory} to create final or intermediate
+     * {@link Editable Editables}.
+     *
+     * @param text text to be displayed
+     * @param type a {@link android.widget.TextView.BufferType} which defines whether the text is
+     *              stored as a static text, styleable/spannable text, or editable text
+     *
+     * @see #setText(CharSequence)
+     * @see android.widget.TextView.BufferType
+     * @see #setSpannableFactory(Spannable.Factory)
+     * @see #setEditableFactory(Editable.Factory)
      *
      * @attr ref android.R.styleable#TextView_text
      * @attr ref android.R.styleable#TextView_bufferType
@@ -4617,10 +4661,14 @@
 
     /**
      * Sets the TextView to display the specified slice of the specified
-     * char array.  You must promise that you will not change the contents
+     * char array. You must promise that you will not change the contents
      * of the array except for right before another call to setText(),
      * since the TextView has no way to know that the text
      * has changed and that it needs to invalidate and re-layout.
+     *
+     * @param text char array to be displayed
+     * @param start start index in the char array
+     * @param len length of char count after {@code start}
      */
     public final void setText(char[] text, int start, int len) {
         int oldlen = 0;
@@ -4651,8 +4699,19 @@
     }
 
     /**
-     * Like {@link #setText(CharSequence, android.widget.TextView.BufferType)},
-     * except that the cursor position (if any) is retained in the new text.
+     * Sets the text to be displayed and the {@link android.widget.TextView.BufferType} but retains
+     * the cursor position. Same as
+     * {@link #setText(CharSequence, android.widget.TextView.BufferType)} except that the cursor
+     * position (if any) is retained in the new text.
+     * <p/>
+     * When required, TextView will use {@link android.text.Spannable.Factory} to create final or
+     * intermediate {@link Spannable Spannables}. Likewise it will use
+     * {@link android.text.Editable.Factory} to create final or intermediate
+     * {@link Editable Editables}.
+     *
+     * @param text text to be displayed
+     * @param type a {@link android.widget.TextView.BufferType} which defines whether the text is
+     *              stored as a static text, styleable/spannable text, or editable text
      *
      * @see #setText(CharSequence, android.widget.TextView.BufferType)
      */
@@ -4672,11 +4731,42 @@
         }
     }
 
+    /**
+     * Sets the text to be displayed using a string resource identifier.
+     *
+     * @param resid the resource identifier of the string resource to be displayed
+     *
+     * @see #setText(CharSequence)
+     *
+     * @attr ref android.R.styleable#TextView_text
+     */
     @android.view.RemotableViewMethod
     public final void setText(@StringRes int resid) {
         setText(getContext().getResources().getText(resid));
     }
 
+    /**
+     * Sets the text to be displayed using a string resource identifier and the
+     * {@link android.widget.TextView.BufferType}.
+     * <p/>
+     * When required, TextView will use {@link android.text.Spannable.Factory} to create final or
+     * intermediate {@link Spannable Spannables}. Likewise it will use
+     * {@link android.text.Editable.Factory} to create final or intermediate
+     * {@link Editable Editables}.
+     *
+     * @param resid the resource identifier of the string resource to be displayed
+     * @param type a {@link android.widget.TextView.BufferType} which defines whether the text is
+     *              stored as a static text, styleable/spannable text, or editable text
+     *
+     * @see #setText(int)
+     * @see #setText(CharSequence)
+     * @see android.widget.TextView.BufferType
+     * @see #setSpannableFactory(Spannable.Factory)
+     * @see #setEditableFactory(Editable.Factory)
+     *
+     * @attr ref android.R.styleable#TextView_text
+     * @attr ref android.R.styleable#TextView_bufferType
+     */
     public final void setText(@StringRes int resid, BufferType type) {
         setText(getContext().getResources().getText(resid), type);
     }
@@ -5110,7 +5200,7 @@
      * Set the extra input data of the text, which is the
      * {@link EditorInfo#extras TextBoxAttribute.extras}
      * Bundle that will be filled in when creating an input connection.  The
-     * given integer is the resource ID of an XML resource holding an
+     * given integer is the resource identifier of an XML resource holding an
      * {@link android.R.styleable#InputExtras &lt;input-extras&gt;} XML tree.
      *
      * @see #getInputExtras(boolean)
@@ -8832,8 +8922,12 @@
         }
     }
 
+    /**
+     * Type of the text buffer that defines the characteristics of the text such as static,
+     * styleable, or editable.
+     */
     public enum BufferType {
-        NORMAL, SPANNABLE, EDITABLE,
+        NORMAL, SPANNABLE, EDITABLE
     }
 
     /**
diff --git a/core/java/com/android/internal/policy/PipSnapAlgorithm.java b/core/java/com/android/internal/policy/PipSnapAlgorithm.java
index cbacf26..62d506f 100644
--- a/core/java/com/android/internal/policy/PipSnapAlgorithm.java
+++ b/core/java/com/android/internal/policy/PipSnapAlgorithm.java
@@ -41,8 +41,12 @@
     // Allows snapping to anywhere along the edge of the screen
     private static final int SNAP_MODE_EDGE = 2;
 
+    // The friction multiplier to control how slippery the PIP is when flung
     private static final float SCROLL_FRICTION_MULTIPLIER = 8f;
 
+    // The fraction of the stack width to show when minimized
+    private static final float MINIMIZED_VISIBLE_FRACTION = 0.25f;
+
     private final Context mContext;
 
     private final ArrayList<Integer> mSnapGravities = new ArrayList<>();
@@ -122,6 +126,18 @@
     }
 
     /**
+     * Applies the offset to the {@param stackBounds} to adjust it to a minimized state.
+     */
+    public void applyMinimizedOffset(Rect stackBounds, Rect movementBounds, Point displaySize) {
+        int visibleWidth = (int) (MINIMIZED_VISIBLE_FRACTION * stackBounds.width());
+        if (stackBounds.left <= movementBounds.centerX()) {
+            stackBounds.offsetTo(-stackBounds.width() + visibleWidth, stackBounds.top);
+        } else {
+            stackBounds.offsetTo(displaySize.x - visibleWidth, stackBounds.top);
+        }
+    }
+
+    /**
      * @return returns a fraction that describes where along the {@param movementBounds} the
      *         {@param stackBounds} are. If the {@param stackBounds} are not currently on the
      *         {@param movementBounds} exactly, then they will be snapped to the movement bounds.
@@ -208,15 +224,19 @@
         final int fromTop = Math.abs(stackBounds.top - movementBounds.top);
         final int fromRight = Math.abs(movementBounds.right - stackBounds.left);
         final int fromBottom = Math.abs(movementBounds.bottom - stackBounds.top);
+        final int boundedLeft = Math.max(movementBounds.left, Math.min(movementBounds.right,
+                stackBounds.left));
+        final int boundedTop = Math.max(movementBounds.top, Math.min(movementBounds.bottom,
+                stackBounds.top));
         boundsOut.set(stackBounds);
         if (fromLeft <= fromTop && fromLeft <= fromRight && fromLeft <= fromBottom) {
-            boundsOut.offsetTo(movementBounds.left, stackBounds.top);
+            boundsOut.offsetTo(movementBounds.left, boundedTop);
         } else if (fromTop <= fromLeft && fromTop <= fromRight && fromTop <= fromBottom) {
-            boundsOut.offsetTo(stackBounds.left, movementBounds.top);
+            boundsOut.offsetTo(boundedLeft, movementBounds.top);
         } else if (fromRight < fromLeft && fromRight < fromTop && fromRight < fromBottom) {
-            boundsOut.offsetTo(movementBounds.right, stackBounds.top);
+            boundsOut.offsetTo(movementBounds.right, boundedTop);
         } else {
-            boundsOut.offsetTo(stackBounds.left, movementBounds.bottom);
+            boundsOut.offsetTo(boundedLeft, movementBounds.bottom);
         }
     }
 
diff --git a/core/jni/android_os_HwParcel.cpp b/core/jni/android_os_HwParcel.cpp
index 886c0e6..f909580 100644
--- a/core/jni/android_os_HwParcel.cpp
+++ b/core/jni/android_os_HwParcel.cpp
@@ -267,9 +267,9 @@
 
     const jchar *interfaceName = env->GetStringCritical(interfaceNameObj, NULL);
     if (interfaceName) {
-        String16 nameCopy(
+        String8 nameCopy = String8(String16(
                 reinterpret_cast<const char16_t *>(interfaceName),
-                env->GetStringLength(interfaceNameObj));
+                env->GetStringLength(interfaceNameObj)));
 
         env->ReleaseStringCritical(interfaceNameObj, interfaceName);
         interfaceName = NULL;
@@ -277,7 +277,7 @@
         hardware::Parcel *parcel =
             JHwParcel::GetNativeContext(env, thiz)->getParcel();
 
-        status_t err = parcel->writeInterfaceToken(nameCopy);
+        status_t err = parcel->writeInterfaceToken(nameCopy.string());
         signalExceptionForError(env, err);
     }
 }
@@ -294,9 +294,9 @@
 
     const jchar *interfaceName = env->GetStringCritical(interfaceNameObj, NULL);
     if (interfaceName) {
-        String16 interfaceNameCopy(
+        String8 interfaceNameCopy = String8(String16(
                 reinterpret_cast<const char16_t *>(interfaceName),
-                env->GetStringLength(interfaceNameObj));
+                env->GetStringLength(interfaceNameObj)));
 
         env->ReleaseStringCritical(interfaceNameObj, interfaceName);
         interfaceName = NULL;
@@ -304,7 +304,7 @@
         hardware::Parcel *parcel =
             JHwParcel::GetNativeContext(env, thiz)->getParcel();
 
-        bool valid = parcel->enforceInterface(interfaceNameCopy);
+        bool valid = parcel->enforceInterface(interfaceNameCopy.string());
 
         if (!valid) {
             jniThrowException(
diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp
index fc85b4b..da059e3 100644
--- a/core/jni/com_android_internal_os_Zygote.cpp
+++ b/core/jni/com_android_internal_os_Zygote.cpp
@@ -319,12 +319,6 @@
         bool force_mount_namespace) {
     // See storage config details at http://source.android.com/tech/storage/
 
-    // Create a second private mount namespace for our process
-    if (unshare(CLONE_NEWNS) == -1) {
-        ALOGW("Failed to unshare(): %s", strerror(errno));
-        return false;
-    }
-
     String8 storageSource;
     if (mount_mode == MOUNT_EXTERNAL_DEFAULT) {
         storageSource = "/mnt/runtime/default";
@@ -332,10 +326,17 @@
         storageSource = "/mnt/runtime/read";
     } else if (mount_mode == MOUNT_EXTERNAL_WRITE) {
         storageSource = "/mnt/runtime/write";
-    } else {
+    } else if (!force_mount_namespace) {
         // Sane default of no storage visible
         return true;
     }
+
+    // Create a second private mount namespace for our process
+    if (unshare(CLONE_NEWNS) == -1) {
+        ALOGW("Failed to unshare(): %s", strerror(errno));
+        return false;
+    }
+
     if (TEMP_FAILURE_RETRY(mount(storageSource.string(), "/storage",
             NULL, MS_BIND | MS_REC | MS_SLAVE, NULL)) == -1) {
         ALOGW("Failed to mount %s to /storage: %s", storageSource.string(), strerror(errno));
diff --git a/core/jni/fd_utils-inl.h b/core/jni/fd_utils-inl.h
index 2babe44..a712062 100644
--- a/core/jni/fd_utils-inl.h
+++ b/core/jni/fd_utils-inl.h
@@ -280,6 +280,12 @@
       return true;
     }
 
+    // All regular files that are placed under this path are whitelisted automatically.
+    static const std::string kZygoteWhitelistPath = "/vendor/zygote_whitelist/";
+    if (StartsWith(path, kZygoteWhitelistPath) && path.find("/../") == std::string::npos) {
+      return true;
+    }
+
     return false;
   }
 
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 3dea051..0f7b5a5 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3130,6 +3130,12 @@
     <permission android:name="android.permission.MANAGE_AUTO_FILL"
         android:protectionLevel="signature" />
 
+    <!-- Allows an app to set the theme overlay in /vendor/overlay
+         being used.
+         @hide  <p>Not for use by third-party applications.</p> -->
+    <permission android:name="android.permission.MODIFY_THEME_OVERLAY"
+                android:protectionLevel="signature" />
+
     <application android:process="system"
                  android:persistent="true"
                  android:hasCode="false"
diff --git a/core/res/res/values/colors_material.xml b/core/res/res/values/colors_material.xml
index 92426c6..37feff8 100644
--- a/core/res/res/values/colors_material.xml
+++ b/core/res/res/values/colors_material.xml
@@ -36,8 +36,10 @@
     <color name="tertiary_material_settings">@color/material_blue_grey_700</color>
     <color name="quaternary_material_settings">@color/material_blue_grey_200</color>
 
+    <color name="accent_material_700">@color/material_deep_teal_700</color>
     <color name="accent_material_light">@color/material_deep_teal_500</color>
     <color name="accent_material_dark">@color/material_deep_teal_200</color>
+    <color name="accent_material_50">@color/material_deep_teal_50</color>
 
     <color name="button_material_dark">#ff5a595b</color>
     <color name="button_material_light">#ffd6d7d7</color>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 7005afe..996fd55 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2492,11 +2492,11 @@
 
     <!-- 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">10x10</string>
+    <string translatable="false" name="config_defaultPictureInPictureScreenEdgeInsets">8x8</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. -->
-    <string translatable="false" name="config_defaultPictureInPictureSize">216x135</string>
+    <string translatable="false" name="config_defaultPictureInPictureSize">192x120</string>
 
     <!-- The default gravity for the picture-in-picture window.
          Currently, this maps to Gravity.BOTTOM | Gravity.RIGHT -->
diff --git a/core/tests/coretests/src/android/net/IpPrefixTest.java b/core/tests/coretests/src/android/net/IpPrefixTest.java
index fcc6389..4f2387d 100644
--- a/core/tests/coretests/src/android/net/IpPrefixTest.java
+++ b/core/tests/coretests/src/android/net/IpPrefixTest.java
@@ -18,14 +18,14 @@
 
 import android.net.IpPrefix;
 import android.os.Parcel;
-import static android.test.MoreAsserts.assertNotEqual;
 import android.test.suitebuilder.annotation.SmallTest;
-
-import static org.junit.Assert.assertArrayEquals;
 import java.net.InetAddress;
 import java.util.Random;
 import junit.framework.TestCase;
 
+import static android.test.MoreAsserts.assertNotEqual;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
 
 public class IpPrefixTest extends TestCase {
 
@@ -242,25 +242,42 @@
 
     @SmallTest
     public void testHashCode() {
-        IpPrefix p;
-        int oldCode = -1;
+        IpPrefix p = new IpPrefix(new byte[4], 0);
         Random random = new Random();
         for (int i = 0; i < 100; i++) {
+            final IpPrefix oldP = p;
             if (random.nextBoolean()) {
                 // IPv4.
                 byte[] b = new byte[4];
                 random.nextBytes(b);
                 p = new IpPrefix(b, random.nextInt(33));
-                assertNotEqual(oldCode, p.hashCode());
-                oldCode = p.hashCode();
             } else {
                 // IPv6.
                 byte[] b = new byte[16];
                 random.nextBytes(b);
                 p = new IpPrefix(b, random.nextInt(129));
-                assertNotEqual(oldCode, p.hashCode());
-                oldCode = p.hashCode();
             }
+            if (p.equals(oldP)) {
+              assertEquals(p.hashCode(), oldP.hashCode());
+            }
+            if (p.hashCode() != oldP.hashCode()) {
+              assertNotEqual(p, oldP);
+            }
+        }
+    }
+
+    @SmallTest
+    public void testHashCodeIsNotConstant() {
+        IpPrefix[] prefixes = {
+            new IpPrefix("2001:db8:f00::ace:d00d/127"),
+            new IpPrefix("192.0.2.0/23"),
+            new IpPrefix("::/0"),
+            new IpPrefix("0.0.0.0/0"),
+        };
+        for (int i = 0; i < prefixes.length; i++) {
+          for (int j = i + 1; j < prefixes.length; j++) {
+            assertNotEqual(prefixes[i].hashCode(), prefixes[j].hashCode());
+          }
         }
     }
 
diff --git a/docs/html/guide/topics/ui/settings.jd b/docs/html/guide/topics/ui/settings.jd
index 619fd26..b51e6d9 100644
--- a/docs/html/guide/topics/ui/settings.jd
+++ b/docs/html/guide/topics/ui/settings.jd
@@ -390,7 +390,9 @@
     <dd>The package part of the component name, as per the {@link
 android.content.Intent#setComponent setComponent()} method.</dd>
 </dl>
-
+<p class="note"><strong>Note: </strong>You must use string literals as the values for these
+intent attributes. You cannot use resource strings, such as <code>@string/foo</code>, to define the attributes.
+</p>
 
 
 <h2 id="Activity">Creating a Preference Activity</h2>
diff --git a/libs/hwui/Android.mk b/libs/hwui/Android.mk
index 8dc502a..4722050 100644
--- a/libs/hwui/Android.mk
+++ b/libs/hwui/Android.mk
@@ -40,6 +40,7 @@
     renderthread/OpenGLPipeline.cpp \
     renderthread/DrawFrameTask.cpp \
     renderthread/EglManager.cpp \
+    renderthread/VulkanManager.cpp \
     renderthread/RenderProxy.cpp \
     renderthread/RenderTask.cpp \
     renderthread/RenderThread.cpp \
diff --git a/libs/hwui/hwui/Typeface.cpp b/libs/hwui/hwui/Typeface.cpp
index 50bc6c3..59b1185 100644
--- a/libs/hwui/hwui/Typeface.cpp
+++ b/libs/hwui/hwui/Typeface.cpp
@@ -49,10 +49,60 @@
 }
 
 Typeface* gDefaultTypeface = NULL;
+pthread_once_t gDefaultTypefaceOnce = PTHREAD_ONCE_INIT;
+
+// This installs a default typeface (from a hardcoded path) that allows
+// layouts to work (not crash on null pointer) before the default
+// typeface is set. This happens if HWUI is used outside of zygote/app_process.
+static minikin::FontCollection *makeFontCollection() {
+    std::vector<minikin::FontFamily *>typefaces;
+    const char *fns[] = {
+        "/system/fonts/Roboto-Regular.ttf",
+    };
+
+    minikin::FontFamily *family = new minikin::FontFamily();
+    for (size_t i = 0; i < sizeof(fns)/sizeof(fns[0]); i++) {
+        const char *fn = fns[i];
+        ALOGD("makeFontCollection adding %s", fn);
+        sk_sp<SkTypeface> skFace = SkTypeface::MakeFromFile(fn);
+        if (skFace != NULL) {
+            // TODO: might be a nice optimization to get access to the underlying font
+            // data, but would require us opening the file ourselves and passing that
+            // to the appropriate Create method of SkTypeface.
+            minikin::MinikinFont *font = new MinikinFontSkia(std::move(skFace), NULL, 0, 0);
+            family->addFont(font);
+            font->Unref();
+        } else {
+            ALOGE("failed to create font %s", fn);
+        }
+    }
+    typefaces.push_back(family);
+
+    minikin::FontCollection *result = new minikin::FontCollection(typefaces);
+    family->Unref();
+    return result;
+}
+
+static void getDefaultTypefaceOnce() {
+  minikin::Layout::init();
+    if (gDefaultTypeface == NULL) {
+        // We expect the client to set a default typeface, but provide a
+        // default so we can make progress before that happens.
+        gDefaultTypeface = new Typeface;
+        gDefaultTypeface->fFontCollection = makeFontCollection();
+        gDefaultTypeface->fSkiaStyle = SkTypeface::kNormal;
+        gDefaultTypeface->fBaseWeight = 400;
+        resolveStyle(gDefaultTypeface);
+    }
+}
 
 Typeface* Typeface::resolveDefault(Typeface* src) {
-    LOG_ALWAYS_FATAL_IF(gDefaultTypeface == nullptr);
-    return src == nullptr ? gDefaultTypeface : src;
+    if (src == NULL) {
+        pthread_once(&gDefaultTypefaceOnce, getDefaultTypefaceOnce);
+        return gDefaultTypeface;
+    } else {
+        return src;
+    }
 }
 
 Typeface* Typeface::createFromTypeface(Typeface* src, SkTypeface::Style style) {
diff --git a/libs/hwui/hwui_static_deps.mk b/libs/hwui/hwui_static_deps.mk
index dca78b3..37126a6 100644
--- a/libs/hwui/hwui_static_deps.mk
+++ b/libs/hwui/hwui_static_deps.mk
@@ -18,6 +18,7 @@
     libutils \
     libEGL \
     libGLESv2 \
+    libvulkan \
     libskia \
     libui \
     libgui \
diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp
index ba13ca5..ca394b2 100644
--- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp
+++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp
@@ -23,37 +23,43 @@
 #include "SkiaPipeline.h"
 #include "SkiaProfileRenderer.h"
 
+#include <SkSurface.h>
 #include <SkTypes.h>
-#include <WindowContextFactory_android.h>
-#include <VulkanWindowContext.h>
+
+#include <GrContext.h>
+#include <GrTypes.h>
+#include <vk/GrVkTypes.h>
 
 #include <android/native_window.h>
 #include <cutils/properties.h>
 #include <strings.h>
 
 using namespace android::uirenderer::renderthread;
-using namespace sk_app;
 
 namespace android {
 namespace uirenderer {
 namespace skiapipeline {
 
+SkiaVulkanPipeline::SkiaVulkanPipeline(renderthread::RenderThread& thread)
+        : SkiaPipeline(thread)
+        , mVkManager(thread.vulkanManager()) {}
+
 MakeCurrentResult SkiaVulkanPipeline::makeCurrent() {
-    return (mWindowContext != nullptr) ?
-        MakeCurrentResult::AlreadyCurrent : MakeCurrentResult::Failed;
+    return MakeCurrentResult::AlreadyCurrent;
 }
 
 Frame SkiaVulkanPipeline::getFrame() {
-    LOG_ALWAYS_FATAL_IF(mWindowContext == nullptr, "Tried to draw into null vulkan context!");
-    mBackbuffer = mWindowContext->getBackbufferSurface();
-    if (mBackbuffer.get() == nullptr) {
-        // try recreating the context?
+    LOG_ALWAYS_FATAL_IF(mVkSurface == nullptr,
+                "drawRenderNode called on a context with no surface!");
+
+    SkSurface* backBuffer = mVkManager.getBackbufferSurface(mVkSurface);
+    if (backBuffer == nullptr) {
         SkDebugf("failed to get backbuffer");
         return Frame(-1, -1, 0);
     }
 
     // TODO: support buffer age if Vulkan API can do it
-    Frame frame(mBackbuffer->width(), mBackbuffer->height(), 0);
+    Frame frame(backBuffer->width(), backBuffer->height(), 0);
     return frame;
 }
 
@@ -66,16 +72,18 @@
         const std::vector<sp<RenderNode>>& renderNodes,
         FrameInfoVisualizer* profiler) {
 
-    if (mBackbuffer.get() == nullptr) {
+    sk_sp<SkSurface> backBuffer = mVkSurface->getBackBufferSurface();
+    if (backBuffer.get() == nullptr) {
         return false;
     }
-    renderFrame(*layerUpdateQueue, dirty, renderNodes, opaque, contentDrawBounds, mBackbuffer);
+    SkiaPipeline::updateLighting(lightGeometry, lightInfo);
+    renderFrame(*layerUpdateQueue, dirty, renderNodes, opaque, contentDrawBounds, backBuffer);
     layerUpdateQueue->clear();
 
     // Draw visual debugging features
     if (CC_UNLIKELY(Properties::showDirtyRegions
             || ProfileType::None != Properties::getProfileType())) {
-        SkCanvas* profileCanvas = mBackbuffer->getCanvas();
+        SkCanvas* profileCanvas = backBuffer->getCanvas();
         SkiaProfileRenderer profileRenderer(profileCanvas);
         profiler->draw(profileRenderer);
         profileCanvas->flush();
@@ -99,11 +107,9 @@
     currentFrameInfo->markSwapBuffers();
 
     if (*requireSwap) {
-        mWindowContext->swapBuffers();
+        mVkManager.swapBuffers(mVkSurface);
     }
 
-    mBackbuffer.reset();
-
     return *requireSwap;
 }
 
@@ -113,6 +119,7 @@
 }
 
 DeferredLayerUpdater* SkiaVulkanPipeline::createTextureLayer() {
+    mVkManager.initialize();
     Layer* layer = new Layer(mRenderThread.renderState(), 0, 0);
     return new DeferredLayerUpdater(layer);
 }
@@ -121,33 +128,24 @@
 }
 
 bool SkiaVulkanPipeline::setSurface(Surface* surface, SwapBehavior swapBehavior) {
-
-    if (mWindowContext) {
-        delete mWindowContext;
-        mWindowContext = nullptr;
+    if (mVkSurface) {
+        mVkManager.destroySurface(mVkSurface);
+        mVkSurface = nullptr;
     }
 
     if (surface) {
-        DisplayParams displayParams;
-        mWindowContext = window_context_factory::NewVulkanForAndroid(surface, displayParams);
-        if (mWindowContext) {
-            DeviceInfo::initialize(mWindowContext->getGrContext()->caps()->maxRenderTargetSize());
-        }
+        mVkSurface = mVkManager.createSurface(surface);
     }
 
-
-    // this doesn't work for if there is more than one CanvasContext available at one time!
-    mRenderThread.setGrContext(mWindowContext ? mWindowContext->getGrContext() : nullptr);
-
-    return mWindowContext != nullptr;
+    return mVkSurface != nullptr;
 }
 
 bool SkiaVulkanPipeline::isSurfaceReady() {
-    return CC_LIKELY(mWindowContext != nullptr) && mWindowContext->isValid();
+    return CC_UNLIKELY(mVkSurface != nullptr);
 }
 
 bool SkiaVulkanPipeline::isContextReady() {
-    return CC_LIKELY(mWindowContext != nullptr);
+    return CC_LIKELY(mVkManager.hasVkContext());
 }
 
 void SkiaVulkanPipeline::invokeFunctor(const RenderThread& thread, Functor* functor) {
diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h
index cdc8692..aab1d7a 100644
--- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h
+++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h
@@ -17,11 +17,7 @@
 #pragma once
 
 #include "SkiaPipeline.h"
-#include <SkSurface.h>
-
-namespace sk_app {
-class WindowContext;
-}
+#include "renderthread/VulkanManager.h"
 
 namespace android {
 namespace uirenderer {
@@ -29,7 +25,7 @@
 
 class SkiaVulkanPipeline : public SkiaPipeline {
 public:
-    SkiaVulkanPipeline(renderthread::RenderThread& thread) : SkiaPipeline(thread) {}
+    SkiaVulkanPipeline(renderthread::RenderThread& thread);
     virtual ~SkiaVulkanPipeline() {}
 
     renderthread::MakeCurrentResult makeCurrent() override;
@@ -53,8 +49,8 @@
     static void invokeFunctor(const renderthread::RenderThread& thread, Functor* functor);
 
 private:
-    sk_app::WindowContext* mWindowContext = nullptr;
-    sk_sp<SkSurface> mBackbuffer;
+    renderthread::VulkanManager& mVkManager;
+    renderthread::VulkanSurface* mVkSurface = nullptr;
 };
 
 } /* namespace skiapipeline */
diff --git a/libs/hwui/renderthread/RenderThread.cpp b/libs/hwui/renderthread/RenderThread.cpp
index 9688340..f3789c8 100644
--- a/libs/hwui/renderthread/RenderThread.cpp
+++ b/libs/hwui/renderthread/RenderThread.cpp
@@ -20,6 +20,7 @@
 #include "CanvasContext.h"
 #include "EglManager.h"
 #include "RenderProxy.h"
+#include "VulkanManager.h"
 
 #include <gui/DisplayEventReceiver.h>
 #include <gui/ISurfaceComposer.h>
@@ -157,7 +158,8 @@
         , mFrameCallbackTaskPending(false)
         , mFrameCallbackTask(nullptr)
         , mRenderState(nullptr)
-        , mEglManager(nullptr) {
+        , mEglManager(nullptr)
+        , mVkManager(nullptr) {
     Properties::load();
     mFrameCallbackTask = new DispatchFrameCallbacks(this);
     mLooper = new Looper(false);
@@ -191,6 +193,7 @@
     mEglManager = new EglManager(*this);
     mRenderState = new RenderState(*this);
     mJankTracker = new JankTracker(mDisplayInfo);
+    mVkManager = new VulkanManager(*this);
 }
 
 int RenderThread::displayEventReceiverCallback(int fd, int events, void* data) {
diff --git a/libs/hwui/renderthread/RenderThread.h b/libs/hwui/renderthread/RenderThread.h
index d8677e13..12050dd 100644
--- a/libs/hwui/renderthread/RenderThread.h
+++ b/libs/hwui/renderthread/RenderThread.h
@@ -46,6 +46,7 @@
 class DispatchFrameCallbacks;
 class EglManager;
 class RenderProxy;
+class VulkanManager;
 
 class TaskQueue {
 public:
@@ -98,6 +99,8 @@
     GrContext* getGrContext() const { return mGrContext.get(); }
     void setGrContext(GrContext* cxt) { mGrContext.reset(cxt); }
 
+    VulkanManager& vulkanManager() { return *mVkManager; }
+
 protected:
     virtual bool threadLoop() override;
 
@@ -150,6 +153,7 @@
     JankTracker* mJankTracker = nullptr;
 
     sk_sp<GrContext> mGrContext;
+    VulkanManager* mVkManager;
 };
 
 } /* namespace renderthread */
diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp
new file mode 100644
index 0000000..4d239bc
--- /dev/null
+++ b/libs/hwui/renderthread/VulkanManager.cpp
@@ -0,0 +1,675 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "VulkanManager.h"
+
+#include "DeviceInfo.h"
+#include "RenderThread.h"
+
+#include <GrContext.h>
+#include <GrTypes.h>
+#include <vk/GrVkTypes.h>
+
+namespace android {
+namespace uirenderer {
+namespace renderthread {
+
+#define GET_PROC(F) m ## F = (PFN_vk ## F) vkGetInstanceProcAddr(instance, "vk" #F)
+#define GET_DEV_PROC(F) m ## F = (PFN_vk ## F) vkGetDeviceProcAddr(device, "vk" #F)
+
+VulkanManager::VulkanManager(RenderThread& thread) : mRenderThread(thread) {
+}
+
+void VulkanManager::destroy() {
+    if (!hasVkContext()) return;
+
+    if (VK_NULL_HANDLE != mCommandPool) {
+        mDestroyCommandPool(mBackendContext->fDevice, mCommandPool, nullptr);
+        mCommandPool = VK_NULL_HANDLE;
+    }
+}
+
+void VulkanManager::initialize() {
+    if (hasVkContext()) { return; }
+
+    auto canPresent = [](VkInstance, VkPhysicalDevice, uint32_t) { return true; };
+
+    mBackendContext.reset(GrVkBackendContext::Create(&mPresentQueueIndex, canPresent));
+
+    // Get all the addresses of needed vulkan functions
+    VkInstance instance = mBackendContext->fInstance;
+    VkDevice device = mBackendContext->fDevice;
+    GET_PROC(CreateAndroidSurfaceKHR);
+    GET_PROC(DestroySurfaceKHR);
+    GET_PROC(GetPhysicalDeviceSurfaceSupportKHR);
+    GET_PROC(GetPhysicalDeviceSurfaceCapabilitiesKHR);
+    GET_PROC(GetPhysicalDeviceSurfaceFormatsKHR);
+    GET_PROC(GetPhysicalDeviceSurfacePresentModesKHR);
+    GET_DEV_PROC(CreateSwapchainKHR);
+    GET_DEV_PROC(DestroySwapchainKHR);
+    GET_DEV_PROC(GetSwapchainImagesKHR);
+    GET_DEV_PROC(AcquireNextImageKHR);
+    GET_DEV_PROC(QueuePresentKHR);
+    GET_DEV_PROC(CreateCommandPool);
+    GET_DEV_PROC(DestroyCommandPool);
+    GET_DEV_PROC(AllocateCommandBuffers);
+    GET_DEV_PROC(FreeCommandBuffers);
+    GET_DEV_PROC(ResetCommandBuffer);
+    GET_DEV_PROC(BeginCommandBuffer);
+    GET_DEV_PROC(EndCommandBuffer);
+    GET_DEV_PROC(CmdPipelineBarrier);
+    GET_DEV_PROC(GetDeviceQueue);
+    GET_DEV_PROC(QueueSubmit);
+    GET_DEV_PROC(QueueWaitIdle);
+    GET_DEV_PROC(DeviceWaitIdle);
+    GET_DEV_PROC(CreateSemaphore);
+    GET_DEV_PROC(DestroySemaphore);
+    GET_DEV_PROC(CreateFence);
+    GET_DEV_PROC(DestroyFence);
+    GET_DEV_PROC(WaitForFences);
+    GET_DEV_PROC(ResetFences);
+
+    // create the command pool for the command buffers
+    if (VK_NULL_HANDLE == mCommandPool) {
+        VkCommandPoolCreateInfo commandPoolInfo;
+        memset(&commandPoolInfo, 0, sizeof(VkCommandPoolCreateInfo));
+        commandPoolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
+        // this needs to be on the render queue
+        commandPoolInfo.queueFamilyIndex = mBackendContext->fGraphicsQueueIndex;
+        commandPoolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
+        SkDEBUGCODE(VkResult res =) mCreateCommandPool(mBackendContext->fDevice,
+                &commandPoolInfo, nullptr, &mCommandPool);
+        SkASSERT(VK_SUCCESS == res);
+    }
+
+    mGetDeviceQueue(mBackendContext->fDevice, mPresentQueueIndex, 0, &mPresentQueue);
+
+    mRenderThread.setGrContext(GrContext::Create(kVulkan_GrBackend,
+            (GrBackendContext) mBackendContext.get()));
+    DeviceInfo::initialize(mRenderThread.getGrContext()->caps()->maxRenderTargetSize());
+}
+
+// Returns the next BackbufferInfo to use for the next draw. The function will make sure all
+// previous uses have finished before returning.
+VulkanSurface::BackbufferInfo* VulkanManager::getAvailableBackbuffer(VulkanSurface* surface) {
+    SkASSERT(surface->mBackbuffers);
+
+    ++surface->mCurrentBackbufferIndex;
+    if (surface->mCurrentBackbufferIndex > surface->mImageCount) {
+        surface->mCurrentBackbufferIndex = 0;
+    }
+
+    VulkanSurface::BackbufferInfo* backbuffer = surface->mBackbuffers +
+            surface->mCurrentBackbufferIndex;
+
+    // Before we reuse a backbuffer, make sure its fences have all signaled so that we can safely
+    // reuse its commands buffers.
+    VkResult res = mWaitForFences(mBackendContext->fDevice, 2, backbuffer->mUsageFences,
+            true, UINT64_MAX);
+    if (res != VK_SUCCESS) {
+        return nullptr;
+    }
+
+    return backbuffer;
+}
+
+
+SkSurface* VulkanManager::getBackbufferSurface(VulkanSurface* surface) {
+    VulkanSurface::BackbufferInfo* backbuffer = getAvailableBackbuffer(surface);
+    SkASSERT(backbuffer);
+
+    VkResult res;
+
+    res = mResetFences(mBackendContext->fDevice, 2, backbuffer->mUsageFences);
+    SkASSERT(VK_SUCCESS == res);
+
+    // The acquire will signal the attached mAcquireSemaphore. We use this to know the image has
+    // finished presenting and that it is safe to begin sending new commands to the returned image.
+    res = mAcquireNextImageKHR(mBackendContext->fDevice, surface->mSwapchain, UINT64_MAX,
+            backbuffer->mAcquireSemaphore, VK_NULL_HANDLE, &backbuffer->mImageIndex);
+
+    if (VK_ERROR_SURFACE_LOST_KHR == res) {
+        // need to figure out how to create a new vkSurface without the platformData*
+        // maybe use attach somehow? but need a Window
+        return nullptr;
+    }
+    if (VK_ERROR_OUT_OF_DATE_KHR == res) {
+        // tear swapchain down and try again
+        if (!createSwapchain(surface)) {
+            return nullptr;
+        }
+
+        // acquire the image
+        res = mAcquireNextImageKHR(mBackendContext->fDevice, surface->mSwapchain, UINT64_MAX,
+                backbuffer->mAcquireSemaphore, VK_NULL_HANDLE, &backbuffer->mImageIndex);
+
+        if (VK_SUCCESS != res) {
+            return nullptr;
+        }
+    }
+
+    // set up layout transfer from initial to color attachment
+    VkImageLayout layout = surface->mImageLayouts[backbuffer->mImageIndex];
+    SkASSERT(VK_IMAGE_LAYOUT_UNDEFINED == layout || VK_IMAGE_LAYOUT_PRESENT_SRC_KHR == layout);
+    VkPipelineStageFlags srcStageMask = (VK_IMAGE_LAYOUT_UNDEFINED == layout) ?
+                                        VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT :
+                                        VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
+    VkPipelineStageFlags dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
+    VkAccessFlags srcAccessMask = (VK_IMAGE_LAYOUT_UNDEFINED == layout) ?
+                                  0 : VK_ACCESS_MEMORY_READ_BIT;
+    VkAccessFlags dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
+
+    VkImageMemoryBarrier imageMemoryBarrier = {
+        VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,    // sType
+        NULL,                                      // pNext
+        srcAccessMask,                             // outputMask
+        dstAccessMask,                             // inputMask
+        layout,                                    // oldLayout
+        VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,  // newLayout
+        mPresentQueueIndex,                        // srcQueueFamilyIndex
+        mBackendContext->fGraphicsQueueIndex,      // dstQueueFamilyIndex
+        surface->mImages[backbuffer->mImageIndex], // image
+        { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }  // subresourceRange
+    };
+    mResetCommandBuffer(backbuffer->mTransitionCmdBuffers[0], 0);
+
+    VkCommandBufferBeginInfo info;
+    memset(&info, 0, sizeof(VkCommandBufferBeginInfo));
+    info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
+    info.flags = 0;
+    mBeginCommandBuffer(backbuffer->mTransitionCmdBuffers[0], &info);
+
+    mCmdPipelineBarrier(backbuffer->mTransitionCmdBuffers[0], srcStageMask, dstStageMask, 0,
+            0, nullptr, 0, nullptr, 1, &imageMemoryBarrier);
+
+    mEndCommandBuffer(backbuffer->mTransitionCmdBuffers[0]);
+
+    VkPipelineStageFlags waitDstStageFlags = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
+    // insert the layout transfer into the queue and wait on the acquire
+    VkSubmitInfo submitInfo;
+    memset(&submitInfo, 0, sizeof(VkSubmitInfo));
+    submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
+    submitInfo.waitSemaphoreCount = 1;
+    // Wait to make sure aquire semaphore set above has signaled.
+    submitInfo.pWaitSemaphores = &backbuffer->mAcquireSemaphore;
+    submitInfo.pWaitDstStageMask = &waitDstStageFlags;
+    submitInfo.commandBufferCount = 1;
+    submitInfo.pCommandBuffers = &backbuffer->mTransitionCmdBuffers[0];
+    submitInfo.signalSemaphoreCount = 0;
+
+    // Attach first fence to submission here so we can track when the command buffer finishes.
+    mQueueSubmit(mBackendContext->fQueue, 1, &submitInfo, backbuffer->mUsageFences[0]);
+
+    // We need to notify Skia that we changed the layout of the wrapped VkImage
+    GrVkImageInfo* imageInfo;
+    sk_sp<SkSurface> skSurface = surface->mSurfaces[backbuffer->mImageIndex];
+    skSurface->getRenderTargetHandle((GrBackendObject*)&imageInfo,
+            SkSurface::kFlushRead_BackendHandleAccess);
+    imageInfo->updateImageLayout(VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
+
+    surface->mBackbuffer = std::move(skSurface);
+    return surface->mBackbuffer.get();
+}
+
+void VulkanManager::destroyBuffers(VulkanSurface* surface) {
+    if (surface->mBackbuffers) {
+        for (uint32_t i = 0; i < surface->mImageCount + 1; ++i) {
+            mWaitForFences(mBackendContext->fDevice, 2, surface->mBackbuffers[i].mUsageFences, true,
+                    UINT64_MAX);
+            surface->mBackbuffers[i].mImageIndex = -1;
+            mDestroySemaphore(mBackendContext->fDevice, surface->mBackbuffers[i].mAcquireSemaphore,
+                    nullptr);
+            mDestroySemaphore(mBackendContext->fDevice, surface->mBackbuffers[i].mRenderSemaphore,
+                    nullptr);
+            mFreeCommandBuffers(mBackendContext->fDevice, mCommandPool, 2,
+                    surface->mBackbuffers[i].mTransitionCmdBuffers);
+            mDestroyFence(mBackendContext->fDevice, surface->mBackbuffers[i].mUsageFences[0], 0);
+            mDestroyFence(mBackendContext->fDevice, surface->mBackbuffers[i].mUsageFences[1], 0);
+        }
+    }
+
+    delete[] surface->mBackbuffers;
+    surface->mBackbuffers = nullptr;
+    delete[] surface->mSurfaces;
+    surface->mSurfaces = nullptr;
+    delete[] surface->mImageLayouts;
+    surface->mImageLayouts = nullptr;
+    delete[] surface->mImages;
+    surface->mImages = nullptr;
+}
+
+void VulkanManager::destroySurface(VulkanSurface* surface) {
+    // Make sure all submit commands have finished before starting to destroy objects.
+    if (VK_NULL_HANDLE != mPresentQueue) {
+        mQueueWaitIdle(mPresentQueue);
+    }
+    mDeviceWaitIdle(mBackendContext->fDevice);
+
+    destroyBuffers(surface);
+
+    if (VK_NULL_HANDLE != surface->mSwapchain) {
+        mDestroySwapchainKHR(mBackendContext->fDevice, surface->mSwapchain, nullptr);
+        surface->mSwapchain = VK_NULL_HANDLE;
+    }
+
+    if (VK_NULL_HANDLE != surface->mVkSurface) {
+        mDestroySurfaceKHR(mBackendContext->fInstance, surface->mVkSurface, nullptr);
+        surface->mVkSurface = VK_NULL_HANDLE;
+    }
+    delete surface;
+}
+
+void VulkanManager::createBuffers(VulkanSurface* surface, VkFormat format, VkExtent2D extent) {
+    mGetSwapchainImagesKHR(mBackendContext->fDevice, surface->mSwapchain, &surface->mImageCount,
+            nullptr);
+    SkASSERT(surface->mImageCount);
+    surface->mImages = new VkImage[surface->mImageCount];
+    mGetSwapchainImagesKHR(mBackendContext->fDevice, surface->mSwapchain,
+            &surface->mImageCount, surface->mImages);
+
+    SkSurfaceProps props(0, kUnknown_SkPixelGeometry);
+
+    bool wantSRGB = VK_FORMAT_R8G8B8A8_SRGB == format;
+    GrPixelConfig config = wantSRGB ? kSRGBA_8888_GrPixelConfig : kRGBA_8888_GrPixelConfig;
+
+    // set up initial image layouts and create surfaces
+    surface->mImageLayouts = new VkImageLayout[surface->mImageCount];
+    surface->mSurfaces = new sk_sp<SkSurface>[surface->mImageCount];
+    for (uint32_t i = 0; i < surface->mImageCount; ++i) {
+        GrBackendRenderTargetDesc desc;
+        GrVkImageInfo info;
+        info.fImage = surface->mImages[i];
+        info.fAlloc = { VK_NULL_HANDLE, 0, 0, 0 };
+        info.fImageLayout = VK_IMAGE_LAYOUT_UNDEFINED;
+        info.fImageTiling = VK_IMAGE_TILING_OPTIMAL;
+        info.fFormat = format;
+        info.fLevelCount = 1;
+
+        desc.fWidth = extent.width;
+        desc.fHeight = extent.height;
+        desc.fConfig = config;
+        desc.fOrigin = kTopLeft_GrSurfaceOrigin;
+        desc.fSampleCnt = 0;
+        desc.fStencilBits = 0;
+        desc.fRenderTargetHandle = (GrBackendObject) &info;
+
+        surface->mSurfaces[i] = SkSurface::MakeFromBackendRenderTarget(mRenderThread.getGrContext(),
+                desc, &props);
+        surface->mImageLayouts[i] = VK_IMAGE_LAYOUT_UNDEFINED;
+    }
+
+    SkASSERT(mCommandPool != VK_NULL_HANDLE);
+
+    // set up the backbuffers
+    VkSemaphoreCreateInfo semaphoreInfo;
+    memset(&semaphoreInfo, 0, sizeof(VkSemaphoreCreateInfo));
+    semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
+    semaphoreInfo.pNext = nullptr;
+    semaphoreInfo.flags = 0;
+    VkCommandBufferAllocateInfo commandBuffersInfo;
+    memset(&commandBuffersInfo, 0, sizeof(VkCommandBufferAllocateInfo));
+    commandBuffersInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
+    commandBuffersInfo.pNext = nullptr;
+    commandBuffersInfo.commandPool = mCommandPool;
+    commandBuffersInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
+    commandBuffersInfo.commandBufferCount = 2;
+    VkFenceCreateInfo fenceInfo;
+    memset(&fenceInfo, 0, sizeof(VkFenceCreateInfo));
+    fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
+    fenceInfo.pNext = nullptr;
+    fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
+
+    // we create one additional backbuffer structure here, because we want to
+    // give the command buffers they contain a chance to finish before we cycle back
+    surface->mBackbuffers = new VulkanSurface::BackbufferInfo[surface->mImageCount + 1];
+    for (uint32_t i = 0; i < surface->mImageCount + 1; ++i) {
+        SkDEBUGCODE(VkResult res);
+        surface->mBackbuffers[i].mImageIndex = -1;
+        SkDEBUGCODE(res = ) mCreateSemaphore(mBackendContext->fDevice, &semaphoreInfo, nullptr,
+                &surface->mBackbuffers[i].mAcquireSemaphore);
+        SkDEBUGCODE(res = ) mCreateSemaphore(mBackendContext->fDevice, &semaphoreInfo, nullptr,
+                &surface->mBackbuffers[i].mRenderSemaphore);
+        SkDEBUGCODE(res = ) mAllocateCommandBuffers(mBackendContext->fDevice, &commandBuffersInfo,
+                surface->mBackbuffers[i].mTransitionCmdBuffers);
+        SkDEBUGCODE(res = ) mCreateFence(mBackendContext->fDevice, &fenceInfo, nullptr,
+                &surface->mBackbuffers[i].mUsageFences[0]);
+        SkDEBUGCODE(res = ) mCreateFence(mBackendContext->fDevice, &fenceInfo, nullptr,
+                &surface->mBackbuffers[i].mUsageFences[1]);
+        SkASSERT(VK_SUCCESS == res);
+    }
+    surface->mCurrentBackbufferIndex = surface->mImageCount;
+}
+
+bool VulkanManager::createSwapchain(VulkanSurface* surface) {
+    // check for capabilities
+    VkSurfaceCapabilitiesKHR caps;
+    VkResult res = mGetPhysicalDeviceSurfaceCapabilitiesKHR(mBackendContext->fPhysicalDevice,
+            surface->mVkSurface, &caps);
+    if (VK_SUCCESS != res) {
+        return false;
+    }
+
+    uint32_t surfaceFormatCount;
+    res = mGetPhysicalDeviceSurfaceFormatsKHR(mBackendContext->fPhysicalDevice, surface->mVkSurface,
+            &surfaceFormatCount, nullptr);
+    if (VK_SUCCESS != res) {
+        return false;
+    }
+
+    SkAutoMalloc surfaceFormatAlloc(surfaceFormatCount * sizeof(VkSurfaceFormatKHR));
+    VkSurfaceFormatKHR* surfaceFormats = (VkSurfaceFormatKHR*)surfaceFormatAlloc.get();
+    res = mGetPhysicalDeviceSurfaceFormatsKHR(mBackendContext->fPhysicalDevice, surface->mVkSurface,
+            &surfaceFormatCount, surfaceFormats);
+    if (VK_SUCCESS != res) {
+        return false;
+    }
+
+    uint32_t presentModeCount;
+    res = mGetPhysicalDeviceSurfacePresentModesKHR(mBackendContext->fPhysicalDevice,
+            surface->mVkSurface, &presentModeCount, nullptr);
+    if (VK_SUCCESS != res) {
+        return false;
+    }
+
+    SkAutoMalloc presentModeAlloc(presentModeCount * sizeof(VkPresentModeKHR));
+    VkPresentModeKHR* presentModes = (VkPresentModeKHR*)presentModeAlloc.get();
+    res = mGetPhysicalDeviceSurfacePresentModesKHR(mBackendContext->fPhysicalDevice,
+            surface->mVkSurface, &presentModeCount, presentModes);
+    if (VK_SUCCESS != res) {
+        return false;
+    }
+
+    VkExtent2D extent = caps.currentExtent;
+    // clamp width; to handle currentExtent of -1 and  protect us from broken hints
+    if (extent.width < caps.minImageExtent.width) {
+        extent.width = caps.minImageExtent.width;
+    }
+    SkASSERT(extent.width <= caps.maxImageExtent.width);
+    // clamp height
+    if (extent.height < caps.minImageExtent.height) {
+        extent.height = caps.minImageExtent.height;
+    }
+    SkASSERT(extent.height <= caps.maxImageExtent.height);
+
+    uint32_t imageCount = caps.minImageCount + 2;
+    if (caps.maxImageCount > 0 && imageCount > caps.maxImageCount) {
+        // Application must settle for fewer images than desired:
+        imageCount = caps.maxImageCount;
+    }
+
+    // Currently Skia requires the images to be color attchments and support all transfer
+    // operations.
+    VkImageUsageFlags usageFlags = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT |
+                                   VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
+                                   VK_IMAGE_USAGE_TRANSFER_DST_BIT;
+    SkASSERT((caps.supportedUsageFlags & usageFlags) == usageFlags);
+    SkASSERT(caps.supportedTransforms & caps.currentTransform);
+    SkASSERT(caps.supportedCompositeAlpha & (VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR |
+                                             VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR));
+    VkCompositeAlphaFlagBitsKHR composite_alpha =
+        (caps.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR) ?
+                                        VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR :
+                                        VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
+
+    // Pick our surface format. For now, just make sure it matches our sRGB request:
+    VkFormat surfaceFormat = VK_FORMAT_UNDEFINED;
+    VkColorSpaceKHR colorSpace = VK_COLORSPACE_SRGB_NONLINEAR_KHR;
+
+    bool wantSRGB = false;
+#ifdef ANDROID_ENABLE_LINEAR_BLENDING
+    wantSRGB = true;
+#endif
+    for (uint32_t i = 0; i < surfaceFormatCount; ++i) {
+        // We are assuming we can get either R8G8B8A8_UNORM or R8G8B8A8_SRGB
+        VkFormat desiredFormat = wantSRGB ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM;
+        if (desiredFormat == surfaceFormats[i].format) {
+            surfaceFormat = surfaceFormats[i].format;
+            colorSpace = surfaceFormats[i].colorSpace;
+        }
+    }
+
+    if (VK_FORMAT_UNDEFINED == surfaceFormat) {
+        return false;
+    }
+
+    // If mailbox mode is available, use it, as it is the lowest-latency non-
+    // tearing mode. If not, fall back to FIFO which is always available.
+    VkPresentModeKHR mode = VK_PRESENT_MODE_FIFO_KHR;
+    for (uint32_t i = 0; i < presentModeCount; ++i) {
+        // use mailbox
+        if (VK_PRESENT_MODE_MAILBOX_KHR == presentModes[i]) {
+            mode = presentModes[i];
+            break;
+        }
+    }
+
+    VkSwapchainCreateInfoKHR swapchainCreateInfo;
+    memset(&swapchainCreateInfo, 0, sizeof(VkSwapchainCreateInfoKHR));
+    swapchainCreateInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
+    swapchainCreateInfo.surface = surface->mVkSurface;
+    swapchainCreateInfo.minImageCount = imageCount;
+    swapchainCreateInfo.imageFormat = surfaceFormat;
+    swapchainCreateInfo.imageColorSpace = colorSpace;
+    swapchainCreateInfo.imageExtent = extent;
+    swapchainCreateInfo.imageArrayLayers = 1;
+    swapchainCreateInfo.imageUsage = usageFlags;
+
+    uint32_t queueFamilies[] = { mBackendContext->fGraphicsQueueIndex, mPresentQueueIndex };
+    if (mBackendContext->fGraphicsQueueIndex != mPresentQueueIndex) {
+        swapchainCreateInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
+        swapchainCreateInfo.queueFamilyIndexCount = 2;
+        swapchainCreateInfo.pQueueFamilyIndices = queueFamilies;
+    } else {
+        swapchainCreateInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
+        swapchainCreateInfo.queueFamilyIndexCount = 0;
+        swapchainCreateInfo.pQueueFamilyIndices = nullptr;
+    }
+
+    swapchainCreateInfo.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
+    swapchainCreateInfo.compositeAlpha = composite_alpha;
+    swapchainCreateInfo.presentMode = mode;
+    swapchainCreateInfo.clipped = true;
+    swapchainCreateInfo.oldSwapchain = surface->mSwapchain;
+
+    res = mCreateSwapchainKHR(mBackendContext->fDevice, &swapchainCreateInfo, nullptr,
+            &surface->mSwapchain);
+    if (VK_SUCCESS != res) {
+        return false;
+    }
+
+    // destroy the old swapchain
+    if (swapchainCreateInfo.oldSwapchain != VK_NULL_HANDLE) {
+        mDeviceWaitIdle(mBackendContext->fDevice);
+
+        destroyBuffers(surface);
+
+        mDestroySwapchainKHR(mBackendContext->fDevice, swapchainCreateInfo.oldSwapchain, nullptr);
+    }
+
+    createBuffers(surface, surfaceFormat, extent);
+
+    return true;
+}
+
+
+VulkanSurface* VulkanManager::createSurface(ANativeWindow* window) {
+    initialize();
+
+    if (!window) {
+        return nullptr;
+    }
+
+    VulkanSurface* surface = new VulkanSurface();
+
+    VkAndroidSurfaceCreateInfoKHR surfaceCreateInfo;
+    memset(&surfaceCreateInfo, 0, sizeof(VkAndroidSurfaceCreateInfoKHR));
+    surfaceCreateInfo.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR;
+    surfaceCreateInfo.pNext = nullptr;
+    surfaceCreateInfo.flags = 0;
+    surfaceCreateInfo.window = window;
+
+    VkResult res = mCreateAndroidSurfaceKHR(mBackendContext->fInstance, &surfaceCreateInfo,
+            nullptr, &surface->mVkSurface);
+    if (VK_SUCCESS != res) {
+        delete surface;
+        return nullptr;
+    }
+
+SkDEBUGCODE(
+    VkBool32 supported;
+    res = mGetPhysicalDeviceSurfaceSupportKHR(mBackendContext->fPhysicalDevice,
+            mPresentQueueIndex, surface->mVkSurface, &supported);
+    // All physical devices and queue families on Android must be capable of presentation with any
+    // native window.
+    SkASSERT(VK_SUCCESS == res && supported);
+);
+
+    if (!createSwapchain(surface)) {
+        destroySurface(surface);
+        return nullptr;
+    }
+
+    return surface;
+}
+
+// Helper to know which src stage flags we need to set when transitioning to the present layout
+static VkPipelineStageFlags layoutToPipelineStageFlags(const VkImageLayout layout) {
+    if (VK_IMAGE_LAYOUT_GENERAL == layout) {
+        return VK_PIPELINE_STAGE_ALL_COMMANDS_BIT;
+    } else if (VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL == layout ||
+               VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL == layout) {
+        return VK_PIPELINE_STAGE_TRANSFER_BIT;
+    } else if (VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL == layout ||
+               VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL == layout ||
+               VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL == layout ||
+               VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL == layout) {
+        return VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT;
+    } else if (VK_IMAGE_LAYOUT_PREINITIALIZED == layout) {
+        return VK_PIPELINE_STAGE_HOST_BIT;
+    }
+
+    SkASSERT(VK_IMAGE_LAYOUT_UNDEFINED == layout);
+    return VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
+}
+
+// Helper to know which src access mask we need to set when transitioning to the present layout
+static VkAccessFlags layoutToSrcAccessMask(const VkImageLayout layout) {
+    VkAccessFlags flags = 0;
+    if (VK_IMAGE_LAYOUT_GENERAL == layout) {
+        flags = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
+                VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT |
+                VK_ACCESS_TRANSFER_WRITE_BIT |
+                VK_ACCESS_TRANSFER_READ_BIT |
+                VK_ACCESS_SHADER_READ_BIT |
+                VK_ACCESS_HOST_WRITE_BIT | VK_ACCESS_HOST_READ_BIT;
+    } else if (VK_IMAGE_LAYOUT_PREINITIALIZED == layout) {
+        flags = VK_ACCESS_HOST_WRITE_BIT;
+    } else if (VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL == layout) {
+        flags = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
+    } else if (VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL == layout) {
+        flags = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
+    } else if (VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL == layout) {
+        flags = VK_ACCESS_TRANSFER_WRITE_BIT;
+    } else if (VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL == layout) {
+        flags = VK_ACCESS_TRANSFER_READ_BIT;
+    } else if (VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL == layout) {
+        flags = VK_ACCESS_SHADER_READ_BIT;
+    }
+    return flags;
+}
+
+void VulkanManager::swapBuffers(VulkanSurface* surface) {
+    VulkanSurface::BackbufferInfo* backbuffer = surface->mBackbuffers +
+            surface->mCurrentBackbufferIndex;
+    GrVkImageInfo* imageInfo;
+    SkSurface* skSurface = surface->mSurfaces[backbuffer->mImageIndex].get();
+    skSurface->getRenderTargetHandle((GrBackendObject*)&imageInfo,
+            SkSurface::kFlushRead_BackendHandleAccess);
+    // Check to make sure we never change the actually wrapped image
+    SkASSERT(imageInfo->fImage == surface->mImages[backbuffer->mImageIndex]);
+
+    // We need to transition the image to VK_IMAGE_LAYOUT_PRESENT_SRC_KHR and make sure that all
+    // previous work is complete for before presenting. So we first add the necessary barrier here.
+    VkImageLayout layout = imageInfo->fImageLayout;
+    VkPipelineStageFlags srcStageMask = layoutToPipelineStageFlags(layout);
+    VkPipelineStageFlags dstStageMask = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT;
+    VkAccessFlags srcAccessMask = layoutToSrcAccessMask(layout);
+    VkAccessFlags dstAccessMask = VK_ACCESS_MEMORY_READ_BIT;
+
+    VkImageMemoryBarrier imageMemoryBarrier = {
+        VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,    // sType
+        NULL,                                      // pNext
+        srcAccessMask,                             // outputMask
+        dstAccessMask,                             // inputMask
+        layout,                                    // oldLayout
+        VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,           // newLayout
+        mBackendContext->fGraphicsQueueIndex,      // srcQueueFamilyIndex
+        mPresentQueueIndex,                        // dstQueueFamilyIndex
+        surface->mImages[backbuffer->mImageIndex], // image
+        { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }  // subresourceRange
+    };
+
+    mResetCommandBuffer(backbuffer->mTransitionCmdBuffers[1], 0);
+    VkCommandBufferBeginInfo info;
+    memset(&info, 0, sizeof(VkCommandBufferBeginInfo));
+    info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
+    info.flags = 0;
+    mBeginCommandBuffer(backbuffer->mTransitionCmdBuffers[1], &info);
+    mCmdPipelineBarrier(backbuffer->mTransitionCmdBuffers[1], srcStageMask, dstStageMask, 0,
+            0, nullptr, 0, nullptr, 1, &imageMemoryBarrier);
+    mEndCommandBuffer(backbuffer->mTransitionCmdBuffers[1]);
+
+    surface->mImageLayouts[backbuffer->mImageIndex] = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
+
+    // insert the layout transfer into the queue and wait on the acquire
+    VkSubmitInfo submitInfo;
+    memset(&submitInfo, 0, sizeof(VkSubmitInfo));
+    submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
+    submitInfo.waitSemaphoreCount = 0;
+    submitInfo.pWaitDstStageMask = 0;
+    submitInfo.commandBufferCount = 1;
+    submitInfo.pCommandBuffers = &backbuffer->mTransitionCmdBuffers[1];
+    submitInfo.signalSemaphoreCount = 1;
+    // When this command buffer finishes we will signal this semaphore so that we know it is now
+    // safe to present the image to the screen.
+    submitInfo.pSignalSemaphores = &backbuffer->mRenderSemaphore;
+
+    // Attach second fence to submission here so we can track when the command buffer finishes.
+    mQueueSubmit(mBackendContext->fQueue, 1, &submitInfo, backbuffer->mUsageFences[1]);
+
+    // Submit present operation to present queue. We use a semaphore here to make sure all rendering
+    // to the image is complete and that the layout has been change to present on the graphics
+    // queue.
+    const VkPresentInfoKHR presentInfo =
+    {
+        VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, // sType
+        NULL, // pNext
+        1, // waitSemaphoreCount
+        &backbuffer->mRenderSemaphore, // pWaitSemaphores
+        1, // swapchainCount
+        &surface->mSwapchain, // pSwapchains
+        &backbuffer->mImageIndex, // pImageIndices
+        NULL // pResults
+    };
+
+    mQueuePresentKHR(mPresentQueue, &presentInfo);
+
+    surface->mBackbuffer.reset();
+}
+
+} /* namespace renderthread */
+} /* namespace uirenderer */
+} /* namespace android */
diff --git a/libs/hwui/renderthread/VulkanManager.h b/libs/hwui/renderthread/VulkanManager.h
new file mode 100644
index 0000000..f0e3320
--- /dev/null
+++ b/libs/hwui/renderthread/VulkanManager.h
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef VULKANMANAGER_H
+#define VULKANMANAGER_H
+
+#include <SkSurface.h>
+#include <vk/GrVkBackendContext.h>
+
+#include <vulkan/vulkan.h>
+
+namespace android {
+namespace uirenderer {
+namespace renderthread {
+
+class RenderThread;
+
+class VulkanSurface {
+public:
+    VulkanSurface() {}
+
+    sk_sp<SkSurface> getBackBufferSurface() { return mBackbuffer; }
+
+private:
+    friend class VulkanManager;
+    struct BackbufferInfo {
+        uint32_t        mImageIndex;          // image this is associated with
+        VkSemaphore     mAcquireSemaphore;    // we signal on this for acquisition of image
+        VkSemaphore     mRenderSemaphore;     // we wait on this for rendering to be done
+        VkCommandBuffer mTransitionCmdBuffers[2]; // to transition layout between present and render
+        // We use these fences to make sure the above Command buffers have finished their work
+        // before attempting to reuse them or destroy them.
+        VkFence         mUsageFences[2];
+    };
+
+    sk_sp<SkSurface> mBackbuffer;
+
+    VkSurfaceKHR mVkSurface = VK_NULL_HANDLE;
+    VkSwapchainKHR mSwapchain = VK_NULL_HANDLE;
+
+    BackbufferInfo* mBackbuffers;
+    uint32_t mCurrentBackbufferIndex;
+
+    uint32_t mImageCount;
+    VkImage* mImages;
+    VkImageLayout* mImageLayouts;
+    sk_sp<SkSurface>* mSurfaces;
+};
+
+// This class contains the shared global Vulkan objects, such as VkInstance, VkDevice and VkQueue,
+// which are re-used by CanvasContext. This class is created once and should be used by all vulkan
+// windowing contexts. The VulkanManager must be initialized before use.
+class VulkanManager {
+public:
+    // Sets up the vulkan context that is shared amonst all clients of the VulkanManager. This must
+    // be call once before use of the VulkanManager. Multiple calls after the first will simiply
+    // return.
+    void initialize();
+
+    // Quick check to see if the VulkanManager has been initialized.
+    bool hasVkContext() { return mBackendContext.get() != nullptr; }
+
+    // Given a window this creates a new VkSurfaceKHR and VkSwapchain and stores them inside a new
+    // VulkanSurface object which is returned.
+    VulkanSurface* createSurface(ANativeWindow* window);
+
+    // Destroy the VulkanSurface and all associated vulkan objects.
+    void destroySurface(VulkanSurface* surface);
+
+    // Cleans up all the global state in the VulkanManger.
+    void destroy();
+
+    // No work is needed to make a VulkanSurface current, and all functions require that a
+    // VulkanSurface is passed into them so we just return true here.
+    bool isCurrent(VulkanSurface* surface) { return true; }
+
+    // Returns an SkSurface which wraps the next image returned from vkAcquireNextImageKHR. It also
+    // will transition the VkImage from a present layout to color attachment so that it can be used
+    // by the client for drawing.
+    SkSurface* getBackbufferSurface(VulkanSurface* surface);
+
+    // Presents the current VkImage.
+    void swapBuffers(VulkanSurface* surface);
+
+private:
+    friend class RenderThread;
+
+    explicit VulkanManager(RenderThread& thread);
+    ~VulkanManager() { destroy(); }
+
+    void destroyBuffers(VulkanSurface* surface);
+
+    bool createSwapchain(VulkanSurface* surface);
+    void createBuffers(VulkanSurface* surface, VkFormat format, VkExtent2D extent);
+
+    VulkanSurface::BackbufferInfo* getAvailableBackbuffer(VulkanSurface* surface);
+
+    // simple wrapper class that exists only to initialize a pointer to NULL
+    template <typename FNPTR_TYPE> class VkPtr {
+    public:
+        VkPtr() : fPtr(NULL) {}
+        VkPtr operator=(FNPTR_TYPE ptr) { fPtr = ptr; return *this; }
+        operator FNPTR_TYPE() const { return fPtr; }
+    private:
+        FNPTR_TYPE fPtr;
+    };
+
+    // WSI interface functions
+    VkPtr<PFN_vkCreateAndroidSurfaceKHR> mCreateAndroidSurfaceKHR;
+    VkPtr<PFN_vkDestroySurfaceKHR> mDestroySurfaceKHR;
+    VkPtr<PFN_vkGetPhysicalDeviceSurfaceSupportKHR> mGetPhysicalDeviceSurfaceSupportKHR;
+    VkPtr<PFN_vkGetPhysicalDeviceSurfaceCapabilitiesKHR> mGetPhysicalDeviceSurfaceCapabilitiesKHR;
+    VkPtr<PFN_vkGetPhysicalDeviceSurfaceFormatsKHR> mGetPhysicalDeviceSurfaceFormatsKHR;
+    VkPtr<PFN_vkGetPhysicalDeviceSurfacePresentModesKHR> mGetPhysicalDeviceSurfacePresentModesKHR;
+
+    VkPtr<PFN_vkCreateSwapchainKHR> mCreateSwapchainKHR;
+    VkPtr<PFN_vkDestroySwapchainKHR> mDestroySwapchainKHR;
+    VkPtr<PFN_vkGetSwapchainImagesKHR> mGetSwapchainImagesKHR;
+    VkPtr<PFN_vkAcquireNextImageKHR> mAcquireNextImageKHR;
+    VkPtr<PFN_vkQueuePresentKHR> mQueuePresentKHR;
+    VkPtr<PFN_vkCreateSharedSwapchainsKHR> mCreateSharedSwapchainsKHR;
+
+    // Additional vulkan functions
+    VkPtr<PFN_vkCreateCommandPool> mCreateCommandPool;
+    VkPtr<PFN_vkDestroyCommandPool> mDestroyCommandPool;
+    VkPtr<PFN_vkAllocateCommandBuffers> mAllocateCommandBuffers;
+    VkPtr<PFN_vkFreeCommandBuffers> mFreeCommandBuffers;
+    VkPtr<PFN_vkResetCommandBuffer> mResetCommandBuffer;
+    VkPtr<PFN_vkBeginCommandBuffer> mBeginCommandBuffer;
+    VkPtr<PFN_vkEndCommandBuffer> mEndCommandBuffer;
+    VkPtr<PFN_vkCmdPipelineBarrier> mCmdPipelineBarrier;
+
+    VkPtr<PFN_vkGetDeviceQueue> mGetDeviceQueue;
+    VkPtr<PFN_vkQueueSubmit> mQueueSubmit;
+    VkPtr<PFN_vkQueueWaitIdle> mQueueWaitIdle;
+    VkPtr<PFN_vkDeviceWaitIdle> mDeviceWaitIdle;
+
+    VkPtr<PFN_vkCreateSemaphore> mCreateSemaphore;
+    VkPtr<PFN_vkDestroySemaphore> mDestroySemaphore;
+    VkPtr<PFN_vkCreateFence> mCreateFence;
+    VkPtr<PFN_vkDestroyFence> mDestroyFence;
+    VkPtr<PFN_vkWaitForFences> mWaitForFences;
+    VkPtr<PFN_vkResetFences> mResetFences;
+
+    RenderThread& mRenderThread;
+
+    sk_sp<const GrVkBackendContext> mBackendContext;
+    uint32_t mPresentQueueIndex;
+    VkQueue mPresentQueue = VK_NULL_HANDLE;
+    VkCommandPool mCommandPool = VK_NULL_HANDLE;
+};
+
+} /* namespace renderthread */
+} /* namespace uirenderer */
+} /* namespace android */
+
+#endif /* VULKANMANAGER_H */
+
diff --git a/packages/Shell/Android.mk b/packages/Shell/Android.mk
index 2170cc1..935d09b 100644
--- a/packages/Shell/Android.mk
+++ b/packages/Shell/Android.mk
@@ -5,6 +5,13 @@
 
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
+LOCAL_SRC_FILES += \
+        ../../../native/cmds/dumpstate/binder/android/os/IDumpstate.aidl \
+        ../../../native/cmds/dumpstate/binder/android/os/IDumpstateListener.aidl \
+        ../../../native/cmds/dumpstate/binder/android/os/IDumpstateToken.aidl
+
+LOCAL_AIDL_INCLUDES = frameworks/native/cmds/dumpstate/binder
+
 LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4
 
 LOCAL_PACKAGE_NAME := Shell
diff --git a/packages/Shell/src/com/android/shell/BugreportProgressService.java b/packages/Shell/src/com/android/shell/BugreportProgressService.java
index dedb9ac..47abd4f 100644
--- a/packages/Shell/src/com/android/shell/BugreportProgressService.java
+++ b/packages/Shell/src/com/android/shell/BugreportProgressService.java
@@ -31,6 +31,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.PrintWriter;
+import java.io.StringWriter;
 import java.nio.charset.StandardCharsets;
 import java.text.NumberFormat;
 import java.util.ArrayList;
@@ -45,6 +46,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.util.FastPrintWriter;
 
 import com.google.android.collect.Lists;
 
@@ -69,10 +71,16 @@
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
+import android.os.IBinder.DeathRecipient;
+import android.os.IDumpstate;
+import android.os.IDumpstateListener;
+import android.os.IDumpstateToken;
 import android.os.Looper;
 import android.os.Message;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.RemoteException;
+import android.os.ServiceManager;
 import android.os.SystemProperties;
 import android.os.Vibrator;
 import android.support.v4.content.FileProvider;
@@ -146,10 +154,9 @@
     static final String EXTRA_INFO = "android.intent.extra.INFO";
 
     private static final int MSG_SERVICE_COMMAND = 1;
-    private static final int MSG_POLL = 2;
-    private static final int MSG_DELAYED_SCREENSHOT = 3;
-    private static final int MSG_SCREENSHOT_REQUEST = 4;
-    private static final int MSG_SCREENSHOT_RESPONSE = 5;
+    private static final int MSG_DELAYED_SCREENSHOT = 2;
+    private static final int MSG_SCREENSHOT_REQUEST = 3;
+    private static final int MSG_SCREENSHOT_RESPONSE = 4;
 
     // Passed to Message.obtain() when msg.arg2 is not used.
     private static final int UNUSED_ARG2 = -2;
@@ -165,16 +172,9 @@
      */
     static final int SCREENSHOT_DELAY_SECONDS = 3;
 
-    /** Polling frequency, in milliseconds. */
-    static final long POLLING_FREQUENCY = 2 * DateUtils.SECOND_IN_MILLIS;
-
-    /** How long (in ms) a dumpstate process will be monitored if it didn't show progress. */
-    private static final long INACTIVITY_TIMEOUT = 10 * DateUtils.MINUTE_IN_MILLIS;
-
-    /** System properties used for monitoring progress. */
+    // TODO: will be gone once fully migrated to Binder
+    /** System properties used to communicate with dumpstate progress. */
     private static final String DUMPSTATE_PREFIX = "dumpstate.";
-    private static final String PROGRESS_SUFFIX = ".progress";
-    private static final String MAX_SUFFIX = ".max";
     private static final String NAME_SUFFIX = ".name";
 
     /** System property (and value) used to stop dumpstate. */
@@ -190,7 +190,7 @@
     private static final String SCREENSHOT_DIR = "bugreports";
 
     /** Managed dumpstate processes (keyed by id) */
-    private final SparseArray<BugreportInfo> mProcesses = new SparseArray<>();
+    private final SparseArray<DumpstateListener> mProcesses = new SparseArray<>();
 
     private Context mContext;
     private ServiceHandler mMainHandler;
@@ -267,14 +267,16 @@
     protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
         final int size = mProcesses.size();
         if (size == 0) {
-            writer.printf("No monitored processes");
+            writer.println("No monitored processes");
             return;
         }
-        writer.printf("Foreground id: %d\n\n", mForegroundId);
-        writer.printf("Monitored dumpstate processes\n");
-        writer.printf("-----------------------------\n");
+        writer.print("Foreground id: "); writer.println(mForegroundId);
+        writer.println("\n");
+        writer.println("Monitored dumpstate processes");
+        writer.println("-----------------------------");
         for (int i = 0; i < size; i++) {
-            writer.printf("%s\n", mProcesses.valueAt(i));
+            writer.print("#"); writer.println(i + 1);
+            writer.println(mProcesses.valueAt(i).info);
         }
     }
 
@@ -288,11 +290,6 @@
 
         @Override
         public void handleMessage(Message msg) {
-            if (msg.what == MSG_POLL) {
-                poll();
-                return;
-            }
-
             if (msg.what == MSG_DELAYED_SCREENSHOT) {
                 takeScreenshot(msg.arg1, msg.arg2);
                 return;
@@ -339,7 +336,6 @@
                         stopSelfWhenDone();
                         return;
                     }
-                    poll();
                     break;
                 case INTENT_BUGREPORT_FINISHED:
                     if (id == 0) {
@@ -367,15 +363,6 @@
             return;
 
         }
-
-        private void poll() {
-            if (pollProgress()) {
-                // Keep polling...
-                sendEmptyMessageDelayed(MSG_POLL, POLLING_FREQUENCY);
-            } else {
-                Log.i(TAG, "Stopped polling");
-            }
-        }
     }
 
     /**
@@ -397,11 +384,12 @@
     }
 
     private BugreportInfo getInfo(int id) {
-        final BugreportInfo info = mProcesses.get(id);
-        if (info == null) {
+        final DumpstateListener listener = mProcesses.get(id);
+        if (listener == null) {
             Log.w(TAG, "Not monitoring process with ID " + id);
+            return null;
         }
-        return info;
+        return listener.info;
     }
 
     /**
@@ -433,9 +421,15 @@
             Log.w(TAG, "ID " + id + " already watched");
             return true;
         }
-        mProcesses.put(info.id, info);
-        updateProgress(info);
-        return true;
+        final DumpstateListener listener = new DumpstateListener(info);
+        mProcesses.put(info.id, listener);
+        if (listener.connect()) {
+            updateProgress(info);
+            return true;
+        } else {
+            Log.w(TAG, "not updating progress because it could not connect to dumpstate");
+            return false;
+        }
     }
 
     /**
@@ -504,10 +498,7 @@
                 .setActions(infoAction, screenshotAction, cancelAction);
         }
 
-        if (DEBUG) {
-            Log.d(TAG, "Sending 'Progress' notification for id " + info.id + " (pid " + info.pid
-                    + "): " + percentageText);
-        }
+        Log.d(TAG, "Sending 'Progress' notification for id " + info.id + ": " + percentageText);
         sendForegroundabledNotification(info.id, builder.build());
     }
 
@@ -567,96 +558,11 @@
     }
 
     /**
-     * Poll {@link SystemProperties} to get the progress on each monitored process.
-     *
-     * @return whether it should keep polling.
-     */
-    private boolean pollProgress() {
-        final int total = mProcesses.size();
-        if (total == 0) {
-            Log.d(TAG, "No process to poll progress.");
-        }
-        int activeProcesses = 0;
-        for (int i = 0; i < total; i++) {
-            final BugreportInfo info = mProcesses.valueAt(i);
-            if (info == null) {
-                Log.wtf(TAG, "pollProgress(): null info at index " + i + "(ID = "
-                        + mProcesses.keyAt(i) + ")");
-                continue;
-            }
-
-            final int pid = info.pid;
-            final int id = info.id;
-            if (info.finished) {
-                if (DEBUG) Log.v(TAG, "Skipping finished process " + pid + " (id: " + id + ")");
-                continue;
-            }
-            activeProcesses++;
-            final String progressKey = DUMPSTATE_PREFIX + pid + PROGRESS_SUFFIX;
-            info.realProgress = SystemProperties.getInt(progressKey, 0);
-            if (info.realProgress == 0) {
-                Log.v(TAG, "System property " + progressKey + " is not set yet");
-            }
-            final String maxKey = DUMPSTATE_PREFIX + pid + MAX_SUFFIX;
-            info.realMax = SystemProperties.getInt(maxKey, info.max);
-            if (info.realMax <= 0 ) {
-                Log.w(TAG, "Property " + maxKey + " is not positive: " + info.max);
-                continue;
-            }
-            /*
-             * Checks whether the progress changed in a way that should be displayed to the user:
-             * - info.progress / info.max represents the displayed progress
-             * - info.realProgress / info.realMax represents the real progress
-             * - since the real progress can decrease, the displayed progress is only updated if it
-             *   increases
-             * - the displayed progress is capped at a maximum (like 99%)
-             */
-            final int oldPercentage = (CAPPED_MAX * info.progress) / info.max;
-            int newPercentage = (CAPPED_MAX * info.realProgress) / info.realMax;
-            int max = info.realMax;
-            int progress = info.realProgress;
-
-            if (newPercentage > CAPPED_PROGRESS) {
-                progress = newPercentage = CAPPED_PROGRESS;
-                max = CAPPED_MAX;
-            }
-
-            if (newPercentage > oldPercentage) {
-                if (DEBUG) {
-                    if (progress != info.progress) {
-                        Log.v(TAG, "Updating progress for PID " + pid + "(id: " + id + ") from "
-                                + info.progress + " to " + progress);
-                    }
-                    if (max != info.max) {
-                        Log.v(TAG, "Updating max progress for PID " + pid + "(id: " + id + ") from "
-                                + info.max + " to " + max);
-                    }
-                }
-                info.progress = progress;
-                info.max = max;
-                info.lastUpdate = System.currentTimeMillis();
-                updateProgress(info);
-            } else {
-                long inactiveTime = System.currentTimeMillis() - info.lastUpdate;
-                if (inactiveTime >= INACTIVITY_TIMEOUT) {
-                    Log.w(TAG, "No progress update for PID " + pid + " since "
-                            + info.getFormattedLastUpdate());
-                    stopProgress(info.id);
-                }
-            }
-        }
-        if (DEBUG) Log.v(TAG, "pollProgress() total=" + total + ", actives=" + activeProcesses);
-        return activeProcesses > 0;
-    }
-
-    /**
      * Fetches a {@link BugreportInfo} for a given process and launches a dialog where the user can
      * change its values.
      */
     private void launchBugreportInfoDialog(int id) {
         MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_DETAILS);
-        // Copy values so it doesn't lock mProcesses while UI is being updated
-        final String name, title, description;
         final BugreportInfo info = getInfo(id);
         if (info == null) {
             // Most likely am killed Shell before user tapped the notification. Since system might
@@ -737,7 +643,7 @@
         synchronized (BugreportProgressService.this) {
             mTakingScreenshot = flag;
             for (int i = 0; i < mProcesses.size(); i++) {
-                final BugreportInfo info = mProcesses.valueAt(i);
+                final BugreportInfo info = mProcesses.valueAt(i).info;
                 if (info.finished) {
                     Log.d(TAG, "Not updating progress for " + info.id + " while taking screenshot"
                             + " because share notification was already sent");
@@ -809,7 +715,7 @@
         final int total = mProcesses.size();
         if (total > 0) {
             for (int i = 0; i < total; i++) {
-                final BugreportInfo info = mProcesses.valueAt(i);
+                final BugreportInfo info = mProcesses.valueAt(i).info;
                 if (!info.finished) {
                     updateProgress(info);
                     break;
@@ -848,13 +754,13 @@
             Log.wtf(TAG, "Missing " + EXTRA_BUGREPORT + " on intent " + intent);
             return;
         }
-        mInfoDialog.onBugreportFinished(id);
+        mInfoDialog.onBugreportFinished();
         BugreportInfo info = getInfo(id);
         if (info == null) {
             // Happens when BUGREPORT_FINISHED was received without a BUGREPORT_STARTED first.
             Log.v(TAG, "Creating info for untracked ID " + id);
             info = new BugreportInfo(mContext, id);
-            mProcesses.put(id, info);
+            mProcesses.put(id, new DumpstateListener(info));
         }
         info.renameScreenshots(mScreenshotsDir);
         info.bugreportFile = bugreportFile;
@@ -1363,7 +1269,6 @@
         if (bitmap == null) {
             return false;
         }
-        boolean status;
         try (final FileOutputStream fos = new FileOutputStream(path)) {
             if (bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)) {
                 ((Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE)).vibrate(150);
@@ -1570,7 +1475,7 @@
          * <p>Once the bugreport is finished dumpstate has already generated the final files, so
          * changing the name would have no effect.
          */
-        void onBugreportFinished(int id) {
+        void onBugreportFinished() {
             if (mInfoName != null) {
                 mInfoName.setEnabled(false);
                 mInfoName.setText(mSavedName);
@@ -1684,7 +1589,7 @@
             this.id = id;
             this.pid = pid;
             this.name = name;
-            this.max = max;
+            this.max = this.realMax = max;
         }
 
         /**
@@ -1750,14 +1655,17 @@
         public String toString() {
             final float percent = ((float) progress * 100 / max);
             final float realPercent = ((float) realProgress * 100 / realMax);
-            return "id: " + id + ", pid: " + pid + ", name: " + name + ", finished: " + finished
-                    + "\n\ttitle: " + title + "\n\tdescription: " + description
-                    + "\n\tfile: " + bugreportFile + "\n\tscreenshots: " + screenshotFiles
+            return "\tid: " + id + ", pid: " + pid + ", name: " + name + ", finished: " + finished
+                    + "\n\ttitle: " + title
+                    + "\n\tdescription: " + description
+                    + "\n\tfile: " + bugreportFile
+                    + "\n\tscreenshots: " + screenshotFiles
                     + "\n\tprogress: " + progress + "/" + max + " (" + percent + ")"
-                    + "\n\treal progress: " + realProgress + "/" + realMax + " (" + realPercent + ")"
+                    + "\n\treal progress: " + realProgress + "/" + realMax + " (" + realPercent
+                    + ")"
                     + "\n\tlast_update: " + getFormattedLastUpdate()
-                    + "\naddingDetailsToZip: " + addingDetailsToZip
-                    + " addedDetailsToZip: " + addedDetailsToZip;
+                    + "\n\taddingDetailsToZip: " + addingDetailsToZip + " addedDetailsToZip: "
+                    + addedDetailsToZip;
         }
 
         // Parcelable contract
@@ -1823,16 +1731,118 @@
             return path == null ? null : new File(path);
         }
 
+        @SuppressWarnings("unused")
         public static final Parcelable.Creator<BugreportInfo> CREATOR =
                 new Parcelable.Creator<BugreportInfo>() {
+            @Override
             public BugreportInfo createFromParcel(Parcel source) {
                 return new BugreportInfo(source);
             }
 
+            @Override
             public BugreportInfo[] newArray(int size) {
                 return new BugreportInfo[size];
             }
         };
 
     }
+
+    private final class DumpstateListener extends IDumpstateListener.Stub
+        implements DeathRecipient {
+
+        private final BugreportInfo info;
+        private IDumpstateToken token;
+
+        DumpstateListener(BugreportInfo info) {
+            this.info = info;
+        }
+
+        /**
+         * Connects to the {@code dumpstate} binder to receive updates.
+         */
+        boolean connect() {
+            if (token != null) {
+                Log.d(TAG, "connect(): " + info.id + " already connected");
+                return true;
+            }
+            final IBinder service = ServiceManager.getService("dumpstate");
+            if (service == null) {
+                Log.d(TAG, "dumpstate service not bound yet");
+                return true;
+            }
+            final IDumpstate dumpstate = IDumpstate.Stub.asInterface(service);
+            try {
+                token = dumpstate.setListener("Shell", this);
+                if (token != null) {
+                    token.asBinder().linkToDeath(this, 0);
+                }
+            } catch (Exception e) {
+                Log.e(TAG, "Could not set dumpstate listener: " + e);
+            }
+            return token != null;
+        }
+
+        @Override
+        public void binderDied() {
+            if (!info.finished) {
+                // TODO: linkToDeath() might be called BEFORE Shell received the
+                // BUGREPORT_FINISHED broadcast, in which case the statements below
+                // spam logcat (but are harmless).
+                // The right, long-term solution is to provide an onFinished() callback
+                // on IDumpstateListener and call it instead of using a broadcast.
+                Log.w(TAG, "Dumpstate process died:\n" + info);
+                stopProgress(info.id);
+            }
+            token.asBinder().unlinkToDeath(this, 0);
+        }
+
+        @Override
+        public void onProgressUpdated(int progress) throws RemoteException {
+            /*
+             * Checks whether the progress changed in a way that should be displayed to the user:
+             * - info.progress / info.max represents the displayed progress
+             * - info.realProgress / info.realMax represents the real progress
+             * - since the real progress can decrease, the displayed progress is only updated if it
+             *   increases
+             * - the displayed progress is capped at a maximum (like 99%)
+             */
+            info.realProgress = progress;
+            final int oldPercentage = (CAPPED_MAX * info.progress) / info.max;
+            int newPercentage = (CAPPED_MAX * info.realProgress) / info.realMax;
+            int max = info.realMax;
+
+            if (newPercentage > CAPPED_PROGRESS) {
+                progress = newPercentage = CAPPED_PROGRESS;
+                max = CAPPED_MAX;
+            }
+
+            if (newPercentage > oldPercentage) {
+                if (DEBUG) {
+                    if (progress != info.progress) {
+                        Log.v(TAG, "Updating progress for PID " + info.pid + "(id: " + info.id
+                                + ") from " + info.progress + " to " + progress);
+                    }
+                    if (max != info.max) {
+                        Log.v(TAG, "Updating max progress for PID " + info.pid + "(id: " + info.id
+                                + ") from " + info.max + " to " + max);
+                    }
+                }
+                info.progress = progress;
+                info.max = max;
+                info.lastUpdate = System.currentTimeMillis();
+
+                updateProgress(info);
+            }
+        }
+
+        @Override
+        public void onMaxProgressUpdated(int maxProgress) throws RemoteException {
+            Log.d(TAG, "onMaxProgressUpdated: " + maxProgress);
+            info.realMax = maxProgress;
+        }
+
+        public void dump(String prefix, PrintWriter pw) {
+            pw.print(prefix); pw.print("token: "); pw.println(token);
+        }
+    }
 }
diff --git a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java
index b4bfb01..4e3744a 100644
--- a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java
+++ b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java
@@ -33,7 +33,6 @@
 import static com.android.shell.BugreportProgressService.EXTRA_SCREENSHOT;
 import static com.android.shell.BugreportProgressService.INTENT_BUGREPORT_FINISHED;
 import static com.android.shell.BugreportProgressService.INTENT_BUGREPORT_STARTED;
-import static com.android.shell.BugreportProgressService.POLLING_FREQUENCY;
 import static com.android.shell.BugreportProgressService.SCREENSHOT_DELAY_SECONDS;
 
 import static org.junit.Assert.assertEquals;
@@ -65,6 +64,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TestName;
@@ -116,7 +116,7 @@
     private static final String TAG = "BugreportReceiverTest";
 
     // Timeout for UI operations, in milliseconds.
-    private static final int TIMEOUT = (int) POLLING_FREQUENCY * 4;
+    private static final int TIMEOUT = (int) (5 * DateUtils.SECOND_IN_MILLIS);
 
     // Timeout for when waiting for a screenshot to finish.
     private static final int SAFE_SCREENSHOT_DELAY = SCREENSHOT_DELAY_SECONDS + 10;
@@ -209,6 +209,12 @@
         }
     }
 
+    /*
+     * TODO: this test is incomplete because:
+     * - the assertProgressNotification() is not really asserting the progress because the
+     *   UI automation API doesn't provide a way to check the notification progress bar value
+     * - it should use the binder object instead of SystemProperties to update progress
+     */
     @Test
     public void testProgress() throws Exception {
         resetProperties();
@@ -227,7 +233,6 @@
 
         // Make sure progress never goes back...
         SystemProperties.set(MAX_PROPERTY, "2000");
-        sleep(POLLING_FREQUENCY + DateUtils.SECOND_IN_MILLIS);
         assertProgressNotification(NAME, 95.00f);
 
         SystemProperties.set(PROGRESS_PROPERTY, "1000");
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index e07d865..0b5383a 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -171,6 +171,8 @@
     <!-- shortcut manager -->
     <uses-permission android:name="android.permission.RESET_SHORTCUT_MANAGER_THROTTLING" />
 
+    <uses-permission android:name="android.permission.MODIFY_THEME_OVERLAY" />
+
     <application
         android:name=".SystemUIApplication"
         android:persistent="true"
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/FragmentBase.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/FragmentBase.java
new file mode 100644
index 0000000..af55e8b
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/FragmentBase.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.plugins;
+
+import android.content.Context;
+import android.view.View;
+
+/**
+ * Interface to deal with lack of multiple inheritance
+ *
+ * This interface is designed to be used as a base class for plugin interfaces
+ * that need fragment methods. Plugins should not extend Fragment directly, so
+ * plugins that are fragments should be extending PluginFragment, but in SysUI
+ * these same versions should extend Fragment directly.
+ *
+ * Only methods that are on Fragment should be included here.
+ */
+public interface FragmentBase {
+    View getView();
+    Context getContext();
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginFragment.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginFragment.java
new file mode 100644
index 0000000..a9d1fa9
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginFragment.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.plugins;
+
+import android.annotation.Nullable;
+import android.app.Fragment;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+
+public abstract class PluginFragment extends Fragment implements Plugin {
+
+    private static final String KEY_PLUGIN_PACKAGE = "plugin_package_name";
+    private Context mPluginContext;
+
+    @Override
+    public void onCreate(Context sysuiContext, Context pluginContext) {
+        mPluginContext = pluginContext;
+    }
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        if (savedInstanceState != null) {
+            Context sysuiContext = getContext();
+            Context pluginContext = recreatePluginContext(sysuiContext, savedInstanceState);
+            onCreate(sysuiContext, pluginContext);
+        }
+        if (mPluginContext == null) {
+            throw new RuntimeException("PluginFragments must call super.onCreate("
+                    + "Context sysuiContext, Context pluginContext)");
+        }
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putString(KEY_PLUGIN_PACKAGE, getContext().getPackageName());
+    }
+
+    private Context recreatePluginContext(Context sysuiContext, Bundle savedInstanceState) {
+        final String pkg = savedInstanceState.getString(KEY_PLUGIN_PACKAGE);
+        try {
+            ApplicationInfo appInfo = sysuiContext.getPackageManager().getApplicationInfo(pkg, 0);
+            return PluginManager.getInstance(sysuiContext).getContext(appInfo, pkg);
+        } catch (NameNotFoundException e) {
+            throw new RuntimeException("Plugin with invalid package? " + pkg, e);
+        }
+    }
+
+    @Override
+    public LayoutInflater getLayoutInflater(Bundle savedInstanceState) {
+        return super.getLayoutInflater(savedInstanceState).cloneInContext(mPluginContext);
+    }
+
+    /**
+     * Should only be called after {@link Plugin#onCreate(Context, Context)}.
+     */
+    @Override
+    public Context getContext() {
+        return mPluginContext != null ? mPluginContext : super.getContext();
+    }
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java
index c64b188..62d3ce4 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java
@@ -158,7 +158,11 @@
                 case PLUGIN_DISCONNECTED:
                     if (DEBUG) Log.d(TAG, "onPluginDisconnected");
                     mListener.onPluginDisconnected((T) msg.obj);
-                    ((T) msg.obj).onDestroy();
+                    if (!(msg.obj instanceof PluginFragment)) {
+                        // Only call onDestroy for plugins that aren't fragments, as fragments
+                        // will get the onDestroy as part of the fragment lifecycle.
+                        ((T) msg.obj).onDestroy();
+                    }
                     break;
                 default:
                     super.handleMessage(msg);
@@ -186,7 +190,11 @@
                     for (int i = mPlugins.size() - 1; i >= 0; i--) {
                         PluginInfo<T> plugin = mPlugins.get(i);
                         mListener.onPluginDisconnected(plugin.mPlugin);
-                        plugin.mPlugin.onDestroy();
+                        if (!(plugin.mPlugin instanceof PluginFragment)) {
+                            // Only call onDestroy for plugins that aren't fragments, as fragments
+                            // will get the onDestroy as part of the fragment lifecycle.
+                            plugin.mPlugin.onDestroy();
+                        }
                     }
                     mPlugins.clear();
                     handleQueryPlugins(null);
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java
index 85f2e2a..60cf312 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java
@@ -18,6 +18,8 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
 import android.net.Uri;
 import android.os.Build;
 import android.os.HandlerThread;
@@ -26,6 +28,7 @@
 import android.util.ArrayMap;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.plugins.PluginInstanceManager.PluginContextWrapper;
 
 import dalvik.system.PathClassLoader;
 
@@ -163,6 +166,16 @@
         return mParentClassLoader;
     }
 
+    public Context getAllPluginContext(Context context) {
+        return new PluginContextWrapper(context,
+                new AllPluginClassLoader(context.getClassLoader()));
+    }
+
+    public Context getContext(ApplicationInfo info, String pkg) throws NameNotFoundException {
+        ClassLoader classLoader = getClassLoader(info.sourceDir, pkg);
+        return new PluginContextWrapper(mContext.createApplicationContext(info, 0), classLoader);
+    }
+
     public static PluginManager getInstance(Context context) {
         if (sInstance == null) {
             sInstance = new PluginManager(context.getApplicationContext());
@@ -170,6 +183,28 @@
         return sInstance;
     }
 
+    private class AllPluginClassLoader extends ClassLoader {
+        public AllPluginClassLoader(ClassLoader classLoader) {
+            super(classLoader);
+        }
+
+        @Override
+        public Class<?> loadClass(String s) throws ClassNotFoundException {
+            try {
+                return super.loadClass(s);
+            } catch (ClassNotFoundException e) {
+                for (ClassLoader classLoader : mClassLoaders.values()) {
+                    try {
+                        return classLoader.loadClass(s);
+                    } catch (ClassNotFoundException e1) {
+                        // Will re-throw e if all fail.
+                    }
+                }
+                throw e;
+            }
+        }
+    }
+
     @VisibleForTesting
     public static class PluginInstanceManagerFactory {
         public <T extends Plugin> PluginInstanceManager createPluginInstanceManager(Context context,
@@ -180,7 +215,6 @@
         }
     }
 
-
     // This allows plugins to include any libraries or copied code they want by only including
     // classes from the plugin library.
     private static class ClassLoaderFilter extends ClassLoader {
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSContainer.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java
similarity index 93%
rename from packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSContainer.java
rename to packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java
index 4947863..a9874fc 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSContainer.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java
@@ -25,17 +25,21 @@
 import android.widget.FrameLayout;
 import android.widget.RelativeLayout;
 
-public abstract class QSContainer extends FrameLayout {
+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.
+ */
+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 = 3;
+    public static final int VERSION = 4;
 
-    public QSContainer(@NonNull Context context, @Nullable AttributeSet attrs) {
-        super(context, attrs);
-    }
+    String TAG = "QS";
 
     public abstract void setPanelView(HeightListener notificationPanelView);
     public abstract BaseStatusBarHeader getHeader();
diff --git a/packages/SystemUI/res/layout/qs_customize_panel.xml b/packages/SystemUI/res/layout/qs_customize_panel.xml
index 7af247e..9ab8ac6 100644
--- a/packages/SystemUI/res/layout/qs_customize_panel.xml
+++ b/packages/SystemUI/res/layout/qs_customize_panel.xml
@@ -15,7 +15,7 @@
      limitations under the License.
 -->
 
-<!-- Height is 0 because it will be managed by the QSContainer manually -->
+<!-- Height is 0 because it will be managed by the QS manually -->
 <com.android.systemui.qs.customize.QSCustomizer
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
diff --git a/packages/SystemUI/res/layout/status_bar_expanded.xml b/packages/SystemUI/res/layout/status_bar_expanded.xml
index 0339e03..f09657f 100644
--- a/packages/SystemUI/res/layout/status_bar_expanded.xml
+++ b/packages/SystemUI/res/layout/status_bar_expanded.xml
@@ -39,15 +39,15 @@
         android:clipToPadding="false"
         android:clipChildren="false">
 
-        <com.android.systemui.PluginInflateContainer
-            android:id="@+id/qs_auto_reinflate_container"
+        <FrameLayout
+            android:id="@+id/qs_frame"
             android:layout="@layout/qs_panel"
             android:layout_width="@dimen/notification_panel_width"
             android:layout_height="match_parent"
             android:layout_gravity="@integer/notification_panel_layout_gravity"
             android:clipToPadding="false"
             android:clipChildren="false"
-            systemui:viewType="com.android.systemui.plugins.qs.QSContainer" />
+            systemui:viewType="com.android.systemui.plugins.qs.QS" />
 
         <com.android.systemui.statusbar.stack.NotificationStackScrollLayout
             android:id="@+id/notification_stack_scroller"
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 9eea375..0f5d37e 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -48,6 +48,12 @@
     <!-- Whether or not we show the number in the bar. -->
     <bool name="config_statusBarShowNumber">false</bool>
 
+    <!-- Vibrator pattern for camera gesture launch. -->
+    <integer-array translatable="false" name="config_cameraLaunchGestureVibePattern">
+        <item>0</item>
+        <item>400</item>
+    </integer-array>
+
     <!-- How many icons may be shown at once in the system bar. Includes any
          slots that may be reused for things like IME control. -->
     <integer name="config_maxNotificationIcons">5</integer>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 37a7e38..331d09e 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1698,20 +1698,35 @@
         not appear on production builds ever. -->
     <string name="pip_drag_to_dismiss_summary" translatable="false">Drag to the dismiss target at the bottom of the screen to close the PIP</string>
 
-    <!-- PIP tap once to break through to the activity. Non-translatable since it should
+    <!-- PIP tap once to break through to the activity title. Non-translatable since it should
         not appear on production builds ever. -->
     <string name="pip_tap_through_title" translatable="false">Tap to interact</string>
 
-    <!-- PIP tap once to break through to the activity. Non-translatable since it should
+    <!-- PIP tap once to break through to the activity description. Non-translatable since it should
         not appear on production builds ever. -->
     <string name="pip_tap_through_summary" translatable="false">Tap once to interact with the activity</string>
 
-    <!-- PIP snap to closest edge. Non-translatable since it should
+    <!-- PIP snap to closest edge title. Non-translatable since it should
         not appear on production builds ever. -->
     <string name="pip_snap_mode_edge_title" translatable="false">Snap to closest edge</string>
 
-    <!-- PIP snap to closest edge. Non-translatable since it should
+    <!-- PIP snap to closest edge description. Non-translatable since it should
         not appear on production builds ever. -->
     <string name="pip_snap_mode_edge_summary" translatable="false">Snap to the closest edge</string>
 
+    <!-- PIP allow minimize title. Non-translatable since it should
+        not appear on production builds ever. -->
+    <string name="pip_allow_minimize_title" translatable="false">Allow PIP to minimize</string>
+
+    <!-- PIP allow minimize description. Non-translatable since it should
+        not appear on production builds ever. -->
+    <string name="pip_allow_minimize_summary" translatable="false">Allow PIP to minimize slightly offscreen</string>
+
+    <!-- Tuner string -->
+    <string name="change_theme_reboot" translatable="false">Changing the theme requires a restart.</string>
+    <!-- Tuner string -->
+    <string name="theme" translatable="false">Theme</string>
+    <!-- Tuner string -->
+    <string name="default_theme" translatable="false">Default</string>
+
 </resources>
diff --git a/packages/SystemUI/res/xml/other_settings.xml b/packages/SystemUI/res/xml/other_settings.xml
index ce636cd..18cb930 100644
--- a/packages/SystemUI/res/xml/other_settings.xml
+++ b/packages/SystemUI/res/xml/other_settings.xml
@@ -23,5 +23,10 @@
             android:key="power_notification_controls"
             android:title="@string/tuner_full_importance_settings"
             android:fragment="com.android.systemui.tuner.PowerNotificationControlsFragment"/>
+e
+    <com.android.systemui.tuner.ThemePreference
+        android:key="theme"
+        android:title="@string/theme"
+        android:summary="%s" />
 
-</PreferenceScreen>
\ No newline at end of file
+</PreferenceScreen>
diff --git a/packages/SystemUI/res/xml/tuner_prefs.xml b/packages/SystemUI/res/xml/tuner_prefs.xml
index f09d6e9..74d5d6c 100644
--- a/packages/SystemUI/res/xml/tuner_prefs.xml
+++ b/packages/SystemUI/res/xml/tuner_prefs.xml
@@ -149,6 +149,12 @@
             android:summary="@string/pip_snap_mode_edge_summary"
             sysui:defValue="false" />
 
+        <com.android.systemui.tuner.TunerSwitch
+            android:key="pip_allow_minimize"
+            android:title="@string/pip_allow_minimize_title"
+            android:summary="@string/pip_allow_minimize_summary"
+            sysui:defValue="false" />
+
     </PreferenceScreen>
 
     <PreferenceScreen
diff --git a/packages/SystemUI/src/com/android/systemui/BatteryMeterDrawable.java b/packages/SystemUI/src/com/android/systemui/BatteryMeterDrawable.java
index e1d6a94..c4698c3 100755
--- a/packages/SystemUI/src/com/android/systemui/BatteryMeterDrawable.java
+++ b/packages/SystemUI/src/com/android/systemui/BatteryMeterDrawable.java
@@ -32,8 +32,8 @@
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Handler;
-import android.os.Looper;
 import android.provider.Settings;
+
 import com.android.systemui.statusbar.policy.BatteryController;
 
 public class BatteryMeterDrawable extends Drawable implements
@@ -182,13 +182,13 @@
         mContext.getContentResolver().registerContentObserver(
                 Settings.System.getUriFor(SHOW_PERCENT_SETTING), false, mSettingObserver);
         updateShowPercent();
-        mBatteryController.addStateChangedCallback(this);
+        mBatteryController.addCallback(this);
     }
 
     public void stopListening() {
         mListening = false;
         mContext.getContentResolver().unregisterContentObserver(mSettingObserver);
-        mBatteryController.removeStateChangedCallback(this);
+        mBatteryController.removeCallback(this);
     }
 
     public void disableShowPercent() {
diff --git a/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java
index 4f3ffde..ef1c25d 100644
--- a/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java
+++ b/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java
@@ -17,7 +17,6 @@
 
 import android.content.Context;
 import android.content.res.TypedArray;
-import android.os.Handler;
 import android.util.ArraySet;
 import android.util.AttributeSet;
 import android.view.View;
@@ -73,7 +72,7 @@
     @Override
     public void onAttachedToWindow() {
         super.onAttachedToWindow();
-        mBatteryController.addStateChangedCallback(this);
+        mBatteryController.addCallback(this);
         mDrawable.startListening();
         TunerService.get(getContext()).addTunable(this, StatusBarIconController.ICON_BLACKLIST);
     }
@@ -81,7 +80,7 @@
     @Override
     public void onDetachedFromWindow() {
         super.onDetachedFromWindow();
-        mBatteryController.removeStateChangedCallback(this);
+        mBatteryController.removeCallback(this);
         mDrawable.stopListening();
         TunerService.get(getContext()).removeTunable(this);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
index 99e7876..6802fd7 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
@@ -29,9 +29,11 @@
 import android.os.UserHandle;
 import android.util.Log;
 
+import com.android.systemui.fragments.FragmentService;
 import com.android.systemui.keyboard.KeyboardUI;
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.media.RingtonePlayer;
+import com.android.systemui.pip.PipUI;
 import com.android.systemui.plugins.OverlayPlugin;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.plugins.PluginManager;
@@ -42,7 +44,6 @@
 import com.android.systemui.statusbar.SystemBars;
 import com.android.systemui.statusbar.phone.PhoneStatusBar;
 import com.android.systemui.tuner.TunerService;
-import com.android.systemui.pip.PipUI;
 import com.android.systemui.usb.StorageNotification;
 import com.android.systemui.volume.VolumeUI;
 
@@ -61,6 +62,7 @@
      * The classes of the stuff to start.
      */
     private final Class<?>[] SERVICES = new Class[] {
+            FragmentService.class,
             TunerService.class,
             KeyguardViewMediator.class,
             Recents.class,
diff --git a/packages/SystemUI/src/com/android/systemui/fragments/FragmentHostManager.java b/packages/SystemUI/src/com/android/systemui/fragments/FragmentHostManager.java
new file mode 100644
index 0000000..5f27b74
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/fragments/FragmentHostManager.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.fragments;
+
+import android.annotation.Nullable;
+import android.app.Fragment;
+import android.app.FragmentController;
+import android.app.FragmentHostCallback;
+import android.app.FragmentManager;
+import android.app.FragmentManager.FragmentLifecycleCallbacks;
+import android.app.FragmentManagerNonConfig;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Parcelable;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import com.android.settingslib.applications.InterestingConfigChanges;
+import com.android.systemui.SystemUIApplication;
+import com.android.systemui.plugins.PluginManager;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class FragmentHostManager {
+
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+    private final Context mContext;
+    private final HashMap<String, ArrayList<FragmentListener>> mListeners = new HashMap<>();
+    private final View mRootView;
+    private final InterestingConfigChanges mConfigChanges = new InterestingConfigChanges();
+    private final FragmentService mManager;
+
+    private FragmentController mFragments;
+    private FragmentLifecycleCallbacks mLifecycleCallbacks;
+
+    FragmentHostManager(Context context, FragmentService manager, View rootView) {
+        mContext = PluginManager.getInstance(context).getAllPluginContext(context);
+        mManager = manager;
+        mRootView = rootView;
+        mConfigChanges.applyNewConfig(context.getResources());
+        createFragmentHost(null);
+    }
+
+    private void createFragmentHost(Parcelable savedState) {
+        mFragments = FragmentController.createController(new HostCallbacks());
+        mFragments.attachHost(null);
+        // TODO: Remove non-staticness from FragmentLifecycleCallbacks (hopefully).
+        mLifecycleCallbacks = mFragments.getFragmentManager().new FragmentLifecycleCallbacks() {
+            @Override
+            public void onFragmentViewCreated(FragmentManager fm, Fragment f, View v,
+                    Bundle savedInstanceState) {
+                FragmentHostManager.this.onFragmentViewCreated(f);
+            }
+
+            @Override
+            public void onFragmentViewDestroyed(FragmentManager fm, Fragment f) {
+                FragmentHostManager.this.onFragmentViewDestroyed(f);
+            }
+        };
+        mFragments.getFragmentManager().registerFragmentLifecycleCallbacks(mLifecycleCallbacks,
+                true);
+        if (savedState != null) {
+            mFragments.restoreAllState(savedState, (FragmentManagerNonConfig) null);
+        }
+        // For now just keep all fragments in the resumed state.
+        mFragments.dispatchCreate();
+        mFragments.dispatchStart();
+        mFragments.dispatchResume();
+    }
+
+    private Parcelable destroyFragmentHost() {
+        mFragments.dispatchPause();
+        Parcelable p = mFragments.saveAllState();
+        mFragments.dispatchStop();
+        mFragments.dispatchDestroy();
+        mFragments.getFragmentManager().unregisterFragmentLifecycleCallbacks(mLifecycleCallbacks);
+        return p;
+    }
+
+    public void addTagListener(String tag, FragmentListener listener) {
+        ArrayList<FragmentListener> listeners = mListeners.get(tag);
+        if (listeners == null) {
+            listeners = new ArrayList<>();
+            mListeners.put(tag, listeners);
+        }
+        listeners.add(listener);
+        Fragment current = getFragmentManager().findFragmentByTag(tag);
+        if (current != null && current.getView() != null) {
+            listener.onFragmentViewCreated(tag, current);
+        }
+    }
+
+    // Shouldn't generally be needed, included for completeness sake.
+    public void removeTagListener(String tag, FragmentListener listener) {
+        ArrayList<FragmentListener> listeners = mListeners.get(tag);
+        if (listeners != null && listeners.remove(listener) && listeners.size() == 0) {
+            mListeners.remove(tag);
+        }
+    }
+
+    private void onFragmentViewCreated(Fragment fragment) {
+        String tag = fragment.getTag();
+
+        ArrayList<FragmentListener> listeners = mListeners.get(tag);
+        if (listeners != null) {
+            listeners.forEach((listener) -> listener.onFragmentViewCreated(tag, fragment));
+        }
+    }
+
+    private void onFragmentViewDestroyed(Fragment fragment) {
+        String tag = fragment.getTag();
+
+        ArrayList<FragmentListener> listeners = mListeners.get(tag);
+        if (listeners != null) {
+            listeners.forEach((listener) -> listener.onFragmentViewDestroyed(tag, fragment));
+        }
+    }
+
+    /**
+     * Called when the configuration changed, return true if the fragments
+     * should be recreated.
+     */
+    protected void onConfigurationChanged(Configuration newConfig) {
+        if (mConfigChanges.applyNewConfig(mContext.getResources())) {
+            // Save the old state.
+            Parcelable p = destroyFragmentHost();
+            // Generate a new fragment host and restore its state.
+            createFragmentHost(p);
+        } else {
+            mFragments.dispatchConfigurationChanged(newConfig);
+        }
+    }
+
+    private void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+        // TODO: Do something?
+    }
+
+    private View findViewById(int id) {
+        return mRootView.findViewById(id);
+    }
+
+    /**
+     * Note: Values from this shouldn't be cached as they can change after config changes.
+     */
+    public FragmentManager getFragmentManager() {
+        return mFragments.getFragmentManager();
+    }
+
+    public interface FragmentListener {
+        void onFragmentViewCreated(String tag, Fragment fragment);
+
+        // The facts of lifecycle
+        // When a fragment is destroyed, you should not talk to it any longer.
+        default void onFragmentViewDestroyed(String tag, Fragment fragment) {
+        }
+    }
+
+    public static FragmentHostManager get(View view) {
+        try {
+            return ((SystemUIApplication) view.getContext().getApplicationContext())
+                    .getComponent(FragmentService.class).getFragmentHostManager(view);
+        } catch (ClassCastException e) {
+            // TODO: Some auto handling here?
+            throw e;
+        }
+    }
+
+    class HostCallbacks extends FragmentHostCallback<FragmentHostManager> {
+        public HostCallbacks() {
+            super(mContext, FragmentHostManager.this.mHandler, 0);
+        }
+
+        @Override
+        public FragmentHostManager onGetHost() {
+            return FragmentHostManager.this;
+        }
+
+        @Override
+        public void onDump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+            FragmentHostManager.this.dump(prefix, fd, writer, args);
+        }
+
+        @Override
+        public boolean onShouldSaveFragmentState(Fragment fragment) {
+            return true; // True for now.
+        }
+
+        @Override
+        public LayoutInflater onGetLayoutInflater() {
+            return LayoutInflater.from(mContext);
+        }
+
+        @Override
+        public boolean onUseFragmentManagerInflaterFactory() {
+            return true;
+        }
+
+        @Override
+        public boolean onHasWindowAnimations() {
+            return false;
+        }
+
+        @Override
+        public int onGetWindowAnimations() {
+            return 0;
+        }
+
+        @Override
+        public void onAttachFragment(Fragment fragment) {
+        }
+
+        @Override
+        @Nullable
+        public View onFindViewById(int id) {
+            return FragmentHostManager.this.findViewById(id);
+        }
+
+        @Override
+        public boolean onHasView() {
+            return true;
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/fragments/FragmentService.java b/packages/SystemUI/src/com/android/systemui/fragments/FragmentService.java
new file mode 100644
index 0000000..85cde10
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/fragments/FragmentService.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.fragments;
+
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.view.View;
+
+import com.android.systemui.SystemUI;
+import com.android.systemui.SystemUIApplication;
+
+/**
+ * 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 {
+
+    private static final String TAG = "FragmentService";
+
+    private final ArrayMap<View, FragmentHostState> mHosts = new ArrayMap<>();
+    private final Handler mHandler = new Handler();
+
+    @Override
+    public void start() {
+        putComponent(FragmentService.class, this);
+    }
+
+    public FragmentHostManager getFragmentHostManager(View view) {
+        View root = view.getRootView();
+        FragmentHostState state = mHosts.get(root);
+        if (state == null) {
+            state = new FragmentHostState(root);
+            mHosts.put(root, state);
+        }
+        return state.getFragmentHostManager();
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        for (FragmentHostState state : mHosts.values()) {
+            state.sendConfigurationChange(newConfig);
+        }
+    }
+
+    private class FragmentHostState {
+        private final View mView;
+
+        private FragmentHostManager mFragmentHostManager;
+
+        public FragmentHostState(View view) {
+            mView = view;
+            mFragmentHostManager = new FragmentHostManager(mContext, FragmentService.this, mView);
+        }
+
+        public void sendConfigurationChange(Configuration newConfig) {
+            mHandler.post(() -> handleSendConfigurationChange(newConfig));
+        }
+
+        public FragmentHostManager getFragmentHostManager() {
+            return mFragmentHostManager;
+        }
+
+        private void handleSendConfigurationChange(Configuration newConfig) {
+            mFragmentHostManager.onConfigurationChanged(newConfig);
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/fragments/PluginFragmentListener.java b/packages/SystemUI/src/com/android/systemui/fragments/PluginFragmentListener.java
new file mode 100644
index 0000000..e107fcd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/fragments/PluginFragmentListener.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.fragments;
+
+import android.app.Fragment;
+import android.util.Log;
+import android.view.View;
+
+import com.android.systemui.plugins.FragmentBase;
+import com.android.systemui.plugins.Plugin;
+import com.android.systemui.plugins.PluginListener;
+import com.android.systemui.plugins.PluginManager;
+
+public class PluginFragmentListener implements PluginListener<Plugin> {
+
+    private static final String TAG = "PluginFragmentListener";
+
+    private final FragmentHostManager mFragmentHostManager;
+    private final PluginManager mPluginManager;
+    private final Class<? extends Fragment> mDefaultClass;
+    private final int mId;
+    private final String mTag;
+    private final Class<? extends FragmentBase> mExpectedInterface;
+
+    public PluginFragmentListener(View view, String tag, int id,
+            Class<? extends Fragment> defaultFragment,
+            Class<? extends FragmentBase> expectedInterface) {
+        mFragmentHostManager = FragmentHostManager.get(view);
+        mPluginManager = PluginManager.getInstance(view.getContext());
+        mExpectedInterface = expectedInterface;
+        mTag = tag;
+        mDefaultClass = defaultFragment;
+        mId = id;
+    }
+
+    public void startListening(String action, int version) {
+        try {
+            setFragment(mDefaultClass.newInstance());
+        } catch (InstantiationException | IllegalAccessException e) {
+            Log.e(TAG, "Couldn't instantiate " + mDefaultClass.getName(), e);
+        }
+        mPluginManager.addPluginListener(action, this, version, false /* Only allow one */);
+    }
+
+    public void stopListening() {
+        mPluginManager.removePluginListener(this);
+    }
+
+    private void setFragment(Fragment fragment) {
+        mFragmentHostManager.getFragmentManager().beginTransaction()
+                .replace(mId, fragment, mTag)
+                .commit();
+    }
+
+    @Override
+    public void onPluginConnected(Plugin plugin) {
+        try {
+            mExpectedInterface.cast(plugin);
+            setFragment((Fragment) plugin);
+        } catch (ClassCastException e) {
+            Log.e(TAG, plugin.getClass().getName() + " must be a Fragment and implement "
+                    + mExpectedInterface.getName(), e);
+        }
+    }
+
+    @Override
+    public void onPluginDisconnected(Plugin plugin) {
+        try {
+            setFragment(mDefaultClass.newInstance());
+        } catch (InstantiationException | IllegalAccessException e) {
+            Log.e(TAG, "Couldn't instantiate " + mDefaultClass.getName(), e);
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchGesture.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchGesture.java
new file mode 100644
index 0000000..e8e8a4d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchGesture.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.pip.phone;
+
+/**
+ * A generic interface for a touch gesture.
+ */
+public abstract class PipTouchGesture {
+
+    /**
+     * Handle the touch down.
+     */
+    void onDown(PipTouchState touchState) {}
+
+    /**
+     * Handle the touch move, and return whether the event was consumed.
+     */
+    boolean onMove(PipTouchState touchState) {
+        return false;
+    }
+
+    /**
+     * Handle the touch up, and return whether the gesture was consumed.
+     */
+    boolean onUp(PipTouchState touchState) {
+        return false;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java
index a359380..c6dde46 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java
@@ -22,6 +22,7 @@
 
 import static com.android.systemui.Interpolators.FAST_OUT_LINEAR_IN;
 import static com.android.systemui.Interpolators.FAST_OUT_SLOW_IN;
+import static com.android.systemui.Interpolators.LINEAR_OUT_SLOW_IN;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -30,9 +31,9 @@
 import android.app.ActivityManager.StackInfo;
 import android.app.IActivityManager;
 import android.content.Context;
+import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
-import android.os.Handler;
 import android.os.Looper;
 import android.os.RemoteException;
 import android.util.Log;
@@ -43,7 +44,6 @@
 import android.view.InputEvent;
 import android.view.InputEventReceiver;
 import android.view.MotionEvent;
-import android.view.VelocityTracker;
 import android.view.ViewConfiguration;
 
 import com.android.internal.os.BackgroundThread;
@@ -64,10 +64,17 @@
     private static final String TUNER_KEY_DRAG_TO_DISMISS = "pip_drag_to_dismiss";
     private static final String TUNER_KEY_TAP_THROUGH = "pip_tap_through";
     private static final String TUNER_KEY_SNAP_MODE_EDGE = "pip_snap_mode_edge";
+    private static final String TUNER_KEY_ALLOW_MINIMIZE = "pip_allow_minimize";
 
     private static final int SNAP_STACK_DURATION = 225;
     private static final int DISMISS_STACK_DURATION = 375;
     private static final int EXPAND_STACK_DURATION = 225;
+    private static final int MINIMIZE_STACK_MAX_DURATION = 200;
+
+    // The fraction of the stack width that the user has to drag offscreen to minimize the PIP
+    private static final float MINIMIZE_OFFSCREEN_FRACTION = 0.15f;
+    // The fraction of the stack width that the user has to move when flinging to dismiss the PIP
+    private static final float DISMISS_FLING_DISTANCE_FRACTION = 0.3f;
 
     private final Context mContext;
     private final IActivityManager mActivityManager;
@@ -83,10 +90,16 @@
     private final PipSnapAlgorithm mSnapAlgorithm;
     private PipMotionHelper mMotionHelper;
 
+    // Allow swiping offscreen to dismiss the PIP
     private boolean mEnableSwipeToDismiss = true;
+    // Allow dragging the PIP to a location to close it
     private boolean mEnableDragToDismiss = true;
+    // Allow tapping on the PIP to show additional controls
     private boolean mEnableTapThrough = false;
+    // Allow snapping the PIP to the closest edge and not the corners of the screen
     private boolean mEnableSnapToEdge = false;
+    // Allow the PIP to be "docked" slightly offscreen
+    private boolean mEnableMinimizing = false;
 
     private final Rect mPinnedStackBounds = new Rect();
     private final Rect mBoundedPinnedStackBounds = new Rect();
@@ -99,16 +112,16 @@
         }
     };
 
-    private final PointF mDownTouch = new PointF();
-    private final PointF mLastTouch = new PointF();
-    private boolean mIsDragging;
-    private boolean mIsSwipingToDismiss;
+    // Behaviour states
     private boolean mIsTappingThrough;
-    private int mActivePointerId;
+    private boolean mIsMinimized;
 
+    // Touch state
+    private final PipTouchState mTouchState;
     private final FlingAnimationUtils mFlingAnimationUtils;
-    private VelocityTracker mVelocityTracker;
+    private final PipTouchGesture[] mGestures;
 
+    // Temporary vars
     private final Rect mTmpBounds = new Rect();
 
     /**
@@ -183,13 +196,19 @@
         mMenuController.addListener(mMenuListener);
         mDismissViewController = new PipDismissViewController(context);
         mSnapAlgorithm = new PipSnapAlgorithm(mContext);
+        mTouchState = new PipTouchState(mViewConfig);
         mFlingAnimationUtils = new FlingAnimationUtils(context, 2f);
+        mGestures = new PipTouchGesture[]{
+                mDragToDismissGesture, mSwipeToDismissGesture, mTapThroughGesture, mMinimizeGesture,
+                mDefaultMovementGesture
+        };
         mMotionHelper = new PipMotionHelper(BackgroundThread.getHandler());
         registerInputConsumer();
 
         // Register any tuner settings changes
         TunerService.get(context).addTunable(this, TUNER_KEY_SWIPE_TO_DISMISS,
-            TUNER_KEY_DRAG_TO_DISMISS, TUNER_KEY_TAP_THROUGH, TUNER_KEY_SNAP_MODE_EDGE);
+            TUNER_KEY_DRAG_TO_DISMISS, TUNER_KEY_TAP_THROUGH, TUNER_KEY_SNAP_MODE_EDGE,
+                TUNER_KEY_ALLOW_MINIMIZE);
     }
 
     @Override
@@ -198,6 +217,8 @@
             // Reset back to default
             mEnableSwipeToDismiss = true;
             mEnableDragToDismiss = true;
+            mEnableMinimizing = false;
+            setMinimizedState(false);
             mEnableTapThrough = false;
             mIsTappingThrough = false;
             mEnableSnapToEdge = false;
@@ -211,6 +232,9 @@
             case TUNER_KEY_DRAG_TO_DISMISS:
                 mEnableDragToDismiss = Integer.parseInt(newValue) != 0;
                 break;
+            case TUNER_KEY_ALLOW_MINIMIZE:
+                mEnableMinimizing = Integer.parseInt(newValue) != 0;
+                break;
             case TUNER_KEY_TAP_THROUGH:
                 mEnableTapThrough = Integer.parseInt(newValue) != 0;
                 mIsTappingThrough = false;
@@ -233,6 +257,9 @@
             return true;
         }
 
+        // Update the touch state
+        mTouchState.onTouchEvent(ev);
+
         switch (ev.getAction()) {
             case MotionEvent.ACTION_DOWN: {
                 // Cancel any existing animations on the pinned stack
@@ -241,173 +268,58 @@
                 }
 
                 updateBoundedPinnedStackBounds(true /* updatePinnedStackBounds */);
-                initOrResetVelocityTracker();
-                mVelocityTracker.addMovement(ev);
-                mActivePointerId = ev.getPointerId(0);
-                mLastTouch.set(ev.getX(), ev.getY());
-                mDownTouch.set(mLastTouch);
-                mIsDragging = false;
+                for (PipTouchGesture gesture : mGestures) {
+                    gesture.onDown(mTouchState);
+                }
                 try {
                     mPinnedStackController.setInInteractiveMode(true);
                 } catch (RemoteException e) {
                     Log.e(TAG, "Could not set dragging state", e);
                 }
-                if (mEnableDragToDismiss) {
-                    // TODO: Consider setting a timer such at after X time, we show the dismiss
-                    //       target if the user hasn't already dragged some distance
-                    mDismissViewController.createDismissTarget();
-                }
                 break;
             }
             case MotionEvent.ACTION_MOVE: {
-                // Update the velocity tracker
-                mVelocityTracker.addMovement(ev);
-
-                int activePointerIndex = ev.findPointerIndex(mActivePointerId);
-                float x = ev.getX(activePointerIndex);
-                float y = ev.getY(activePointerIndex);
-                float left = mPinnedStackBounds.left + (x - mLastTouch.x);
-                float top = mPinnedStackBounds.top + (y - mLastTouch.y);
-
-                if (!mIsDragging) {
-                    // Check if the pointer has moved far enough
-                    float movement = PointF.length(mDownTouch.x - x, mDownTouch.y - y);
-                    if (movement > mViewConfig.getScaledTouchSlop()) {
-                        mIsDragging = true;
-                        mIsTappingThrough = false;
-                        mMenuController.hideMenu();
-                        if (mEnableSwipeToDismiss) {
-                            // TODO: this check can have some buffer so that we only start swiping
-                            //       after a significant move out of bounds
-                            mIsSwipingToDismiss = !(mBoundedPinnedStackBounds.left <= left &&
-                                    left <= mBoundedPinnedStackBounds.right) &&
-                                    Math.abs(mDownTouch.x - x) > Math.abs(y - mLastTouch.y);
-                        }
-                        if (mEnableDragToDismiss) {
-                            mDismissViewController.showDismissTarget();
-                        }
+                for (PipTouchGesture gesture : mGestures) {
+                    if (gesture.onMove(mTouchState)) {
+                        break;
                     }
                 }
-
-                if (mIsSwipingToDismiss) {
-                    // Ignore the vertical movement
-                    mTmpBounds.set(mPinnedStackBounds);
-                    mTmpBounds.offsetTo((int) left, mPinnedStackBounds.top);
-                    if (!mTmpBounds.equals(mPinnedStackBounds)) {
-                        mPinnedStackBounds.set(mTmpBounds);
-                        mMotionHelper.resizeToBounds(mPinnedStackBounds);
-                    }
-                } else if (mIsDragging) {
-                    // Move the pinned stack
-                    if (!DEBUG_ALLOW_OUT_OF_BOUNDS_STACK) {
-                        left = Math.max(mBoundedPinnedStackBounds.left, Math.min(
-                                mBoundedPinnedStackBounds.right, left));
-                        top = Math.max(mBoundedPinnedStackBounds.top, Math.min(
-                                mBoundedPinnedStackBounds.bottom, top));
-                    }
-                    mTmpBounds.set(mPinnedStackBounds);
-                    mTmpBounds.offsetTo((int) left, (int) top);
-                    if (!mTmpBounds.equals(mPinnedStackBounds)) {
-                        mPinnedStackBounds.set(mTmpBounds);
-                        mMotionHelper.resizeToBounds(mPinnedStackBounds);
-                    }
-                }
-                mLastTouch.set(ev.getX(), ev.getY());
-                break;
-            }
-            case MotionEvent.ACTION_POINTER_UP: {
-                // Update the velocity tracker
-                mVelocityTracker.addMovement(ev);
-
-                int pointerIndex = ev.getActionIndex();
-                int pointerId = ev.getPointerId(pointerIndex);
-                if (pointerId == mActivePointerId) {
-                    // Select a new active pointer id and reset the movement state
-                    final int newPointerIndex = (pointerIndex == 0) ? 1 : 0;
-                    mActivePointerId = ev.getPointerId(newPointerIndex);
-                    mLastTouch.set(ev.getX(newPointerIndex), ev.getY(newPointerIndex));
-                }
                 break;
             }
             case MotionEvent.ACTION_UP: {
-                // Update the velocity tracker
-                mVelocityTracker.addMovement(ev);
-                mVelocityTracker.computeCurrentVelocity(1000,
-                    ViewConfiguration.get(mContext).getScaledMaximumFlingVelocity());
-                float velocityX = mVelocityTracker.getXVelocity();
-                float velocityY = mVelocityTracker.getYVelocity();
-                float velocity = PointF.length(velocityX, velocityY);
-
                 // Update the movement bounds again if the state has changed since the user started
                 // dragging (ie. when the IME shows)
                 updateBoundedPinnedStackBounds(false /* updatePinnedStackBounds */);
 
-                if (mIsSwipingToDismiss) {
-                    if (Math.abs(velocityX) > mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
-                        flingToDismiss(velocityX);
-                    } else {
-                        animateToClosestSnapTarget();
+                for (PipTouchGesture gesture : mGestures) {
+                    if (gesture.onUp(mTouchState)) {
+                        break;
                     }
-                } else if (mIsDragging) {
-                    if (velocity > mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
-                        flingToSnapTarget(velocity, velocityX, velocityY);
-                    } else {
-                        int activePointerIndex = ev.findPointerIndex(mActivePointerId);
-                        int x = (int) ev.getX(activePointerIndex);
-                        int y = (int) ev.getY(activePointerIndex);
-                        Rect dismissBounds = mEnableDragToDismiss
-                                ? mDismissViewController.getDismissBounds()
-                                : null;
-                        if (dismissBounds != null && dismissBounds.contains(x, y)) {
-                            animateDismissPinnedStack(dismissBounds);
-                        } else {
-                            animateToClosestSnapTarget();
-                        }
-                    }
-                } else {
-                    if (mEnableTapThrough) {
-                        if (!mIsTappingThrough) {
-                            mMenuController.showMenu();
-                            mIsTappingThrough = true;
-                        }
-                    } else {
-                        expandPinnedStackToFullscreen();
-                    }
-                }
-                if (mEnableDragToDismiss) {
-                    mDismissViewController.destroyDismissTarget();
                 }
 
                 // Fall through to clean up
             }
             case MotionEvent.ACTION_CANCEL: {
-                mIsDragging = false;
-                mIsSwipingToDismiss = false;
                 try {
                     mPinnedStackController.setInInteractiveMode(false);
                 } catch (RemoteException e) {
                     Log.e(TAG, "Could not set dragging state", e);
                 }
-                recycleVelocityTracker();
                 break;
             }
         }
         return !mIsTappingThrough;
     }
 
-    private void initOrResetVelocityTracker() {
-        if (mVelocityTracker == null) {
-            mVelocityTracker = VelocityTracker.obtain();
-        } else {
-            mVelocityTracker.clear();
-        }
-    }
-
-    private void recycleVelocityTracker() {
-        if (mVelocityTracker != null) {
-            mVelocityTracker.recycle();
-            mVelocityTracker = null;
-        }
+    /**
+     * @return whether the current touch state is a horizontal drag offscreen.
+     */
+    private boolean isDraggingOffscreen(PipTouchState touchState) {
+        PointF lastDelta = touchState.getLastTouchDelta();
+        PointF downDelta = touchState.getDownTouchDelta();
+        float left = mPinnedStackBounds.left + lastDelta.x;
+        return !(mBoundedPinnedStackBounds.left <= left && left <= mBoundedPinnedStackBounds.right)
+                && Math.abs(downDelta.x) > Math.abs(downDelta.y);
     }
 
     /**
@@ -449,6 +361,71 @@
     }
 
     /**
+     * Sets the minimized state and notifies the controller.
+     */
+    private void setMinimizedState(boolean isMinimized) {
+        mIsMinimized = isMinimized;
+        try {
+            mPinnedStackController.setIsMinimized(isMinimized);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Could not set minimized state", e);
+        }
+    }
+
+    /**
+     * @return whether the given {@param pinnedStackBounds} indicates the PIP should be minimized.
+     */
+    private boolean shouldMinimizedPinnedStack() {
+        Point displaySize = new Point();
+        mContext.getDisplay().getRealSize(displaySize);
+        if (mPinnedStackBounds.left < 0) {
+            float offscreenFraction = (float) -mPinnedStackBounds.left / mPinnedStackBounds.width();
+            return offscreenFraction >= MINIMIZE_OFFSCREEN_FRACTION;
+        } else if (mPinnedStackBounds.right > displaySize.x) {
+            float offscreenFraction = (float) (mPinnedStackBounds.right - displaySize.x) /
+                    mPinnedStackBounds.width();
+            return offscreenFraction >= MINIMIZE_OFFSCREEN_FRACTION;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Flings the minimized PIP to the closest minimized snap target.
+     */
+    private void flingToMinimizedSnapTarget(float velocityY) {
+        // We currently only allow flinging the minimized stack up and down, so just lock the
+        // movement bounds to the current stack bounds horizontally
+        Rect movementBounds = new Rect(mPinnedStackBounds.left, mBoundedPinnedStackBounds.top,
+                mPinnedStackBounds.left, mBoundedPinnedStackBounds.bottom);
+        Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(movementBounds, mPinnedStackBounds,
+                0 /* velocityX */, velocityY);
+        if (!mPinnedStackBounds.equals(toBounds)) {
+            mPinnedStackBoundsAnimator = mMotionHelper.createAnimationToBounds(mPinnedStackBounds,
+                    toBounds, 0, FAST_OUT_SLOW_IN, mUpdatePinnedStackBoundsListener);
+            mFlingAnimationUtils.apply(mPinnedStackBoundsAnimator, 0,
+                    distanceBetweenRectOffsets(mPinnedStackBounds, toBounds),
+                    velocityY);
+            mPinnedStackBoundsAnimator.start();
+        }
+    }
+
+    /**
+     * Animates the PIP to the minimized state, slightly offscreen.
+     */
+    private void animateToClosestMinimizedTarget() {
+        Point displaySize = new Point();
+        mContext.getDisplay().getRealSize(displaySize);
+        Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(mBoundedPinnedStackBounds,
+                mPinnedStackBounds);
+        mSnapAlgorithm.applyMinimizedOffset(toBounds, mBoundedPinnedStackBounds, displaySize);
+        mPinnedStackBoundsAnimator = mMotionHelper.createAnimationToBounds(mPinnedStackBounds,
+                toBounds, MINIMIZE_STACK_MAX_DURATION, LINEAR_OUT_SLOW_IN,
+                mUpdatePinnedStackBoundsListener);
+        mPinnedStackBoundsAnimator.start();
+    }
+
+    /**
      * Flings the PIP to the closest snap target.
      */
     private void flingToSnapTarget(float velocity, float velocityX, float velocityY) {
@@ -478,12 +455,26 @@
     }
 
     /**
+     * @return whether the velocity is coincident with the current pinned stack bounds to be
+     *         considered a fling to dismiss.
+     */
+    private boolean isFlingToDismiss(float velocityX) {
+        Point displaySize = new Point();
+        mContext.getDisplay().getRealSize(displaySize);
+        return (mPinnedStackBounds.right > displaySize.x && velocityX > 0) ||
+                (mPinnedStackBounds.left < 0 && velocityX < 0);
+    }
+
+    /**
      * Flings the PIP to dismiss it offscreen.
      */
     private void flingToDismiss(float velocityX) {
+        Point displaySize = new Point();
+        mContext.getDisplay().getRealSize(displaySize);
         float offsetX = velocityX > 0
-            ? mBoundedPinnedStackBounds.right + 2 * mPinnedStackBounds.width()
-            : mBoundedPinnedStackBounds.left - 2 * mPinnedStackBounds.width();
+                ? displaySize.x + mPinnedStackBounds.width()
+                : -mPinnedStackBounds.width();
+
         Rect toBounds = new Rect(mPinnedStackBounds);
         toBounds.offsetTo((int) offsetX, toBounds.top);
         if (!mPinnedStackBounds.equals(toBounds)) {
@@ -495,13 +486,7 @@
             mPinnedStackBoundsAnimator.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationEnd(Animator animation) {
-                    BackgroundThread.getHandler().post(() -> {
-                        try {
-                            mActivityManager.removeStack(PINNED_STACK_ID);
-                        } catch (RemoteException e) {
-                            Log.e(TAG, "Failed to remove PIP", e);
-                        }
-                    });
+                    BackgroundThread.getHandler().post(PipTouchHandler.this::dismissPinnedStack);
                 }
             });
             mPinnedStackBoundsAnimator.start();
@@ -521,13 +506,7 @@
         mPinnedStackBoundsAnimator.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
-                BackgroundThread.getHandler().post(() -> {
-                    try {
-                        mActivityManager.removeStack(PINNED_STACK_ID);
-                    } catch (RemoteException e) {
-                        Log.e(TAG, "Failed to remove PIP", e);
-                    }
-                });
+                BackgroundThread.getHandler().post(PipTouchHandler.this::dismissPinnedStack);
             }
         });
         mPinnedStackBoundsAnimator.start();
@@ -549,6 +528,27 @@
     }
 
     /**
+     * Tries to the move the pinned stack to the given {@param bounds}.
+     */
+    private void movePinnedStack(Rect bounds) {
+        if (!bounds.equals(mPinnedStackBounds)) {
+            mPinnedStackBounds.set(bounds);
+            mMotionHelper.resizeToBounds(mPinnedStackBounds);
+        }
+    }
+
+    /**
+     * Dismisses the pinned stack.
+     */
+    private void dismissPinnedStack() {
+        try {
+            mActivityManager.removeStack(PINNED_STACK_ID);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to remove PIP", e);
+        }
+    }
+
+    /**
      * Updates the movement bounds of the pinned stack.
      */
     private void updateBoundedPinnedStackBounds(boolean updatePinnedStackBounds) {
@@ -572,4 +572,237 @@
     private float distanceBetweenRectOffsets(Rect r1, Rect r2) {
         return PointF.length(r1.left - r2.left, r1.top - r2.top);
     }
+
+    /**
+     * Gesture controlling dragging over a target to dismiss the PIP.
+     */
+    private PipTouchGesture mDragToDismissGesture = new PipTouchGesture() {
+        @Override
+        public void onDown(PipTouchState touchState) {
+            if (mEnableDragToDismiss) {
+                // TODO: Consider setting a timer such at after X time, we show the dismiss
+                //       target if the user hasn't already dragged some distance
+                mDismissViewController.createDismissTarget();
+            }
+        }
+
+        @Override
+        boolean onMove(PipTouchState touchState) {
+            if (mEnableDragToDismiss && touchState.startedDragging()) {
+                mDismissViewController.showDismissTarget();
+            }
+            return false;
+        }
+
+        @Override
+        public boolean onUp(PipTouchState touchState) {
+            if (mEnableDragToDismiss) {
+                try {
+                    if (touchState.isDragging()) {
+                        Rect dismissBounds = mDismissViewController.getDismissBounds();
+                        PointF lastTouch = touchState.getLastTouchPosition();
+                        if (dismissBounds.contains((int) lastTouch.x, (int) lastTouch.y)) {
+                            animateDismissPinnedStack(dismissBounds);
+                            return true;
+                        }
+                    }
+                } finally {
+                    mDismissViewController.destroyDismissTarget();
+                }
+            }
+            return false;
+        }
+    };
+
+    /**** Gestures ****/
+
+    /**
+     * Gesture controlling swiping offscreen to dismiss the PIP.
+     */
+    private PipTouchGesture mSwipeToDismissGesture = new PipTouchGesture() {
+        @Override
+        boolean onMove(PipTouchState touchState) {
+            if (mEnableSwipeToDismiss) {
+                boolean isDraggingOffscreen = isDraggingOffscreen(touchState);
+
+                if (touchState.startedDragging() && isDraggingOffscreen) {
+                    // Reset the minimized state once we drag horizontally
+                    setMinimizedState(false);
+                }
+
+                if (touchState.allowDraggingOffscreen() && isDraggingOffscreen) {
+                    // Move the pinned stack, but ignore the vertical movement
+                    float left = mPinnedStackBounds.left + touchState.getLastTouchDelta().x;
+                    mTmpBounds.set(mPinnedStackBounds);
+                    mTmpBounds.offsetTo((int) left, mPinnedStackBounds.top);
+                    if (!mTmpBounds.equals(mPinnedStackBounds)) {
+                        mPinnedStackBounds.set(mTmpBounds);
+                        mMotionHelper.resizeToBounds(mPinnedStackBounds);
+                    }
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public boolean onUp(PipTouchState touchState) {
+            if (mEnableSwipeToDismiss && touchState.isDragging()) {
+                PointF vel = touchState.getVelocity();
+                PointF downDelta = touchState.getDownTouchDelta();
+                float minFlingVel = mFlingAnimationUtils.getMinVelocityPxPerSecond();
+                float flingVelScale = mEnableMinimizing ? 3f : 2f;
+                if (Math.abs(vel.x) > (flingVelScale * minFlingVel)) {
+                    // Determine if this gesture is actually a fling to dismiss
+                    if (isFlingToDismiss(vel.x) && Math.abs(downDelta.x) >=
+                            (DISMISS_FLING_DISTANCE_FRACTION * mPinnedStackBounds.width())) {
+                        flingToDismiss(vel.x);
+                    } else {
+                        flingToSnapTarget(vel.length(), vel.x, vel.y);
+                    }
+                    return true;
+                }
+            }
+            return false;
+        }
+    };
+
+    /**
+     * Gesture controlling dragging the PIP slightly offscreen to minimize it.
+     */
+    private PipTouchGesture mMinimizeGesture = new PipTouchGesture() {
+        @Override
+        boolean onMove(PipTouchState touchState) {
+            if (mEnableMinimizing) {
+                boolean isDraggingOffscreen = isDraggingOffscreen(touchState);
+                if (touchState.startedDragging() && isDraggingOffscreen) {
+                    // Reset the minimized state once we drag horizontally
+                    setMinimizedState(false);
+                }
+
+                if (touchState.allowDraggingOffscreen() && isDraggingOffscreen) {
+                    // Move the pinned stack, but ignore the vertical movement
+                    float left = mPinnedStackBounds.left + touchState.getLastTouchDelta().x;
+                    mTmpBounds.set(mPinnedStackBounds);
+                    mTmpBounds.offsetTo((int) left, mPinnedStackBounds.top);
+                    if (!mTmpBounds.equals(mPinnedStackBounds)) {
+                        mPinnedStackBounds.set(mTmpBounds);
+                        mMotionHelper.resizeToBounds(mPinnedStackBounds);
+                    }
+                    return true;
+                } else if (mIsMinimized && touchState.isDragging()) {
+                    // Move the pinned stack, but ignore the horizontal movement
+                    PointF lastDelta = touchState.getLastTouchDelta();
+                    float top = mPinnedStackBounds.top + lastDelta.y;
+                    top = Math.max(mBoundedPinnedStackBounds.top, Math.min(
+                            mBoundedPinnedStackBounds.bottom, top));
+                    mTmpBounds.set(mPinnedStackBounds);
+                    mTmpBounds.offsetTo(mPinnedStackBounds.left, (int) top);
+                    movePinnedStack(mTmpBounds);
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public boolean onUp(PipTouchState touchState) {
+            if (mEnableMinimizing) {
+                if (touchState.isDragging()) {
+                    if (isDraggingOffscreen(touchState)) {
+                        if (shouldMinimizedPinnedStack()) {
+                            setMinimizedState(true);
+                            animateToClosestMinimizedTarget();
+                            return true;
+                        }
+                    } else if (mIsMinimized) {
+                        PointF vel = touchState.getVelocity();
+                        if (vel.length() > mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
+                            flingToMinimizedSnapTarget(vel.y);
+                        } else {
+                            animateToClosestMinimizedTarget();
+                        }
+                        return true;
+                    }
+                } else if (mIsMinimized) {
+                    setMinimizedState(false);
+                    animateToClosestSnapTarget();
+                    return true;
+                }
+            }
+            return false;
+        }
+    };
+
+    /**
+     * Gesture controlling tapping on the PIP to show an overlay.
+     */
+    private PipTouchGesture mTapThroughGesture = new PipTouchGesture() {
+        @Override
+        boolean onMove(PipTouchState touchState) {
+            if (mEnableTapThrough && touchState.startedDragging()) {
+                mIsTappingThrough = false;
+                mMenuController.hideMenu();
+            }
+            return false;
+        }
+
+        @Override
+        public boolean onUp(PipTouchState touchState) {
+            if (mEnableTapThrough && !touchState.isDragging() && !mIsTappingThrough) {
+                mMenuController.showMenu();
+                mIsTappingThrough = true;
+                return true;
+            }
+            return false;
+        }
+    };
+
+    /**
+     * Gesture controlling normal movement of the PIP.
+     */
+    private PipTouchGesture mDefaultMovementGesture = new PipTouchGesture() {
+        @Override
+        boolean onMove(PipTouchState touchState) {
+            if (touchState.startedDragging()) {
+                // For now, once the user has started a drag that the other gestures have not
+                // intercepted, disallow those gestures from intercepting again to drag offscreen
+                touchState.setDisallowDraggingOffscreen();
+            }
+
+            if (touchState.isDragging()) {
+                // Move the pinned stack freely
+                PointF lastDelta = touchState.getLastTouchDelta();
+                float left = mPinnedStackBounds.left + lastDelta.x;
+                float top = mPinnedStackBounds.top + lastDelta.y;
+                if (!DEBUG_ALLOW_OUT_OF_BOUNDS_STACK) {
+                    left = Math.max(mBoundedPinnedStackBounds.left, Math.min(
+                            mBoundedPinnedStackBounds.right, left));
+                    top = Math.max(mBoundedPinnedStackBounds.top, Math.min(
+                            mBoundedPinnedStackBounds.bottom, top));
+                }
+                mTmpBounds.set(mPinnedStackBounds);
+                mTmpBounds.offsetTo((int) left, (int) top);
+                movePinnedStack(mTmpBounds);
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public boolean onUp(PipTouchState touchState) {
+            if (touchState.isDragging()) {
+                PointF vel = mTouchState.getVelocity();
+                float velocity = PointF.length(vel.x, vel.y);
+                if (velocity > mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
+                    flingToSnapTarget(velocity, vel.x, vel.y);
+                } else {
+                    animateToClosestSnapTarget();
+                }
+            } else {
+                expandPinnedStackToFullscreen();
+            }
+            return true;
+        }
+    };
 }
diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchState.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchState.java
new file mode 100644
index 0000000..17d9864
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchState.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.pip.phone;
+
+import android.app.IActivityManager;
+import android.graphics.PointF;
+import android.view.IPinnedStackController;
+import android.view.IPinnedStackListener;
+import android.view.IWindowManager;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.ViewConfiguration;
+
+/**
+ * This keeps track of the touch state throughout the current touch gesture.
+ */
+public class PipTouchState {
+
+    private ViewConfiguration mViewConfig;
+
+    private VelocityTracker mVelocityTracker;
+    private final PointF mDownTouch = new PointF();
+    private final PointF mDownDelta = new PointF();
+    private final PointF mLastTouch = new PointF();
+    private final PointF mLastDelta = new PointF();
+    private final PointF mVelocity = new PointF();
+    private boolean mIsDragging = false;
+    private boolean mStartedDragging = false;
+    private boolean mAllowDraggingOffscreen = false;
+    private int mActivePointerId;
+
+    public PipTouchState(ViewConfiguration viewConfig) {
+        mViewConfig = viewConfig;
+    }
+
+    /**
+     * Processess a given touch event and updates the state.
+     */
+    public void onTouchEvent(MotionEvent ev) {
+        switch (ev.getAction()) {
+            case MotionEvent.ACTION_DOWN: {
+                // Initialize the velocity tracker
+                initOrResetVelocityTracker();
+                mActivePointerId = ev.getPointerId(0);
+                mLastTouch.set(ev.getX(), ev.getY());
+                mDownTouch.set(mLastTouch);
+                mIsDragging = false;
+                mStartedDragging = false;
+                mAllowDraggingOffscreen = true;
+                break;
+            }
+            case MotionEvent.ACTION_MOVE: {
+                // Update the velocity tracker
+                mVelocityTracker.addMovement(ev);
+                int pointerIndex = ev.findPointerIndex(mActivePointerId);
+                float x = ev.getX(pointerIndex);
+                float y = ev.getY(pointerIndex);
+                mLastDelta.set(x - mLastTouch.x, y - mLastTouch.y);
+                mDownDelta.set(x - mDownTouch.x, y - mDownTouch.y);
+
+                boolean hasMovedBeyondTap = mDownDelta.length() > mViewConfig.getScaledTouchSlop();
+                if (!mIsDragging) {
+                    if (hasMovedBeyondTap) {
+                        mIsDragging = true;
+                        mStartedDragging = true;
+                    }
+                } else {
+                    mStartedDragging = false;
+                }
+                mLastTouch.set(x, y);
+                break;
+            }
+            case MotionEvent.ACTION_POINTER_UP: {
+                // Update the velocity tracker
+                mVelocityTracker.addMovement(ev);
+
+                int pointerIndex = ev.getActionIndex();
+                int pointerId = ev.getPointerId(pointerIndex);
+                if (pointerId == mActivePointerId) {
+                    // Select a new active pointer id and reset the movement state
+                    final int newPointerIndex = (pointerIndex == 0) ? 1 : 0;
+                    mActivePointerId = ev.getPointerId(newPointerIndex);
+                    mLastTouch.set(ev.getX(newPointerIndex), ev.getY(newPointerIndex));
+                }
+                break;
+            }
+            case MotionEvent.ACTION_UP: {
+                // Update the velocity tracker
+                mVelocityTracker.addMovement(ev);
+                mVelocityTracker.computeCurrentVelocity(1000,
+                        mViewConfig.getScaledMaximumFlingVelocity());
+                mVelocity.set(mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
+
+                int pointerIndex = ev.findPointerIndex(mActivePointerId);
+                mLastTouch.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
+
+                // Fall through to clean up
+            }
+            case MotionEvent.ACTION_CANCEL: {
+                recycleVelocityTracker();
+                break;
+            }
+        }
+    }
+
+    /**
+     * @return the velocity of the active touch pointer at the point it is lifted off the screen.
+     */
+    public PointF getVelocity() {
+        return mVelocity;
+    }
+
+    /**
+     * @return the last touch position of the active pointer.
+     */
+    public PointF getLastTouchPosition() {
+        return mLastTouch;
+    }
+
+    /**
+     * @return the movement delta between the last handled touch event and the previous touch
+     *         position.
+     */
+    public PointF getLastTouchDelta() {
+        return mLastDelta;
+    }
+
+    /**
+     * @return the movement delta between the last handled touch event and the down touch
+     *         position.
+     */
+    public PointF getDownTouchDelta() {
+        return mDownDelta;
+    }
+
+    /**
+     * @return whether the user has started dragging.
+     */
+    public boolean isDragging() {
+        return mIsDragging;
+    }
+
+    /**
+     * @return whether the user has started dragging just in the last handled touch event.
+     */
+    public boolean startedDragging() {
+        return mStartedDragging;
+    }
+
+    /**
+     * Disallows dragging offscreen for the duration of the current gesture.
+     */
+    public void setDisallowDraggingOffscreen() {
+        mAllowDraggingOffscreen = false;
+    }
+
+    /**
+     * @return whether dragging offscreen is allowed during this gesture.
+     */
+    public boolean allowDraggingOffscreen() {
+        return mAllowDraggingOffscreen;
+    }
+
+    private void initOrResetVelocityTracker() {
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        } else {
+            mVelocityTracker.clear();
+        }
+    }
+
+    private void recycleVelocityTracker() {
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+            mVelocityTracker = null;
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
index e1cd143..dc42adc 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
@@ -20,7 +20,7 @@
 import android.view.View.OnLayoutChangeListener;
 import android.widget.TextView;
 
-import com.android.systemui.plugins.qs.QSContainer;
+import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.qs.PagedTileLayout.PageListener;
 import com.android.systemui.qs.QSPanel.QSTileLayout;
 import com.android.systemui.qs.QSTile.Host.Callback;
@@ -47,7 +47,7 @@
     private final ArrayList<View> mTopFiveQs = new ArrayList<>();
     private final QuickQSPanel mQuickQsPanel;
     private final QSPanel mQsPanel;
-    private final QSContainer mQsContainer;
+    private final QS mQs;
 
     private PagedTileLayout mPagedLayout;
 
@@ -67,12 +67,15 @@
     private float mLastPosition;
     private QSTileHost mHost;
 
-    public QSAnimator(QSContainer container, QuickQSPanel quickPanel, QSPanel panel) {
-        mQsContainer = container;
+    public QSAnimator(QS qs, QuickQSPanel quickPanel, QSPanel panel) {
+        mQs = qs;
         mQuickQsPanel = quickPanel;
         mQsPanel = panel;
         mQsPanel.addOnAttachStateChangeListener(this);
-        container.addOnLayoutChangeListener(this);
+        qs.getView().addOnLayoutChangeListener(this);
+        if (mQsPanel.isAttachedToWindow()) {
+            onViewAttachedToWindow(null);
+        }
         QSTileLayout tileLayout = mQsPanel.getTileLayout();
         if (tileLayout instanceof PagedTileLayout) {
             mPagedLayout = ((PagedTileLayout) tileLayout);
@@ -102,7 +105,7 @@
 
     @Override
     public void onViewAttachedToWindow(View v) {
-        TunerService.get(mQsContainer.getContext()).addTunable(this, ALLOW_FANCY_ANIMATION,
+        TunerService.get(mQs.getContext()).addTunable(this, ALLOW_FANCY_ANIMATION,
                 MOVE_FULL_ROWS, QuickQSPanel.NUM_QUICK_TILES);
     }
 
@@ -111,7 +114,7 @@
         if (mHost != null) {
             mHost.removeCallback(this);
         }
-        TunerService.get(mQsContainer.getContext()).removeTunable(this);
+        TunerService.get(mQs.getContext()).removeTunable(this);
     }
 
     @Override
@@ -124,7 +127,7 @@
         } else if (MOVE_FULL_ROWS.equals(key)) {
             mFullRows = newValue == null || Integer.parseInt(newValue) != 0;
         } else if (QuickQSPanel.NUM_QUICK_TILES.equals(key)) {
-            mNumQuickTiles = mQuickQsPanel.getNumQuickTiles(mQsContainer.getContext());
+            mNumQuickTiles = mQuickQsPanel.getNumQuickTiles(mQs.getContext());
             clearAnimationState();
         }
         updateAnimators();
@@ -166,13 +169,14 @@
             }
             final TextView label = ((QSTileView) tileView).getLabel();
             final View tileIcon = tileView.getIcon().getIconView();
+            View view = mQs.getView();
             if (count < mNumQuickTiles && mAllowFancy) {
                 // Quick tiles.
                 QSTileBaseView quickTileView = mQuickQsPanel.getTileView(tile);
 
                 lastX = loc1[0];
-                getRelativePosition(loc1, quickTileView.getIcon().getIconView(), mQsContainer);
-                getRelativePosition(loc2, tileIcon, mQsContainer);
+                getRelativePosition(loc1, quickTileView.getIcon().getIconView(), view);
+                getRelativePosition(loc2, tileIcon, view);
                 final int xDiff = loc2[0] - loc1[0];
                 final int yDiff = loc2[1] - loc1[1];
                 lastXDiff = loc1[0] - lastX;
@@ -198,7 +202,7 @@
                 // This makes the extra icons seems as if they are coming from positions in the
                 // quick panel.
                 loc1[0] += lastXDiff;
-                getRelativePosition(loc2, tileIcon, mQsContainer);
+                getRelativePosition(loc2, tileIcon, view);
                 final int xDiff = loc2[0] - loc1[0];
                 final int yDiff = loc2[1] - loc1[1];
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
index f345172..f4da5ec 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
@@ -16,53 +16,34 @@
 
 package com.android.systemui.qs;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
 import android.content.Context;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
+import android.widget.FrameLayout;
 
-import com.android.systemui.Interpolators;
 import com.android.systemui.R;
-import com.android.systemui.plugins.qs.QSContainer;
+import com.android.systemui.plugins.qs.QS.HeightListener;
 import com.android.systemui.qs.customize.QSCustomizer;
 import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer;
 import com.android.systemui.statusbar.phone.QSTileHost;
 import com.android.systemui.statusbar.phone.QuickStatusBarHeader;
-import com.android.systemui.statusbar.stack.StackStateAnimator;
 
 /**
  * Wrapper view with background which contains {@link QSPanel} and {@link BaseStatusBarHeader}
- *
- * Also manages animations for the QS Header and Panel.
  */
-public class QSContainerImpl extends QSContainer {
-    private static final String TAG = "QSContainer";
-    private static final boolean DEBUG = false;
+public class QSContainerImpl extends FrameLayout {
 
     private final Point mSizePoint = new Point();
-    private final Rect mQsBounds = new Rect();
 
     private int mHeightOverride = -1;
-    protected QSPanel mQSPanel;
-    private QSDetail mQSDetail;
-    protected QuickStatusBarHeader mHeader;
+    protected View mQSPanel;
+    private View mQSDetail;
+    protected View mHeader;
     protected float mQsExpansion;
-    private boolean mQsExpanded;
-    private boolean mHeaderAnimating;
-    private boolean mKeyguardShowing;
-    private boolean mStackScrollerOverscrolling;
-
-    private long mDelay;
-    private QSAnimator mQSAnimator;
     private QSCustomizer mQSCustomizer;
-    private HeightListener mPanelView;
-    private boolean mListening;
 
     public QSContainerImpl(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -71,31 +52,10 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
-        mQSPanel = (QSPanel) findViewById(R.id.quick_settings_panel);
-        mQSDetail = (QSDetail) findViewById(R.id.qs_detail);
-        mHeader = (QuickStatusBarHeader) findViewById(R.id.header);
-        mQSDetail.setQsPanel(mQSPanel, mHeader);
-        mQSAnimator = new QSAnimator(this, (QuickQSPanel) mHeader.findViewById(R.id.quick_qs_panel),
-                mQSPanel);
+        mQSPanel = findViewById(R.id.quick_settings_panel);
+        mQSDetail = findViewById(R.id.qs_detail);
+        mHeader = findViewById(R.id.header);
         mQSCustomizer = (QSCustomizer) findViewById(R.id.qs_customize);
-        mQSCustomizer.setQsContainer(this);
-    }
-
-    @Override
-    public void onRtlPropertiesChanged(int layoutDirection) {
-        super.onRtlPropertiesChanged(layoutDirection);
-        mQSAnimator.onRtlChanged();
-    }
-
-    public void setHost(QSTileHost qsh) {
-        mQSPanel.setHost(qsh, mQSCustomizer);
-        mHeader.setQSPanel(mQSPanel);
-        mQSDetail.setHost(qsh);
-        mQSAnimator.setHost(qsh);
-    }
-
-    public void setPanelView(HeightListener panelView) {
-        mPanelView = panelView;
     }
 
     @Override
@@ -111,8 +71,8 @@
         super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                 MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
 
-        // QSCustomizer is always be the height of the screen, but do this after
-        // other measuring to avoid changing the height of the QSContainer.
+        // QSCustomizer will always be the height of the screen, but do this after
+        // other measuring to avoid changing the height of the QS.
         getDisplay().getRealSize(mSizePoint);
         mQSCustomizer.measure(widthMeasureSpec,
                 MeasureSpec.makeMeasureSpec(mSizePoint.y, MeasureSpec.EXACTLY));
@@ -124,10 +84,6 @@
         updateBottom();
     }
 
-    public boolean isCustomizing() {
-        return mQSCustomizer.isCustomizing();
-    }
-
     /**
      * Overrides the height of this view (post-layout), so that the content is clipped to that
      * height and the background is set to that height.
@@ -139,41 +95,7 @@
         updateBottom();
     }
 
-    @Override
-    public void setContainer(ViewGroup container) {
-        if (container instanceof NotificationsQuickSettingsContainer) {
-            mQSCustomizer.setContainer((NotificationsQuickSettingsContainer) container);
-        }
-    }
-
-    /**
-     * The height this view wants to be. This is different from {@link #getMeasuredHeight} such that
-     * during closing the detail panel, this already returns the smaller height.
-     */
-    public int getDesiredHeight() {
-        if (isCustomizing()) {
-            return getHeight();
-        }
-        if (mQSDetail.isClosingDetail()) {
-            int panelHeight = ((LayoutParams) mQSPanel.getLayoutParams()).topMargin
-                    + mQSPanel.getMeasuredHeight();
-            return panelHeight + getPaddingBottom();
-        } else {
-            return getMeasuredHeight();
-        }
-    }
-
-    public void notifyCustomizeChanged() {
-        // The customize state changed, so our height changed.
-        updateBottom();
-        mQSPanel.setVisibility(!mQSCustomizer.isCustomizing() ? View.VISIBLE : View.INVISIBLE);
-        mHeader.setVisibility(!mQSCustomizer.isCustomizing() ? View.VISIBLE : View.INVISIBLE);
-        // Let the panel know the position changed and it needs to update where notifications
-        // and whatnot are.
-        mPanelView.onQsHeightChanged();
-    }
-
-    private void updateBottom() {
+    void updateBottom() {
         int height = calculateContainerHeight();
         setBottom(getTop() + height);
         mQSDetail.setBottom(getTop() + height);
@@ -182,162 +104,12 @@
     protected int calculateContainerHeight() {
         int heightOverride = mHeightOverride != -1 ? mHeightOverride : getMeasuredHeight();
         return mQSCustomizer.isCustomizing() ? mQSCustomizer.getHeight()
-                : (int) (mQsExpansion * (heightOverride - mHeader.getCollapsedHeight()))
-                + mHeader.getCollapsedHeight();
+                : (int) (mQsExpansion * (heightOverride - mHeader.getHeight()))
+                + mHeader.getHeight();
     }
 
-    private void updateQsState() {
-        boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling || mHeaderAnimating;
-        mQSPanel.setExpanded(mQsExpanded);
-        mQSDetail.setExpanded(mQsExpanded);
-        mHeader.setVisibility((mQsExpanded || !mKeyguardShowing || mHeaderAnimating)
-                ? View.VISIBLE
-                : View.INVISIBLE);
-        mHeader.setExpanded((mKeyguardShowing && !mHeaderAnimating)
-                || (mQsExpanded && !mStackScrollerOverscrolling));
-        mQSPanel.setVisibility(expandVisually ? View.VISIBLE : View.INVISIBLE);
-    }
-
-    public BaseStatusBarHeader getHeader() {
-        return mHeader;
-    }
-
-    public QSPanel getQsPanel() {
-        return mQSPanel;
-    }
-
-    public QSCustomizer getCustomizer() {
-        return mQSCustomizer;
-    }
-
-    public boolean isShowingDetail() {
-        return mQSPanel.isShowingCustomize() || mQSDetail.isShowingDetail();
-    }
-
-    public void setHeaderClickable(boolean clickable) {
-        if (DEBUG) Log.d(TAG, "setHeaderClickable " + clickable);
-        mHeader.setClickable(clickable);
-    }
-
-    public void setExpanded(boolean expanded) {
-        if (DEBUG) Log.d(TAG, "setExpanded " + expanded);
-        mQsExpanded = expanded;
-        mQSPanel.setListening(mListening && mQsExpanded);
-        updateQsState();
-    }
-
-    public void setKeyguardShowing(boolean keyguardShowing) {
-        if (DEBUG) Log.d(TAG, "setKeyguardShowing " + keyguardShowing);
-        mKeyguardShowing = keyguardShowing;
-        mQSAnimator.setOnKeyguard(keyguardShowing);
-        updateQsState();
-    }
-
-    public void setOverscrolling(boolean stackScrollerOverscrolling) {
-        if (DEBUG) Log.d(TAG, "setOverscrolling " + stackScrollerOverscrolling);
-        mStackScrollerOverscrolling = stackScrollerOverscrolling;
-        updateQsState();
-    }
-
-    public void setListening(boolean listening) {
-        if (DEBUG) Log.d(TAG, "setListening " + listening);
-        mListening = listening;
-        mHeader.setListening(listening);
-        mQSPanel.setListening(mListening && mQsExpanded);
-    }
-
-    public void setHeaderListening(boolean listening) {
-        mHeader.setListening(listening);
-    }
-
-    public void setQsExpansion(float expansion, float headerTranslation) {
-        if (DEBUG) Log.d(TAG, "setQSExpansion " + expansion + " " + headerTranslation);
+    public void setExpansion(float expansion) {
         mQsExpansion = expansion;
-        final float translationScaleY = expansion - 1;
-        if (!mHeaderAnimating) {
-            setTranslationY(mKeyguardShowing ? (translationScaleY * mHeader.getHeight())
-                    : headerTranslation);
-        }
-        mHeader.setExpansion(mKeyguardShowing ? 1 : expansion);
-        mQSPanel.setTranslationY(translationScaleY * mQSPanel.getHeight());
-        mQSDetail.setFullyExpanded(expansion == 1);
-        mQSAnimator.setPosition(expansion);
         updateBottom();
-
-        // Set bounds on the QS panel so it doesn't run over the header.
-        mQsBounds.top = (int) (mQSPanel.getHeight() * (1 - expansion));
-        mQsBounds.right = mQSPanel.getWidth();
-        mQsBounds.bottom = mQSPanel.getHeight();
-        mQSPanel.setClipBounds(mQsBounds);
-    }
-
-    public void animateHeaderSlidingIn(long delay) {
-        if (DEBUG) Log.d(TAG, "animateHeaderSlidingIn");
-        // If the QS is already expanded we don't need to slide in the header as it's already
-        // visible.
-        if (!mQsExpanded) {
-            mHeaderAnimating = true;
-            mDelay = delay;
-            getViewTreeObserver().addOnPreDrawListener(mStartHeaderSlidingIn);
-        }
-    }
-
-    public void animateHeaderSlidingOut() {
-        if (DEBUG) Log.d(TAG, "animateHeaderSlidingOut");
-        mHeaderAnimating = true;
-        animate().y(-mHeader.getHeight())
-                .setStartDelay(0)
-                .setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD)
-                .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
-                .setListener(new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        animate().setListener(null);
-                        mHeaderAnimating = false;
-                        updateQsState();
-                    }
-                })
-                .start();
-    }
-
-    @Override
-    public void closeDetail() {
-        mQSPanel.closeDetail();
-    }
-
-    private final ViewTreeObserver.OnPreDrawListener mStartHeaderSlidingIn
-            = new ViewTreeObserver.OnPreDrawListener() {
-        @Override
-        public boolean onPreDraw() {
-            getViewTreeObserver().removeOnPreDrawListener(this);
-            animate()
-                    .translationY(0f)
-                    .setStartDelay(mDelay)
-                    .setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE)
-                    .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
-                    .setListener(mAnimateHeaderSlidingInListener)
-                    .start();
-            setY(-mHeader.getHeight());
-            return true;
-        }
-    };
-
-    private final Animator.AnimatorListener mAnimateHeaderSlidingInListener
-            = new AnimatorListenerAdapter() {
-        @Override
-        public void onAnimationEnd(Animator animation) {
-            mHeaderAnimating = false;
-            updateQsState();
-        }
-    };
-
-    public int getQsMinExpansionHeight() {
-        return mHeader.getHeight();
-    }
-
-    @Override
-    public void hideImmediately() {
-        animate().cancel();
-        setY(-mHeader.getHeight());
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java b/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java
index 2b9320b..5027144 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java
@@ -35,9 +35,9 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.systemui.FontSizeUtils;
 import com.android.systemui.R;
-import com.android.systemui.plugins.qs.QSContainer.BaseStatusBarHeader;
-import com.android.systemui.plugins.qs.QSContainer.Callback;
-import com.android.systemui.plugins.qs.QSContainer.DetailAdapter;
+import com.android.systemui.plugins.qs.QS.BaseStatusBarHeader;
+import com.android.systemui.plugins.qs.QS.Callback;
+import com.android.systemui.plugins.qs.QS.DetailAdapter;
 import com.android.systemui.statusbar.phone.QSTileHost;
 
 public class QSDetail extends LinearLayout {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
new file mode 100644
index 0000000..c8f1670
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -0,0 +1,300 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.qs;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.annotation.Nullable;
+import android.app.Fragment;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.widget.FrameLayout.LayoutParams;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.qs.customize.QSCustomizer;
+import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer;
+import com.android.systemui.statusbar.phone.QSTileHost;
+import com.android.systemui.statusbar.phone.QuickStatusBarHeader;
+import com.android.systemui.statusbar.stack.StackStateAnimator;
+
+public class QSFragment extends Fragment implements QS {
+    private static final String TAG = "QS";
+    private static final boolean DEBUG = false;
+
+    private final Rect mQsBounds = new Rect();
+    private boolean mQsExpanded;
+    private boolean mHeaderAnimating;
+    private boolean mKeyguardShowing;
+    private boolean mStackScrollerOverscrolling;
+
+    private long mDelay;
+
+    private QSAnimator mQSAnimator;
+    private HeightListener mPanelView;
+    protected QuickStatusBarHeader mHeader;
+    private QSCustomizer mQSCustomizer;
+    protected QSPanel mQSPanel;
+    private QSDetail mQSDetail;
+    private boolean mListening;
+    private QSContainerImpl mContainer;
+    private int mLayoutDirection;
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+            Bundle savedInstanceState) {
+        return inflater.inflate(R.layout.qs_panel, container, false);
+    }
+
+    @Override
+    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+        mQSPanel = (QSPanel) view.findViewById(R.id.quick_settings_panel);
+        mQSDetail = (QSDetail) view.findViewById(R.id.qs_detail);
+        mHeader = (QuickStatusBarHeader) view.findViewById(R.id.header);
+        mContainer = (QSContainerImpl) view;
+
+        mQSDetail.setQsPanel(mQSPanel, mHeader);
+        mQSAnimator = new QSAnimator(this, (QuickQSPanel) mHeader.findViewById(R.id.quick_qs_panel),
+                mQSPanel);
+        mQSCustomizer = (QSCustomizer) view.findViewById(R.id.qs_customize);
+        mQSCustomizer.setQs(this);
+    }
+
+    public void setPanelView(HeightListener panelView) {
+        mPanelView = panelView;
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        if (newConfig.getLayoutDirection() != mLayoutDirection) {
+            mLayoutDirection = newConfig.getLayoutDirection();
+            mQSAnimator.onRtlChanged();
+        }
+    }
+
+    @Override
+    public void setContainer(ViewGroup container) {
+        if (container instanceof NotificationsQuickSettingsContainer) {
+            mQSCustomizer.setContainer((NotificationsQuickSettingsContainer) container);
+        }
+    }
+
+    public boolean isCustomizing() {
+        return mQSCustomizer.isCustomizing();
+    }
+
+    public void setHost(QSTileHost qsh) {
+        mQSPanel.setHost(qsh, mQSCustomizer);
+        mHeader.setQSPanel(mQSPanel);
+        mQSDetail.setHost(qsh);
+        mQSAnimator.setHost(qsh);
+    }
+
+    private void updateQsState() {
+        final boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling
+                || mHeaderAnimating;
+        mQSPanel.setExpanded(mQsExpanded);
+        mQSDetail.setExpanded(mQsExpanded);
+        mHeader.setVisibility((mQsExpanded || !mKeyguardShowing || mHeaderAnimating)
+                ? View.VISIBLE
+                : View.INVISIBLE);
+        mHeader.setExpanded((mKeyguardShowing && !mHeaderAnimating)
+                || (mQsExpanded && !mStackScrollerOverscrolling));
+        mQSPanel.setVisibility(expandVisually ? View.VISIBLE : View.INVISIBLE);
+    }
+
+    public BaseStatusBarHeader getHeader() {
+        return mHeader;
+    }
+
+    public QSPanel getQsPanel() {
+        return mQSPanel;
+    }
+
+    public QSCustomizer getCustomizer() {
+        return mQSCustomizer;
+    }
+
+    public boolean isShowingDetail() {
+        return mQSPanel.isShowingCustomize() || mQSDetail.isShowingDetail();
+    }
+
+    public void setHeaderClickable(boolean clickable) {
+        if (DEBUG) Log.d(TAG, "setHeaderClickable " + clickable);
+        mHeader.setClickable(clickable);
+    }
+
+    public void setExpanded(boolean expanded) {
+        if (DEBUG) Log.d(TAG, "setExpanded " + expanded);
+        mQsExpanded = expanded;
+        mQSPanel.setListening(mListening && mQsExpanded);
+        updateQsState();
+    }
+
+    public void setKeyguardShowing(boolean keyguardShowing) {
+        if (DEBUG) Log.d(TAG, "setKeyguardShowing " + keyguardShowing);
+        mKeyguardShowing = keyguardShowing;
+        mQSAnimator.setOnKeyguard(keyguardShowing);
+        updateQsState();
+    }
+
+    public void setOverscrolling(boolean stackScrollerOverscrolling) {
+        if (DEBUG) Log.d(TAG, "setOverscrolling " + stackScrollerOverscrolling);
+        mStackScrollerOverscrolling = stackScrollerOverscrolling;
+        updateQsState();
+    }
+
+    public void setListening(boolean listening) {
+        if (DEBUG) Log.d(TAG, "setListening " + listening);
+        mListening = listening;
+        mHeader.setListening(listening);
+        mQSPanel.setListening(mListening && mQsExpanded);
+    }
+
+    public void setHeaderListening(boolean listening) {
+        mHeader.setListening(listening);
+    }
+
+    public void setQsExpansion(float expansion, float headerTranslation) {
+        if (DEBUG) Log.d(TAG, "setQSExpansion " + expansion + " " + headerTranslation);
+        mContainer.setExpansion(expansion);
+        final float translationScaleY = expansion - 1;
+        if (!mHeaderAnimating) {
+            getView().setTranslationY(mKeyguardShowing ? (translationScaleY * mHeader.getHeight())
+                    : headerTranslation);
+        }
+        mHeader.setExpansion(mKeyguardShowing ? 1 : expansion);
+        mQSPanel.setTranslationY(translationScaleY * mQSPanel.getHeight());
+        mQSDetail.setFullyExpanded(expansion == 1);
+        mQSAnimator.setPosition(expansion);
+
+        // Set bounds on the QS panel so it doesn't run over the header.
+        mQsBounds.top = (int) (mQSPanel.getHeight() * (1 - expansion));
+        mQsBounds.right = mQSPanel.getWidth();
+        mQsBounds.bottom = mQSPanel.getHeight();
+        mQSPanel.setClipBounds(mQsBounds);
+    }
+
+    public void animateHeaderSlidingIn(long delay) {
+        if (DEBUG) Log.d(TAG, "animateHeaderSlidingIn");
+        // If the QS is already expanded we don't need to slide in the header as it's already
+        // visible.
+        if (!mQsExpanded) {
+            mHeaderAnimating = true;
+            mDelay = delay;
+            getView().getViewTreeObserver().addOnPreDrawListener(mStartHeaderSlidingIn);
+        }
+    }
+
+    public void animateHeaderSlidingOut() {
+        if (DEBUG) Log.d(TAG, "animateHeaderSlidingOut");
+        mHeaderAnimating = true;
+        getView().animate().y(-mHeader.getHeight())
+                .setStartDelay(0)
+                .setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD)
+                .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+                .setListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        getView().animate().setListener(null);
+                        mHeaderAnimating = false;
+                        updateQsState();
+                    }
+                })
+                .start();
+    }
+
+    @Override
+    public void closeDetail() {
+        mQSPanel.closeDetail();
+    }
+
+    public void notifyCustomizeChanged() {
+        // The customize state changed, so our height changed.
+        mContainer.updateBottom();
+        mQSPanel.setVisibility(!mQSCustomizer.isCustomizing() ? View.VISIBLE : View.INVISIBLE);
+        mHeader.setVisibility(!mQSCustomizer.isCustomizing() ? View.VISIBLE : View.INVISIBLE);
+        // Let the panel know the position changed and it needs to update where notifications
+        // and whatnot are.
+        mPanelView.onQsHeightChanged();
+    }
+
+    /**
+     * The height this view wants to be. This is different from {@link #getMeasuredHeight} such that
+     * during closing the detail panel, this already returns the smaller height.
+     */
+    public int getDesiredHeight() {
+        if (mQSCustomizer.isCustomizing()) {
+            return getView().getHeight();
+        }
+        if (mQSDetail.isClosingDetail()) {
+            int panelHeight = ((LayoutParams) mQSPanel.getLayoutParams()).topMargin
+                    + mQSPanel.getMeasuredHeight();
+            return panelHeight + getView().getPaddingBottom();
+        } else {
+            return getView().getMeasuredHeight();
+        }
+    }
+
+    @Override
+    public void setHeightOverride(int desiredHeight) {
+        mContainer.setHeightOverride(desiredHeight);
+    }
+
+    public int getQsMinExpansionHeight() {
+        return mHeader.getHeight();
+    }
+
+    @Override
+    public void hideImmediately() {
+        getView().animate().cancel();
+        getView().setY(-mHeader.getHeight());
+    }
+
+    private final ViewTreeObserver.OnPreDrawListener mStartHeaderSlidingIn
+            = new ViewTreeObserver.OnPreDrawListener() {
+        @Override
+        public boolean onPreDraw() {
+            getView().getViewTreeObserver().removeOnPreDrawListener(this);
+            getView().animate()
+                    .translationY(0f)
+                    .setStartDelay(mDelay)
+                    .setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE)
+                    .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+                    .setListener(mAnimateHeaderSlidingInListener)
+                    .start();
+            getView().setY(-mHeader.getHeight());
+            return true;
+        }
+    };
+
+    private final Animator.AnimatorListener mAnimateHeaderSlidingInListener
+            = new AnimatorListenerAdapter() {
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            mHeaderAnimating = false;
+            updateQsState();
+        }
+    };
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 49307d8..e55ff70 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -30,8 +30,8 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.systemui.R;
-import com.android.systemui.plugins.qs.QSContainer;
-import com.android.systemui.plugins.qs.QSContainer.DetailAdapter;
+import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.plugins.qs.QS.DetailAdapter;
 import com.android.systemui.qs.QSTile.Host.Callback;
 import com.android.systemui.qs.customize.QSCustomizer;
 import com.android.systemui.qs.external.CustomTile;
@@ -60,7 +60,7 @@
     protected boolean mExpanded;
     protected boolean mListening;
 
-    private QSContainer.Callback mCallback;
+    private QS.Callback mCallback;
     private BrightnessController mBrightnessController;
     protected QSTileHost mHost;
 
@@ -171,7 +171,7 @@
         return mBrightnessView;
     }
 
-    public void setCallback(QSContainer.Callback callback) {
+    public void setCallback(QS.Callback callback) {
         mCallback = callback;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTile.java b/packages/SystemUI/src/com/android/systemui/qs/QSTile.java
index 6856575..4341d17 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTile.java
@@ -31,7 +31,7 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.settingslib.RestrictedLockUtils;
-import com.android.systemui.plugins.qs.QSContainer.DetailAdapter;
+import com.android.systemui.plugins.qs.QS.DetailAdapter;
 import com.android.systemui.qs.QSTile.State;
 import com.android.systemui.qs.external.TileServices;
 import com.android.systemui.statusbar.phone.ManagedProfileController;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
index f663c75..9bbead4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
@@ -37,7 +37,7 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto;
 import com.android.systemui.R;
-import com.android.systemui.plugins.qs.QSContainer;
+import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.qs.QSDetailClipper;
 import com.android.systemui.qs.QSTile;
 import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer;
@@ -69,7 +69,7 @@
     private Toolbar mToolbar;
     private boolean mCustomizing;
     private NotificationsQuickSettingsContainer mNotifQsContainer;
-    private QSContainer mQsContainer;
+    private QS mQs;
 
     public QSCustomizer(Context context, AttributeSet attrs) {
         super(new ContextThemeWrapper(context, R.style.edit_theme), attrs);
@@ -127,8 +127,8 @@
         mNotifQsContainer = notificationsQsContainer;
     }
 
-    public void setQsContainer(QSContainer qsContainer) {
-        mQsContainer = qsContainer;
+    public void setQs(QS qs) {
+        mQs = qs;
     }
 
     public void show(int x, int y) {
@@ -169,7 +169,7 @@
 
     private void setCustomizing(boolean customizing) {
         mCustomizing = customizing;
-        mQsContainer.notifyCustomizeChanged();
+        mQs.notifyCustomizeChanged();
     }
 
     public boolean isCustomizing() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
index 91a0eb0..342df5e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
@@ -143,6 +143,7 @@
     }
 
     public void handleDestroy() {
+        setBindAllowed(false);
         mServices.getContext().unregisterReceiver(mUninstallReceiver);
         mStateManager.handleDestroy();
     }
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 6bc94b2..015a4c0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
@@ -306,6 +306,11 @@
         }
     }
 
+    public void destroy() {
+        mServices.values().forEach(service -> service.handleDestroy());
+        mContext.unregisterReceiver(mRequestListeningReceiver);
+    }
+
     private final BroadcastReceiver mRequestListeningReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
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 e5a555a..fc1c1ac 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BatteryTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BatteryTile.java
@@ -20,8 +20,6 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.graphics.drawable.Drawable;
-import android.os.Handler;
-import android.os.Looper;
 import android.text.SpannableStringBuilder;
 import android.text.Spanned;
 import android.text.style.RelativeSizeSpan;
@@ -40,7 +38,7 @@
 import com.android.settingslib.graph.UsageView;
 import com.android.systemui.BatteryMeterDrawable;
 import com.android.systemui.R;
-import com.android.systemui.plugins.qs.QSContainer.DetailAdapter;
+import com.android.systemui.plugins.qs.QS.DetailAdapter;
 import com.android.systemui.qs.QSTile;
 import com.android.systemui.statusbar.policy.BatteryController;
 
@@ -80,9 +78,9 @@
     @Override
     public void setListening(boolean listening) {
         if (listening) {
-            mBatteryController.addStateChangedCallback(this);
+            mBatteryController.addCallback(this);
         } else {
-            mBatteryController.removeStateChangedCallback(this);
+            mBatteryController.removeCallback(this);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
index 53010af..f83b279 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
@@ -32,7 +32,7 @@
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 import com.android.systemui.R;
-import com.android.systemui.plugins.qs.QSContainer.DetailAdapter;
+import com.android.systemui.plugins.qs.QS.DetailAdapter;
 import com.android.systemui.qs.QSDetailItems;
 import com.android.systemui.qs.QSDetailItems.Item;
 import com.android.systemui.qs.QSTile;
@@ -67,9 +67,9 @@
     @Override
     public void setListening(boolean listening) {
         if (listening) {
-            mController.addStateChangedCallback(mCallback);
+            mController.addCallback(mCallback);
         } else {
-            mController.removeStateChangedCallback(mCallback);
+            mController.removeCallback(mCallback);
         }
     }
 
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 4abd84a..8afa91e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
@@ -28,7 +28,7 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.systemui.R;
-import com.android.systemui.plugins.qs.QSContainer.DetailAdapter;
+import com.android.systemui.plugins.qs.QS.DetailAdapter;
 import com.android.systemui.qs.QSDetailItems;
 import com.android.systemui.qs.QSDetailItems.Item;
 import com.android.systemui.qs.QSTile;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java
index 2183565..1406c9f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java
@@ -29,7 +29,7 @@
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.settingslib.net.DataUsageController;
 import com.android.systemui.R;
-import com.android.systemui.plugins.qs.QSContainer.DetailAdapter;
+import com.android.systemui.plugins.qs.QS.DetailAdapter;
 import com.android.systemui.qs.QSIconView;
 import com.android.systemui.qs.QSTile;
 import com.android.systemui.qs.SignalTileView;
@@ -68,9 +68,9 @@
     @Override
     public void setListening(boolean listening) {
         if (listening) {
-            mController.addSignalCallback(mSignalCallback);
+            mController.addCallback(mSignalCallback);
         } else {
-            mController.removeSignalCallback(mSignalCallback);
+            mController.removeCallback(mSignalCallback);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java
index 0ff81e5..65432dc 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java
@@ -44,9 +44,9 @@
     @Override
     public void setListening(boolean listening) {
         if (listening) {
-            mDataSaverController.addListener(this);
+            mDataSaverController.addCallback(this);
         } else {
-            mDataSaverController.remListener(this);
+            mDataSaverController.removeCallback(this);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java
index 544ee91..198375d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java
@@ -37,7 +37,7 @@
 import com.android.systemui.Prefs;
 import com.android.systemui.R;
 import com.android.systemui.SysUIToast;
-import com.android.systemui.plugins.qs.QSContainer.DetailAdapter;
+import com.android.systemui.plugins.qs.QS.DetailAdapter;
 import com.android.systemui.qs.QSTile;
 import com.android.systemui.statusbar.policy.ZenModeController;
 import com.android.systemui.volume.ZenModePanel;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java
index 8fdce65..5b1638f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java
@@ -45,13 +45,13 @@
     public FlashlightTile(Host host) {
         super(host);
         mFlashlightController = host.getFlashlightController();
-        mFlashlightController.addListener(this);
+        mFlashlightController.addCallback(this);
     }
 
     @Override
     protected void handleDestroy() {
         super.handleDestroy();
-        mFlashlightController.removeListener(this);
+        mFlashlightController.removeCallback(this);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java
index b888fc8..dcee659 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java
@@ -16,8 +16,6 @@
 
 package com.android.systemui.qs.tiles;
 
-import android.content.BroadcastReceiver;
-import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.os.UserManager;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java
index 4b89075..8a9a696 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java
@@ -58,10 +58,10 @@
     @Override
     public void setListening(boolean listening) {
         if (listening) {
-            mController.addSettingsChangedCallback(mCallback);
+            mController.addCallback(mCallback);
             mKeyguard.addCallback(mCallback);
         } else {
-            mController.removeSettingsChangedCallback(mCallback);
+            mController.removeCallback(mCallback);
             mKeyguard.removeCallback(mCallback);
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java
index eb4c510..cec5f15 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java
@@ -61,9 +61,9 @@
     public void setListening(boolean listening) {
         if (mController == null) return;
         if (listening) {
-            mController.addRotationLockControllerCallback(mCallback);
+            mController.addCallback(mCallback);
         } else {
-            mController.removeRotationLockControllerCallback(mCallback);
+            mController.removeCallback(mCallback);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserTile.java
index e79e519..246c23e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserTile.java
@@ -22,7 +22,7 @@
 import android.util.Pair;
 
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import com.android.systemui.plugins.qs.QSContainer.DetailAdapter;
+import com.android.systemui.plugins.qs.QS.DetailAdapter;
 import com.android.systemui.qs.QSTile;
 import com.android.systemui.statusbar.policy.UserInfoController;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
@@ -67,9 +67,9 @@
     @Override
     public void setListening(boolean listening) {
         if (listening) {
-            mUserInfoController.addListener(this);
+            mUserInfoController.addCallback(this);
         } else {
-            mUserInfoController.remListener(this);
+            mUserInfoController.removeCallback(this);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java
index 9ce748f6..3876c88 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java
@@ -31,7 +31,7 @@
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.settingslib.wifi.AccessPoint;
 import com.android.systemui.R;
-import com.android.systemui.plugins.qs.QSContainer.DetailAdapter;
+import com.android.systemui.plugins.qs.QS.DetailAdapter;
 import com.android.systemui.qs.QSDetailItems;
 import com.android.systemui.qs.QSDetailItems.Item;
 import com.android.systemui.qs.QSIconView;
@@ -70,9 +70,9 @@
     @Override
     public void setListening(boolean listening) {
         if (listening) {
-            mController.addSignalCallback(mSignalCallback);
+            mController.addCallback(mSignalCallback);
         } else {
-            mController.removeSignalCallback(mSignalCallback);
+            mController.removeCallback(mSignalCallback);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/recents/grid/RecentsGridActivity.java b/packages/SystemUI/src/com/android/systemui/recents/grid/RecentsGridActivity.java
index 9ed4924..e804b52 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/grid/RecentsGridActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/grid/RecentsGridActivity.java
@@ -60,7 +60,6 @@
 import com.android.systemui.recents.views.TaskView;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 
 /**
@@ -177,9 +176,7 @@
         loadOpts.numVisibleTaskThumbnails = numVisibleTasks;
         loader.loadTasks(this, plan, loadOpts);
 
-        List<Task> stackTasks = mTaskStack.getStackTasks();
-        Collections.reverse(stackTasks);
-        mTasks = stackTasks;
+        mTasks = mTaskStack.getStackTasks();
 
         updateControlVisibility();
 
@@ -255,7 +252,10 @@
         removeTaskViews();
         for (int i = 0; i < rects.size(); i++) {
             Rect rect = rects.get(i);
-            View taskView = mTaskViews.get(i);
+            // We keep the same ordering in the model as other Recents flavors (older tasks are
+            // first in the stack) so that the logic can be similar, but we reverse the order
+            // when placing views on the screen so that most recent tasks are displayed first.
+            View taskView = mTaskViews.get(rects.size() - 1 - i);
             taskView.setLayoutParams(new FrameLayout.LayoutParams(rect.width(), rect.height()));
             taskView.setTranslationX(rect.left);
             taskView.setTranslationY(rect.top);
@@ -302,6 +302,9 @@
             dismissRecentsToLaunchTargetTaskOrHome();
         } else if (event.triggeredFromHomeKey) {
             dismissRecentsToHome();
+        } else {
+            // Fall through tap on the background view but not on any of the tasks.
+            dismissRecentsToHome();
         }
     }
 
@@ -365,10 +368,7 @@
 
     public final void onBusEvent(LaunchNextTaskRequestEvent event) {
         if (mTaskStack.getTaskCount() > 0) {
-            // The task to launch is the second most recent, which is at index 1 given our ordering.
-            // If there is only one task, launch that one instead.
-            int launchTaskIndex = (mTaskStack.getStackTaskCount() > 1) ? 1 : 0;
-            Task launchTask = mTaskStack.getStackTasks().get(launchTaskIndex);
+            Task launchTask = mTaskStack.getNextLaunchTarget();
             TaskView launchTaskView = getChildViewForTask(launchTask);
             if (launchTaskView != null) {
                 EventBus.getDefault().send(new LaunchTaskEvent(launchTaskView,
diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java
index 745f5a5..178cb9f 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java
@@ -849,6 +849,24 @@
         return null;
     }
 
+    /**
+     * Returns the task in stack tasks which should be launched next if Recents are toggled
+     * again, or null if there is no task to be launched.
+     */
+    public Task getNextLaunchTarget() {
+        int taskCount = getTaskCount();
+        if (taskCount == 0) {
+            return null;
+        }
+        int launchTaskIndex = indexOfStackTask(getLaunchTarget());
+        if (launchTaskIndex != -1) {
+            launchTaskIndex = Math.max(0, launchTaskIndex - 1);
+        } else {
+            launchTaskIndex = getTaskCount() - 1;
+        }
+        return getStackTasks().get(launchTaskIndex);
+    }
+
     /** Returns the index of this task in this current task stack */
     public int indexOfStackTask(Task t) {
         return mStackTaskList.indexOf(t);
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
index 77c61f8..8c94c35 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
@@ -1683,17 +1683,11 @@
             return;
         }
 
-        int launchTaskIndex = mStack.indexOfStackTask(mStack.getLaunchTarget());
-        if (launchTaskIndex != -1) {
-            launchTaskIndex = Math.max(0, launchTaskIndex - 1);
-        } else {
-            launchTaskIndex = mStack.getTaskCount() - 1;
-        }
-        if (launchTaskIndex != -1) {
+        final Task launchTask = mStack.getNextLaunchTarget();
+        if (launchTask != null) {
             // Stop all animations
             cancelAllTaskViewAnimations();
 
-            final Task launchTask = mStack.getStackTasks().get(launchTaskIndex);
             float curScroll = mStackScroller.getStackScroll();
             float targetScroll = mLayoutAlgorithm.getStackScrollForTaskAtInitialOffset(launchTask);
             float absScrollDiff = Math.abs(targetScroll - curScroll);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java
index 74caa53..68d5cd4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java
@@ -151,8 +151,8 @@
             mBlockEthernet = blockEthernet;
             mBlockWifi = blockWifi;
             // Re-register to get new callbacks.
-            mNC.removeSignalCallback(this);
-            mNC.addSignalCallback(this);
+            mNC.removeCallback(this);
+            mNC.addCallback(this);
         }
     }
 
@@ -224,7 +224,7 @@
 
         apply();
         applyIconTint();
-        mNC.addSignalCallback(this);
+        mNC.addCallback(this);
     }
 
     @Override
@@ -232,7 +232,7 @@
         mMobileSignalGroup.removeAllViews();
         TunerService.get(mContext).removeTunable(this);
         mSC.removeCallback(this);
-        mNC.removeSignalCallback(this);
+        mNC.removeCallback(this);
 
         super.onDetachedFromWindow();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/car/CarBatteryController.java b/packages/SystemUI/src/com/android/systemui/statusbar/car/CarBatteryController.java
index 4977202..fc39648 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/car/CarBatteryController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/car/CarBatteryController.java
@@ -99,7 +99,7 @@
     }
 
     @Override
-    public void addStateChangedCallback(BatteryController.BatteryStateChangeCallback cb) {
+    public void addCallback(BatteryController.BatteryStateChangeCallback cb) {
         mChangeCallbacks.add(cb);
 
         // There is no way to know if the phone is plugged in or charging via bluetooth, so pass
@@ -109,7 +109,7 @@
     }
 
     @Override
-    public void removeStateChangedCallback(BatteryController.BatteryStateChangeCallback cb) {
+    public void removeCallback(BatteryController.BatteryStateChangeCallback cb) {
         mChangeCallbacks.remove(cb);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/car/CarNavigationBarController.java b/packages/SystemUI/src/com/android/systemui/statusbar/car/CarNavigationBarController.java
index baff680..1c8c317 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/car/CarNavigationBarController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/car/CarNavigationBarController.java
@@ -30,7 +30,7 @@
 import android.widget.LinearLayout;
 
 import com.android.systemui.R;
-import com.android.systemui.plugins.qs.QSContainer.ActivityStarter;
+import com.android.systemui.plugins.qs.QS.ActivityStarter;
 
 import java.net.URISyntaxException;
 import java.util.ArrayList;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/car/ConnectedDeviceSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/car/ConnectedDeviceSignalController.java
index 66030b9..a3e1b3a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/car/ConnectedDeviceSignalController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/car/ConnectedDeviceSignalController.java
@@ -94,12 +94,12 @@
         filter.addAction(BluetoothHeadsetClient.ACTION_AG_EVENT);
         mContext.registerReceiver(this, filter);
 
-        mController.addStateChangedCallback(this);
+        mController.addCallback(this);
     }
 
     public void stopListening() {
         mContext.unregisterReceiver(this);
-        mController.removeStateChangedCallback(this);
+        mController.removeCallback(this);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java
index b742479..a011162 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java
@@ -42,7 +42,7 @@
             host.getHotspotController().addCallback(mHotspotCallback);
         }
         if (!Prefs.getBoolean(context, Key.QS_DATA_SAVER_ADDED, false)) {
-            host.getNetworkController().getDataSaverController().addListener(mDataSaverListener);
+            host.getNetworkController().getDataSaverController().addCallback(mDataSaverListener);
         }
         if (!Prefs.getBoolean(context, Key.QS_INVERT_COLORS_ADDED, false)) {
             mColorsSetting = new SecureSetting(mContext, mHandler,
@@ -69,7 +69,10 @@
     }
 
     public void destroy() {
-        // TODO: Remove any registered listeners.
+        mColorsSetting.setListening(false);
+        mHost.getHotspotController().removeCallback(mHotspotCallback);
+        mHost.getNetworkController().getDataSaverController().removeCallback(mDataSaverListener);
+        mHost.getManagedProfileController().removeCallback(mProfileCallback);
     }
 
     private final ManagedProfileController.Callback mProfileCallback =
@@ -105,7 +108,7 @@
                 mHandler.post(new Runnable() {
                     @Override
                     public void run() {
-                        mHost.getNetworkController().getDataSaverController().remListener(
+                        mHost.getNetworkController().getDataSaverController().removeCallback(
                                 mDataSaverListener);
                     }
                 });
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 dfb06d7..dbae6b8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
@@ -66,7 +66,7 @@
 import com.android.systemui.plugins.IntentButtonProvider.IntentButton.IconState;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.plugins.PluginManager;
-import com.android.systemui.plugins.qs.QSContainer.ActivityStarter;
+import com.android.systemui.plugins.qs.QS.ActivityStarter;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.KeyguardAffordanceView;
 import com.android.systemui.statusbar.KeyguardIndicationController;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
index d94def9..e4c778c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
@@ -190,9 +190,9 @@
         }
         mBatteryListening = listening;
         if (mBatteryListening) {
-            mBatteryController.addStateChangedCallback(this);
+            mBatteryController.addCallback(this);
         } else {
-            mBatteryController.removeStateChangedCallback(this);
+            mBatteryController.removeCallback(this);
         }
     }
 
@@ -214,7 +214,7 @@
     }
 
     public void setUserInfoController(UserInfoController userInfoController) {
-        userInfoController.addListener(new UserInfoController.OnUserInfoChangedListener() {
+        userInfoController.addCallback(new UserInfoController.OnUserInfoChangedListener() {
             @Override
             public void onUserInfoChanged(String name, Drawable picture, String userAccount) {
                 mMultiUserAvatar.setImageDrawable(picture);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightStatusBarController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightStatusBarController.java
index df4566b..dd7f3cc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightStatusBarController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightStatusBarController.java
@@ -46,7 +46,7 @@
             BatteryController batteryController) {
         mIconController = iconController;
         mBatteryController = batteryController;
-        batteryController.addStateChangedCallback(this);
+        batteryController.addCallback(this);
     }
 
     public void setFingerprintUnlockController(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileController.java
index 951b096..1a46815 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileController.java
@@ -24,11 +24,14 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 
+import com.android.systemui.statusbar.phone.ManagedProfileController.Callback;
+import com.android.systemui.statusbar.policy.CallbackController;
+
 import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.List;
 
-public class ManagedProfileController {
+public class ManagedProfileController implements CallbackController<Callback> {
 
     private final List<Callback> mCallbacks = new ArrayList<>();
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java
index af9454c..4d4f9d2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java
@@ -30,7 +30,7 @@
 import android.widget.FrameLayout;
 
 import com.android.systemui.R;
-import com.android.systemui.plugins.qs.QSContainer.DetailAdapter;
+import com.android.systemui.plugins.qs.QS.DetailAdapter;
 import com.android.systemui.qs.QSPanel;
 import com.android.systemui.statusbar.policy.KeyguardUserSwitcher;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
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 cda0bfe..69d76e5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -21,6 +21,7 @@
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.app.ActivityManager;
+import android.app.Fragment;
 import android.app.StatusBarManager;
 import android.content.Context;
 import android.content.pm.ResolveInfo;
@@ -34,6 +35,7 @@
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 import android.view.View;
+import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
 import android.view.WindowInsets;
 import android.view.accessibility.AccessibilityEvent;
@@ -42,15 +44,15 @@
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.keyguard.KeyguardStatusView;
-import com.android.systemui.AutoReinflateContainer;
-import com.android.systemui.AutoReinflateContainer.InflateListener;
 import com.android.systemui.DejankUtils;
 import com.android.systemui.EventLogConstants;
 import com.android.systemui.EventLogTags;
 import com.android.systemui.Interpolators;
 import com.android.systemui.R;
 import com.android.systemui.classifier.FalsingManager;
-import com.android.systemui.plugins.qs.QSContainer;
+import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
+import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.statusbar.ExpandableNotificationRow;
 import com.android.systemui.statusbar.ExpandableView;
 import com.android.systemui.statusbar.FlingAnimationUtils;
@@ -70,7 +72,7 @@
         ExpandableView.OnHeightChangedListener,
         View.OnClickListener, NotificationStackScrollLayout.OnOverscrollTopChangedListener,
         KeyguardAffordanceHelper.Callback, NotificationStackScrollLayout.OnEmptySpaceClickListener,
-        HeadsUpManager.OnHeadsUpChangedListener, QSContainer.HeightListener {
+        HeadsUpManager.OnHeadsUpChangedListener, QS.HeightListener {
 
     private static final boolean DEBUG = false;
 
@@ -92,8 +94,8 @@
     private KeyguardAffordanceHelper mAfforanceHelper;
     private KeyguardUserSwitcher mKeyguardUserSwitcher;
     private KeyguardStatusBarView mKeyguardStatusBar;
-    protected QSContainer mQsContainer;
-    private AutoReinflateContainer mQsAutoReinflateContainer;
+    private QS mQs;
+    private FrameLayout mQsFrame;
     private KeyguardStatusView mKeyguardStatusView;
     private TextView mClockView;
     private View mReserveNotificationSpace;
@@ -236,31 +238,19 @@
         mKeyguardBottomArea.setAffordanceHelper(mAfforanceHelper);
         mLastOrientation = getResources().getConfiguration().orientation;
 
-        mQsAutoReinflateContainer =
-                (AutoReinflateContainer) findViewById(R.id.qs_auto_reinflate_container);
-        mQsAutoReinflateContainer.addInflateListener(new InflateListener() {
-            @Override
-            public void onInflated(View v) {
-                mQsContainer = (QSContainer) v.findViewById(R.id.quick_settings_container);
-                mQsContainer.setPanelView(NotificationPanelView.this);
-                mQsContainer.getHeader().getExpandView()
-                        .setOnClickListener(NotificationPanelView.this);
+        mQsFrame = (FrameLayout) findViewById(R.id.qs_frame);
+    }
 
-                // recompute internal state when qspanel height changes
-                mQsContainer.addOnLayoutChangeListener(new OnLayoutChangeListener() {
-                    @Override
-                    public void onLayoutChange(View v, int left, int top, int right, int bottom,
-                            int oldLeft, int oldTop, int oldRight, int oldBottom) {
-                        final int height = bottom - top;
-                        final int oldHeight = oldBottom - oldTop;
-                        if (height != oldHeight) {
-                            onQsHeightChanged();
-                        }
-                    }
-                });
-                mNotificationStackScroller.setQsContainer(mQsContainer);
-            }
-        });
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        FragmentHostManager.get(this).addTagListener(QS.TAG, mFragmentListener);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        FragmentHostManager.get(this).removeTagListener(QS.TAG, mFragmentListener);
     }
 
     @Override
@@ -288,12 +278,12 @@
         int panelWidth = getResources().getDimensionPixelSize(R.dimen.notification_panel_width);
         int panelGravity = getResources().getInteger(R.integer.notification_panel_layout_gravity);
         FrameLayout.LayoutParams lp =
-                (FrameLayout.LayoutParams) mQsAutoReinflateContainer.getLayoutParams();
+                (FrameLayout.LayoutParams) mQsFrame.getLayoutParams();
         if (lp.width != panelWidth) {
             lp.width = panelWidth;
             lp.gravity = panelGravity;
-            mQsAutoReinflateContainer.setLayoutParams(lp);
-            mQsContainer.post(mUpdateHeader);
+            mQsFrame.setLayoutParams(lp);
+            mQs.getView().post(mUpdateHeader);
         }
 
         lp = (FrameLayout.LayoutParams) mNotificationStackScroller.getLayoutParams();
@@ -314,8 +304,10 @@
 
         // Calculate quick setting heights.
         int oldMaxHeight = mQsMaxExpansionHeight;
-        mQsMinExpansionHeight = mKeyguardShowing ? 0 : mQsContainer.getQsMinExpansionHeight();
-        mQsMaxExpansionHeight = mQsContainer.getDesiredHeight();
+        if (mQs != null) {
+            mQsMinExpansionHeight = mKeyguardShowing ? 0 : mQs.getQsMinExpansionHeight();
+            mQsMaxExpansionHeight = mQs.getDesiredHeight();
+        }
         positionClockAndNotifications();
         if (mQsExpanded && mQsFullyExpanded) {
             mQsExpansionHeight = mQsMaxExpansionHeight;
@@ -337,8 +329,8 @@
         // the desired height so when closing the QS detail, it stays smaller after the size change
         // animation is finished but the detail view is still being animated away (this animation
         // takes longer than the size change animation).
-        if (mQsSizeChangeAnimator == null) {
-            mQsContainer.setHeightOverride(mQsContainer.getDesiredHeight());
+        if (mQsSizeChangeAnimator == null && mQs != null) {
+            mQs.setHeightOverride(mQs.getDesiredHeight());
         }
         updateMaxHeadsUpTranslation();
     }
@@ -357,7 +349,7 @@
                 requestScrollerTopPaddingUpdate(false /* animate */);
                 requestPanelHeightUpdate();
                 int height = (int) mQsSizeChangeAnimator.getAnimatedValue();
-                mQsContainer.setHeightOverride(height);
+                mQs.setHeightOverride(height);
             }
         });
         mQsSizeChangeAnimator.addListener(new AnimatorListenerAdapter() {
@@ -377,7 +369,7 @@
         boolean animate = mNotificationStackScroller.isAddOrRemoveAnimationPending();
         int stackScrollerPadding;
         if (mStatusBarState != StatusBarState.KEYGUARD) {
-            stackScrollerPadding = mQsContainer.getHeader().getHeight() + mQsPeekHeight;
+            stackScrollerPadding = (mQs != null ? mQs.getHeader().getHeight() : 0) + mQsPeekHeight;
             mTopPaddingAdjustment = 0;
         } else {
             mClockPositionAlgorithm.setup(
@@ -490,7 +482,8 @@
 
     public void setQsExpansionEnabled(boolean qsExpansionEnabled) {
         mQsExpansionEnabled = qsExpansionEnabled;
-        mQsContainer.setHeaderClickable(qsExpansionEnabled);
+        if (mQs == null) return;
+        mQs.setHeaderClickable(qsExpansionEnabled);
     }
 
     @Override
@@ -571,7 +564,7 @@
 
     @Override
     public boolean onInterceptTouchEvent(MotionEvent event) {
-        if (mBlockTouches || mQsContainer.isCustomizing()) {
+        if (mBlockTouches || mQs.isCustomizing()) {
             return false;
         }
         initDownStates(event);
@@ -731,7 +724,7 @@
 
     @Override
     public boolean onTouchEvent(MotionEvent event) {
-        if (mBlockTouches || mQsContainer.isCustomizing()) {
+        if (mBlockTouches || (mQs != null && mQs.isCustomizing())) {
             return false;
         }
         initDownStates(event);
@@ -804,10 +797,10 @@
     }
 
     private boolean isInQsArea(float x, float y) {
-        return (x >= mQsAutoReinflateContainer.getX()
-                && x <= mQsAutoReinflateContainer.getX() + mQsAutoReinflateContainer.getWidth())
+        return (x >= mQsFrame.getX()
+                && x <= mQsFrame.getX() + mQsFrame.getWidth())
                 && (y <= mNotificationStackScroller.getBottomMostNotificationBottom()
-                || y <= mQsContainer.getY() + mQsContainer.getHeight());
+                || y <= mQs.getView().getY() + mQs.getView().getHeight());
     }
 
     private boolean isOpenQsEvent(MotionEvent event) {
@@ -962,7 +955,8 @@
 
     private void setOverScrolling(boolean overscrolling) {
         mStackScrollerOverscrolling = overscrolling;
-        mQsContainer.setOverscrolling(overscrolling);
+        if (mQs == null) return;
+        mQs.setOverscrolling(overscrolling);
     }
 
     private void onQsExpansionStarted() {
@@ -1000,24 +994,28 @@
 
         mStatusBarState = statusBarState;
         mKeyguardShowing = keyguardShowing;
-        mQsContainer.setKeyguardShowing(mKeyguardShowing);
+        if (mQs != null) {
+            mQs.setKeyguardShowing(mKeyguardShowing);
+        }
 
         if (oldState == StatusBarState.KEYGUARD
                 && (goingToFullShade || statusBarState == StatusBarState.SHADE_LOCKED)) {
             animateKeyguardStatusBarOut();
             long delay = mStatusBarState == StatusBarState.SHADE_LOCKED
                     ? 0 : mStatusBar.calculateGoingToFullShadeDelay();
-            mQsContainer.animateHeaderSlidingIn(delay);
+            mQs.animateHeaderSlidingIn(delay);
         } else if (oldState == StatusBarState.SHADE_LOCKED
                 && statusBarState == StatusBarState.KEYGUARD) {
             animateKeyguardStatusBarIn(StackStateAnimator.ANIMATION_DURATION_STANDARD);
-            mQsContainer.animateHeaderSlidingOut();
+            mQs.animateHeaderSlidingOut();
         } else {
             mKeyguardStatusBar.setAlpha(1f);
             mKeyguardStatusBar.setVisibility(keyguardShowing ? View.VISIBLE : View.INVISIBLE);
             if (keyguardShowing && oldState != mStatusBarState) {
                 mKeyguardBottomArea.onKeyguardShowingChanged();
-                mQsContainer.hideImmediately();
+                if (mQs != null) {
+                    mQs.hideImmediately();
+                }
             }
         }
         if (keyguardShowing) {
@@ -1163,7 +1161,6 @@
     }
 
     private void updateQsState() {
-        mQsContainer.setExpanded(mQsExpanded);
         mNotificationStackScroller.setQsExpanded(mQsExpanded);
         mNotificationStackScroller.setScrollingEnabled(
                 mStatusBarState != StatusBarState.KEYGUARD && (!mQsExpanded
@@ -1176,6 +1173,8 @@
         if (mKeyguardUserSwitcher != null && mQsExpanded && !mStackScrollerOverscrolling) {
             mKeyguardUserSwitcher.hideIfNotSimple(true /* animate */);
         }
+        if (mQs == null) return;
+        mQs.setExpanded(mQsExpanded);
     }
 
     private void setQsExpansion(float height) {
@@ -1222,11 +1221,12 @@
     }
 
     protected void updateQsExpansion() {
-        mQsContainer.setQsExpansion(getQsExpansionFraction(), getHeaderTranslation());
+        if (mQs == null) return;
+        mQs.setQsExpansion(getQsExpansionFraction(), getHeaderTranslation());
     }
 
     private String getKeyguardOrLockScreenString() {
-        if (mQsContainer.isCustomizing()) {
+        if (mQs.isCustomizing()) {
             return getContext().getString(R.string.accessibility_desc_quick_settings_edit);
         } else if (mStatusBarState == StatusBarState.KEYGUARD) {
             return getContext().getString(R.string.accessibility_desc_lock_screen);
@@ -1357,9 +1357,9 @@
         if (!mQsExpansionEnabled || mCollapsedOnDown) {
             return false;
         }
-        View header = mKeyguardShowing ? mKeyguardStatusBar : mQsContainer.getHeader();
-        boolean onHeader = x >= mQsAutoReinflateContainer.getX()
-                && x <= mQsAutoReinflateContainer.getX() + mQsAutoReinflateContainer.getWidth()
+        View header = mKeyguardShowing ? mKeyguardStatusBar : mQs.getHeader();
+        final boolean onHeader = x >= mQsFrame.getX()
+                && x <= mQsFrame.getX() + mQsFrame.getWidth()
                 && y >= header.getTop() && y <= header.getBottom();
         if (mQsExpanded) {
             return onHeader || (yDiff < 0 && isInQsArea(x, y));
@@ -1621,7 +1621,8 @@
         }
         // Since there are QS tiles in the header now, we need to make sure we start listening
         // immediately so they can be up to date.
-        mQsContainer.setHeaderListening(true);
+        if (mQs == null) return;
+        mQs.setHeaderListening(true);
     }
 
     @Override
@@ -1659,8 +1660,9 @@
     }
 
     private void setListening(boolean listening) {
-        mQsContainer.setListening(listening);
         mKeyguardStatusBar.setListening(listening);
+        if (mQs == null) return;
+        mQs.setListening(listening);
     }
 
     @Override
@@ -1748,7 +1750,7 @@
     }
 
     public void onQsHeightChanged() {
-        mQsMaxExpansionHeight = mQsContainer.getDesiredHeight();
+        mQsMaxExpansionHeight = mQs != null ? mQs.getDesiredHeight() : 0;
         if (mQsExpanded && mQsFullyExpanded) {
             mQsExpansionHeight = mQsMaxExpansionHeight;
             requestScrollerTopPaddingUpdate(false /* animate */);
@@ -2018,11 +2020,11 @@
     }
 
     public boolean isQsDetailShowing() {
-        return mQsContainer.isShowingDetail();
+        return mQs.isShowingDetail();
     }
 
     public void closeQsDetail() {
-        mQsContainer.closeDetail();
+        mQs.closeDetail();
     }
 
     @Override
@@ -2110,7 +2112,7 @@
     private final Runnable mUpdateHeader = new Runnable() {
         @Override
         public void run() {
-            mQsContainer.getHeader().updateEverything();
+            mQs.getHeader().updateEverything();
         }
     };
 
@@ -2257,7 +2259,7 @@
 
     protected void setVerticalPanelTranslation(float translation) {
         mNotificationStackScroller.setTranslationX(translation);
-        mQsAutoReinflateContainer.setTranslationX(translation);
+        mQsFrame.setTranslationX(translation);
     }
 
     protected void updateExpandedHeight(float expandedHeight) {
@@ -2355,4 +2357,38 @@
     public void setGroupManager(NotificationGroupManager groupManager) {
         mGroupManager = groupManager;
     }
+
+    private final FragmentListener mFragmentListener = new FragmentListener() {
+        @Override
+        public void onFragmentViewCreated(String tag, Fragment fragment) {
+            mQs = (QS) fragment;
+            mQs.setPanelView(NotificationPanelView.this);
+            mQs.getHeader().getExpandView().setOnClickListener(NotificationPanelView.this);
+            mQs.setHeaderClickable(mQsExpansionEnabled);
+            mQs.setKeyguardShowing(mKeyguardShowing);
+            mQs.setOverscrolling(mStackScrollerOverscrolling);
+
+            // recompute internal state when qspanel height changes
+            mQs.getView().addOnLayoutChangeListener(
+                    (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+                        final int height = bottom - top;
+                        final int oldHeight = oldBottom - oldTop;
+                        if (height != oldHeight) {
+                            onQsHeightChanged();
+                        }
+                    });
+            mNotificationStackScroller.setQsContainer((ViewGroup) mQs.getView());
+            updateQsExpansion();
+        }
+
+        @Override
+        public void onFragmentViewDestroyed(String tag, Fragment fragment) {
+            // Manual handling of fragment lifecycle is only required because this bridges
+            // non-fragment and fragment code. Once we are using a fragment for the notification
+            // panel, mQs will not need to be null cause it will be tied to the same lifecycle.
+            if (fragment == mQs) {
+                mQs = null;
+            }
+        }
+    };
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java
index 8b1fcd6..c85584e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.phone;
 
+import android.app.Fragment;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.graphics.Canvas;
@@ -25,18 +26,17 @@
 import android.view.WindowInsets;
 import android.widget.FrameLayout;
 
-import com.android.systemui.AutoReinflateContainer;
 import com.android.systemui.R;
-import com.android.systemui.plugins.qs.QSContainer;
+import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.plugins.qs.QS;
 
 /**
  * The container with notification stack scroller and quick settings inside.
  */
 public class NotificationsQuickSettingsContainer extends FrameLayout
-        implements ViewStub.OnInflateListener, AutoReinflateContainer.InflateListener {
+        implements ViewStub.OnInflateListener, FragmentHostManager.FragmentListener {
 
-
-    private AutoReinflateContainer mQsContainer;
+    private FrameLayout mQsFrame;
     private View mUserSwitcher;
     private View mStackScroller;
     private View mKeyguardStatusBar;
@@ -54,8 +54,7 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
-        mQsContainer = (AutoReinflateContainer) findViewById(R.id.qs_auto_reinflate_container);
-        mQsContainer.addInflateListener(this);
+        mQsFrame = (FrameLayout) findViewById(R.id.qs_frame);
         mStackScroller = findViewById(R.id.notification_stack_scroller);
         mStackScrollerMargin = ((LayoutParams) mStackScroller.getLayoutParams()).bottomMargin;
         mKeyguardStatusBar = findViewById(R.id.keyguard_header);
@@ -65,9 +64,21 @@
     }
 
     @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        FragmentHostManager.get(this).addTagListener(QS.TAG, this);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        FragmentHostManager.get(this).removeTagListener(QS.TAG, this);
+    }
+
+    @Override
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
-        reloadWidth(mQsContainer);
+        reloadWidth(mQsFrame);
         reloadWidth(mStackScroller);
     }
 
@@ -91,11 +102,11 @@
         boolean statusBarVisible = mKeyguardStatusBar.getVisibility() == View.VISIBLE;
 
         final boolean qsBottom = mQsExpanded && !mCustomizerAnimating;
-        View stackQsTop = qsBottom ? mStackScroller : mQsContainer;
-        View stackQsBottom = !qsBottom ? mStackScroller : mQsContainer;
+        View stackQsTop = qsBottom ? mStackScroller : mQsFrame;
+        View stackQsBottom = !qsBottom ? mStackScroller : mQsFrame;
         // Invert the order of the scroll view and user switcher such that the notifications receive
         // touches first but the panel gets drawn above.
-        if (child == mQsContainer) {
+        if (child == mQsFrame) {
             return super.drawChild(canvas, userSwitcherVisible && statusBarVisible ? mUserSwitcher
                     : statusBarVisible ? mKeyguardStatusBar
                     : userSwitcherVisible ? mUserSwitcher
@@ -129,8 +140,8 @@
     }
 
     @Override
-    public void onInflated(View v) {
-        QSContainer container = (QSContainer) v;
+    public void onFragmentViewCreated(String tag, Fragment fragment) {
+        QS container = (QS) fragment;
         container.setContainer(this);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
index 2f3631c..31a93f0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -90,7 +90,6 @@
 import android.os.UserManager;
 import android.os.Vibrator;
 import android.provider.Settings;
-import android.service.notification.NotificationListenerService;
 import android.service.notification.NotificationListenerService.RankingMap;
 import android.service.notification.StatusBarNotification;
 import android.telecom.TelecomManager;
@@ -126,8 +125,6 @@
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.KeyguardUpdateMonitorCallback;
 import com.android.keyguard.ViewMediatorCallback;
-import com.android.systemui.AutoReinflateContainer;
-import com.android.systemui.AutoReinflateContainer.InflateListener;
 import com.android.systemui.BatteryMeterView;
 import com.android.systemui.DemoMode;
 import com.android.systemui.EventLogConstants;
@@ -141,11 +138,13 @@
 import com.android.systemui.classifier.FalsingManager;
 import com.android.systemui.doze.DozeHost;
 import com.android.systemui.doze.DozeLog;
+import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.fragments.PluginFragmentListener;
 import com.android.systemui.keyguard.KeyguardViewMediator;
-import com.android.systemui.plugins.qs.QSContainer.ActivityStarter;
-import com.android.systemui.plugins.qs.QSContainer.BaseStatusBarHeader;
-import com.android.systemui.plugins.qs.QSContainer;
-import com.android.systemui.qs.QSContainerImpl;
+import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.plugins.qs.QS.ActivityStarter;
+import com.android.systemui.plugins.qs.QS.BaseStatusBarHeader;
+import com.android.systemui.qs.QSFragment;
 import com.android.systemui.qs.QSPanel;
 import com.android.systemui.recents.ScreenPinningRequest;
 import com.android.systemui.recents.events.EventBus;
@@ -561,6 +560,7 @@
     private int mLastCameraLaunchSource;
     private PowerManager.WakeLock mGestureWakeLock;
     private Vibrator mVibrator;
+    private long[] mCameraLaunchGestureVibePattern;
 
     // Fingerprint (as computed by getLoggingFingerprint() of the last logged state.
     private int mLastLoggedStateFingerprint;
@@ -874,7 +874,7 @@
         mLocationController = new LocationControllerImpl(mContext,
                 mHandlerThread.getLooper()); // will post a notification
         mBatteryController = createBatteryController();
-        mBatteryController.addStateChangedCallback(new BatteryStateChangeCallback() {
+        mBatteryController.addCallback(new BatteryStateChangeCallback() {
             @Override
             public void onPowerSaveChanged(boolean isPowerSave) {
                 mHandler.post(mCheckBarModes);
@@ -922,9 +922,11 @@
         }
 
         // Set up the quick settings tile panel
-        AutoReinflateContainer container = (AutoReinflateContainer) mStatusBarWindow.findViewById(
-                R.id.qs_auto_reinflate_container);
+        View container = mStatusBarWindow.findViewById(R.id.qs_frame);
         if (container != null) {
+            FragmentHostManager fragmentHostManager = FragmentHostManager.get(container);
+            new PluginFragmentListener(container, QS.TAG, R.id.qs_frame, QSFragment.class, QS.class)
+                    .startListening(QS.ACTION, QS.VERSION);
             final QSTileHost qsh = SystemUIFactory.getInstance().createQSTileHost(mContext, this,
                     mBluetoothController, mLocationController, mRotationLockController,
                     mNetworkController, mZenModeController, mHotspotController,
@@ -933,21 +935,17 @@
                     mSecurityController, mBatteryController, mIconController,
                     mNextAlarmController);
             mBrightnessMirrorController = new BrightnessMirrorController(mStatusBarWindow);
-            container.addInflateListener(new InflateListener() {
-                @Override
-                public void onInflated(View v) {
-                    QSContainer qsContainer = (QSContainer) v.findViewById(
-                            R.id.quick_settings_container);
-                    if (qsContainer instanceof QSContainerImpl) {
-                        ((QSContainerImpl) qsContainer).setHost(qsh);
-                        mQSPanel = ((QSContainerImpl) qsContainer).getQsPanel();
-                        mQSPanel.setBrightnessMirror(mBrightnessMirrorController);
-                        mKeyguardStatusBar.setQSPanel(mQSPanel);
-                    }
-                    mHeader = qsContainer.getHeader();
-                    initSignalCluster(mHeader);
-                    mHeader.setActivityStarter(PhoneStatusBar.this);
+            fragmentHostManager.addTagListener(QS.TAG, (tag, f) -> {
+                QS qs = (QS) f;
+                if (qs instanceof QSFragment) {
+                    ((QSFragment) qs).setHost(qsh);
+                    mQSPanel = ((QSFragment) qs).getQsPanel();
+                    mQSPanel.setBrightnessMirror(mBrightnessMirrorController);
+                    mKeyguardStatusBar.setQSPanel(mQSPanel);
                 }
+                mHeader = qs.getHeader();
+                initSignalCluster(mHeader);
+                mHeader.setActivityStarter(PhoneStatusBar.this);
             });
         }
 
@@ -996,6 +994,12 @@
         mGestureWakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK,
                 "GestureWakeLock");
         mVibrator = mContext.getSystemService(Vibrator.class);
+        int[] pattern = mContext.getResources().getIntArray(
+                R.array.config_cameraLaunchGestureVibePattern);
+        mCameraLaunchGestureVibePattern = new long[pattern.length];
+        for (int i = 0; i < pattern.length; i++) {
+            mCameraLaunchGestureVibePattern[i] = pattern[i];
+        }
 
         // receive broadcasts
         IntentFilter filter = new IntentFilter();
@@ -1030,7 +1034,7 @@
             if (emergencyViewStub != null) {
                 ((ViewStub) emergencyViewStub).inflate();
             }
-            mNetworkController.addSignalCallback(new NetworkController.SignalCallback() {
+            mNetworkController.addCallback(new NetworkController.SignalCallback() {
                 @Override
                 public void setIsAirplaneMode(NetworkController.IconState icon) {
                     recomputeDisableFlags(true /* animate */);
@@ -3978,9 +3982,9 @@
                 (SignalClusterView) mKeyguardStatusBar.findViewById(R.id.signal_cluster);
         final SignalClusterView signalClusterQs =
                 (SignalClusterView) mHeader.findViewById(R.id.signal_cluster);
-        mNetworkController.removeSignalCallback(signalCluster);
-        mNetworkController.removeSignalCallback(signalClusterKeyguard);
-        mNetworkController.removeSignalCallback(signalClusterQs);
+        mNetworkController.removeCallback(signalCluster);
+        mNetworkController.removeCallback(signalClusterKeyguard);
+        mNetworkController.removeCallback(signalClusterQs);
         if (mQSPanel != null && mQSPanel.getHost() != null) {
             mQSPanel.getHost().destroy();
         }
@@ -4880,7 +4884,7 @@
 
     private void vibrateForCameraGesture() {
         // Make sure to pass -1 for repeat so VibratorService doesn't stop us when going to sleep.
-        mVibrator.vibrate(new long[]{0, 400}, -1 /* repeat */);
+        mVibrator.vibrate(mCameraLaunchGestureVibePattern, -1 /* repeat */);
     }
 
     public void onScreenTurnedOn() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
index 7187ec2..032c86b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
@@ -47,7 +47,6 @@
 import com.android.systemui.statusbar.policy.DataSaverController;
 import com.android.systemui.statusbar.policy.HotspotController;
 import com.android.systemui.statusbar.policy.NextAlarmController;
-import com.android.systemui.statusbar.policy.NextAlarmController.NextAlarmChangeCallback;
 import com.android.systemui.statusbar.policy.RotationLockController;
 import com.android.systemui.statusbar.policy.UserInfoController;
 
@@ -110,7 +109,7 @@
         mCast = cast;
         mHotspot = hotspot;
         mBluetooth = bluetooth;
-        mBluetooth.addStateChangedCallback(this);
+        mBluetooth.addCallback(this);
         mNextAlarm = nextAlarm;
         mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
         mUserInfoController = userInfoController;
@@ -131,7 +130,7 @@
         mSlotHeadset = context.getString(com.android.internal.R.string.status_bar_headset);
         mSlotDataSaver = context.getString(com.android.internal.R.string.status_bar_data_saver);
 
-        mRotationLockController.addRotationLockControllerCallback(this);
+        mRotationLockController.addCallback(this);
 
         // listen for broadcasts
         IntentFilter filter = new IntentFilter();
@@ -162,7 +161,7 @@
         // Alarm clock
         mIconController.setIcon(mSlotAlarmClock, R.drawable.stat_sys_alarm, null);
         mIconController.setIconVisibility(mSlotAlarmClock, false);
-        mNextAlarm.addStateChangedCallback(mNextAlarmCallback);
+        mNextAlarm.addCallback(mNextAlarmCallback);
 
         // zen
         mIconController.setIcon(mSlotZen, R.drawable.stat_sys_zen_important, null);
@@ -193,7 +192,7 @@
         mIconController.setIcon(mSlotDataSaver, R.drawable.stat_sys_data_saver,
                 context.getString(R.string.accessibility_data_saver_on));
         mIconController.setIconVisibility(mSlotDataSaver, false);
-        mDataSaver.addListener(this);
+        mDataSaver.addCallback(this);
     }
 
     public void setStatusBarKeyguardViewManager(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java
index da698d8..fc15477 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java
@@ -113,6 +113,7 @@
     private final AutoTileManager mAutoTiles;
     private final ManagedProfileController mProfileController;
     private final NextAlarmController mNextAlarmController;
+    private final HandlerThread mHandlerThread;
     private View mHeader;
     private int mCurrentUser;
 
@@ -144,10 +145,10 @@
         mNextAlarmController = nextAlarmController;
         mProfileController = new ManagedProfileController(this);
 
-        final HandlerThread ht = new HandlerThread(QSTileHost.class.getSimpleName(),
+        mHandlerThread = new HandlerThread(QSTileHost.class.getSimpleName(),
                 Process.THREAD_PRIORITY_BACKGROUND);
-        ht.start();
-        mLooper = ht.getLooper();
+        mHandlerThread.start();
+        mLooper = mHandlerThread.getLooper();
 
         mServices = new TileServices(this, mLooper);
 
@@ -169,8 +170,11 @@
     }
 
     public void destroy() {
+        mHandlerThread.quitSafely();
+        mTiles.values().forEach(tile -> tile.destroy());
         mAutoTiles.destroy();
         TunerService.get(mContext).removeTunable(this);
+        mServices.destroy();
     }
 
     @Override
@@ -321,12 +325,11 @@
         final List<String> tileSpecs = loadTileSpecs(mContext, newValue);
         int currentUser = ActivityManager.getCurrentUser();
         if (tileSpecs.equals(mTileSpecs) && currentUser == mCurrentUser) return;
-        for (Map.Entry<String, QSTile<?>> tile : mTiles.entrySet()) {
-            if (!tileSpecs.contains(tile.getKey())) {
-                if (DEBUG) Log.d(TAG, "Destroying tile: " + tile.getKey());
-                tile.getValue().destroy();
-            }
-        }
+        mTiles.entrySet().stream().filter(tile -> !tileSpecs.contains(tile.getKey())).forEach(
+                tile -> {
+                    if (DEBUG) Log.d(TAG, "Destroying tile: " + tile.getKey());
+                    tile.getValue().destroy();
+                });
         final LinkedHashMap<String, QSTile<?>> newTiles = new LinkedHashMap<>();
         for (String tileSpec : tileSpecs) {
             QSTile<?> tile = mTiles.get(tileSpec);
@@ -342,9 +345,13 @@
                 if (DEBUG) Log.d(TAG, "Creating tile: " + tileSpec);
                 try {
                     tile = createTile(tileSpec);
-                    if (tile != null && tile.isAvailable()) {
-                        tile.setTileSpec(tileSpec);
-                        newTiles.put(tileSpec, tile);
+                    if (tile != null) {
+                        if (tile.isAvailable()) {
+                            tile.setTileSpec(tileSpec);
+                            newTiles.put(tileSpec, tile);
+                        } else {
+                            tile.destroy();
+                        }
                     }
                 } catch (Throwable t) {
                     Log.w(TAG, "Error creating tile for spec: " + tileSpec, t);
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 65c6347..28aed87 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStatusBarHeader.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStatusBarHeader.java
@@ -37,10 +37,10 @@
 import com.android.keyguard.KeyguardStatusView;
 import com.android.systemui.FontSizeUtils;
 import com.android.systemui.R;
-import com.android.systemui.plugins.qs.QSContainer.ActivityStarter;
-import com.android.systemui.plugins.qs.QSContainer.BaseStatusBarHeader;
+import com.android.systemui.plugins.qs.QS.ActivityStarter;
+import com.android.systemui.plugins.qs.QS.BaseStatusBarHeader;
 import com.android.systemui.qs.QSPanel;
-import com.android.systemui.plugins.qs.QSContainer.Callback;
+import com.android.systemui.plugins.qs.QS.Callback;
 import com.android.systemui.qs.QuickQSPanel;
 import com.android.systemui.qs.TouchAnimator;
 import com.android.systemui.qs.TouchAnimator.Builder;
@@ -238,7 +238,7 @@
     @Override
     protected void onDetachedFromWindow() {
         setListening(false);
-        mHost.getUserInfoController().remListener(this);
+        mHost.getUserInfoController().removeCallback(this);
         mHost.getNetworkController().removeEmergencyListener(this);
         super.onDetachedFromWindow();
     }
@@ -290,9 +290,9 @@
 
     private void updateListeners() {
         if (mListening) {
-            mNextAlarmController.addStateChangedCallback(this);
+            mNextAlarmController.addCallback(this);
         } else {
-            mNextAlarmController.removeStateChangedCallback(this);
+            mNextAlarmController.removeCallback(this);
         }
     }
 
@@ -368,7 +368,7 @@
     }
 
     public void setUserInfoController(UserInfoController userInfoController) {
-        userInfoController.addListener(this);
+        userInfoController.addCallback(this);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
index 559436b..19dcf03 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
@@ -17,11 +17,13 @@
 package com.android.systemui.statusbar.policy;
 
 import com.android.systemui.DemoMode;
+import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 
-public interface BatteryController extends DemoMode {
+public interface BatteryController extends DemoMode,
+        CallbackController<BatteryStateChangeCallback> {
     /**
      * Prints the current state of the {@link BatteryController} to the given {@link PrintWriter}.
      */
@@ -37,9 +39,6 @@
      */
     boolean isPowerSave();
 
-    void addStateChangedCallback(BatteryStateChangeCallback cb);
-    void removeStateChangedCallback(BatteryStateChangeCallback cb);
-
     /**
      * A listener that will be notified whenever a change in battery level or power save mode
      * has occurred.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
index 6726c92..fc86ac3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
@@ -89,7 +89,7 @@
     }
 
     @Override
-    public void addStateChangedCallback(BatteryController.BatteryStateChangeCallback cb) {
+    public void addCallback(BatteryController.BatteryStateChangeCallback cb) {
         synchronized (mChangeCallbacks) {
             mChangeCallbacks.add(cb);
         }
@@ -99,7 +99,7 @@
     }
 
     @Override
-    public void removeStateChangedCallback(BatteryController.BatteryStateChangeCallback cb) {
+    public void removeCallback(BatteryController.BatteryStateChangeCallback cb) {
         synchronized (mChangeCallbacks) {
             mChangeCallbacks.remove(cb);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothController.java
index 08675c4..4c1c378 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothController.java
@@ -17,13 +17,11 @@
 package com.android.systemui.statusbar.policy;
 
 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.systemui.statusbar.policy.BluetoothController.Callback;
 
 import java.util.Collection;
 
-public interface BluetoothController {
-    void addStateChangedCallback(Callback callback);
-    void removeStateChangedCallback(Callback callback);
-
+public interface BluetoothController extends CallbackController<Callback> {
     boolean isBluetoothSupported();
     boolean isBluetoothEnabled();
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
index 4f880b4..15c4afe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
@@ -105,13 +105,13 @@
     }
 
     @Override
-    public void addStateChangedCallback(Callback cb) {
+    public void addCallback(Callback cb) {
         mHandler.obtainMessage(H.MSG_ADD_CALLBACK, cb).sendToTarget();
         mHandler.sendEmptyMessage(H.MSG_STATE_CHANGED);
     }
 
     @Override
-    public void removeStateChangedCallback(Callback cb) {
+    public void removeCallback(Callback cb) {
         mHandler.obtainMessage(H.MSG_REMOVE_CALLBACK, cb).sendToTarget();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CallbackController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CallbackController.java
new file mode 100644
index 0000000..9042ca6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CallbackController.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy;
+
+public interface CallbackController<T> {
+    void addCallback(T listener);
+    void removeCallback(T listener);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastController.java
index 7713e57..6988af7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastController.java
@@ -16,11 +16,11 @@
 
 package com.android.systemui.statusbar.policy;
 
+import com.android.systemui.statusbar.policy.CastController.Callback;
+
 import java.util.Set;
 
-public interface CastController {
-    void addCallback(Callback callback);
-    void removeCallback(Callback callback);
+public interface CastController extends CallbackController<Callback> {
     void setDiscovering(boolean request);
     void setCurrentUserId(int currentUserId);
     Set<CastDevice> getCastDevices();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DataSaverController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DataSaverController.java
index 0fc71d3..e5f1e68 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DataSaverController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DataSaverController.java
@@ -21,9 +21,11 @@
 import android.os.Looper;
 import android.os.RemoteException;
 
+import com.android.systemui.statusbar.policy.DataSaverController.Listener;
+
 import java.util.ArrayList;
 
-public class DataSaverController {
+public class DataSaverController implements CallbackController<Listener> {
 
     private final Handler mHandler = new Handler(Looper.getMainLooper());
     private final ArrayList<Listener> mListeners = new ArrayList<>();
@@ -41,7 +43,7 @@
         }
     }
 
-    public void addListener(Listener listener) {
+    public void addCallback(Listener listener) {
         synchronized (mListeners) {
             mListeners.add(listener);
             if (mListeners.size() == 1) {
@@ -51,7 +53,7 @@
         listener.onDataSaverChanged(isDataSaverEnabled());
     }
 
-    public void remListener(Listener listener) {
+    public void removeCallback(Listener listener) {
         synchronized (mListeners) {
             mListeners.remove(listener);
             if (mListeners.size() == 0) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java
index 4e9fc76..0f77b03 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java
@@ -27,6 +27,8 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import com.android.systemui.statusbar.policy.FlashlightController.FlashlightListener;
+
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
@@ -35,7 +37,7 @@
 /**
  * Manages the flashlight.
  */
-public class FlashlightController {
+public class FlashlightController implements CallbackController<FlashlightListener> {
 
     private static final String TAG = "FlashlightController";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@@ -112,7 +114,7 @@
         return mTorchAvailable;
     }
 
-    public void addListener(FlashlightListener l) {
+    public void addCallback(FlashlightListener l) {
         synchronized (mListeners) {
             if (mCameraId == null) {
                 tryInitCamera();
@@ -122,7 +124,7 @@
         }
     }
 
-    public void removeListener(FlashlightListener l) {
+    public void removeCallback(FlashlightListener l) {
         synchronized (mListeners) {
             cleanUpListenersLocked(l);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotController.java
index 4622ea4..daf9d6b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotController.java
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.policy;
 
-public interface HotspotController {
-    void addCallback(Callback callback);
-    void removeCallback(Callback callback);
+import com.android.systemui.statusbar.policy.HotspotController.Callback;
+
+public interface HotspotController extends CallbackController<Callback> {
     boolean isHotspotEnabled();
     void setHotspotEnabled(boolean enabled);
     boolean isHotspotSupported();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardMonitor.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardMonitor.java
index 44816f9..fafbdd1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardMonitor.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardMonitor.java
@@ -24,10 +24,12 @@
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.KeyguardUpdateMonitorCallback;
 import com.android.systemui.settings.CurrentUserTracker;
+import com.android.systemui.statusbar.policy.KeyguardMonitor.Callback;
 
 import java.util.ArrayList;
 
-public final class KeyguardMonitor extends KeyguardUpdateMonitorCallback {
+public class KeyguardMonitor extends KeyguardUpdateMonitorCallback
+        implements CallbackController<Callback> {
 
     private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>();
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationController.java
index 29a8981..9a5f1b8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationController.java
@@ -16,11 +16,11 @@
 
 package com.android.systemui.statusbar.policy;
 
-public interface LocationController {
+import com.android.systemui.statusbar.policy.LocationController.LocationSettingsChangeCallback;
+
+public interface LocationController extends CallbackController<LocationSettingsChangeCallback> {
     boolean isLocationEnabled();
     boolean setLocationEnabled(boolean enabled);
-    void addSettingsChangedCallback(LocationSettingsChangeCallback cb);
-    void removeSettingsChangedCallback(LocationSettingsChangeCallback cb);
 
     /**
      * A callback for change in location settings (the user has enabled/disabled location).
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java
index 8d84be4..cc61605 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java
@@ -82,12 +82,12 @@
     /**
      * Add a callback to listen for changes in location settings.
      */
-    public void addSettingsChangedCallback(LocationSettingsChangeCallback cb) {
+    public void addCallback(LocationSettingsChangeCallback cb) {
         mSettingsChangeCallbacks.add(cb);
         mHandler.sendEmptyMessage(H.MSG_LOCATION_SETTINGS_CHANGED);
     }
 
-    public void removeSettingsChangedCallback(LocationSettingsChangeCallback cb) {
+    public void removeCallback(LocationSettingsChangeCallback cb) {
         mSettingsChangeCallbacks.remove(cb);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java
index 5f1b871..082fe82 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java
@@ -21,14 +21,15 @@
 import android.telephony.SubscriptionInfo;
 import com.android.settingslib.net.DataUsageController;
 import com.android.settingslib.wifi.AccessPoint;
+import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
 
 import java.util.List;
 
-public interface NetworkController {
+public interface NetworkController extends CallbackController<SignalCallback> {
 
     boolean hasMobileDataFeature();
-    void addSignalCallback(SignalCallback cb);
-    void removeSignalCallback(SignalCallback cb);
+    void addCallback(SignalCallback cb);
+    void removeCallback(SignalCallback cb);
     void setWifiEnabled(boolean enabled);
     void onUserSwitched(int newUserId);
     AccessPointController getAccessPointController();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
index 37e6a2a..1a9756f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
@@ -322,7 +322,7 @@
         mCallbackHandler.setEmergencyCallsOnly(mIsEmergency);
     }
 
-    public void addSignalCallback(SignalCallback cb) {
+    public void addCallback(SignalCallback cb) {
         cb.setSubs(mCurrentSubscriptions);
         cb.setIsAirplaneMode(new IconState(mAirplaneMode,
                 TelephonyIcons.FLIGHT_MODE_ICON, R.string.accessibility_airplane_mode, mContext));
@@ -336,7 +336,7 @@
     }
 
     @Override
-    public void removeSignalCallback(SignalCallback cb) {
+    public void removeCallback(SignalCallback cb) {
         mCallbackHandler.setListening(cb, false);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmController.java
index 787acc5..28935bf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmController.java
@@ -23,11 +23,14 @@
 import android.content.IntentFilter;
 import android.os.UserHandle;
 
+import com.android.systemui.statusbar.policy.NextAlarmController.NextAlarmChangeCallback;
+
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.ArrayList;
 
-public class NextAlarmController extends BroadcastReceiver {
+public class NextAlarmController extends BroadcastReceiver
+        implements CallbackController<NextAlarmChangeCallback> {
 
     private final ArrayList<NextAlarmChangeCallback> mChangeCallbacks = new ArrayList<>();
 
@@ -48,12 +51,12 @@
         pw.print("  mNextAlarm="); pw.println(mNextAlarm);
     }
 
-    public void addStateChangedCallback(NextAlarmChangeCallback cb) {
+    public void addCallback(NextAlarmChangeCallback cb) {
         mChangeCallbacks.add(cb);
         cb.onNextAlarmChanged(mNextAlarm);
     }
 
-    public void removeStateChangedCallback(NextAlarmChangeCallback cb) {
+    public void removeCallback(NextAlarmChangeCallback cb) {
         mChangeCallbacks.remove(cb);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockController.java
index 93c4691..722874b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockController.java
@@ -16,13 +16,14 @@
 
 package com.android.systemui.statusbar.policy;
 
-public interface RotationLockController extends Listenable {
+import com.android.systemui.statusbar.policy.RotationLockController.RotationLockControllerCallback;
+
+public interface RotationLockController extends Listenable,
+        CallbackController<RotationLockControllerCallback> {
     int getRotationLockOrientation();
     boolean isRotationLockAffordanceVisible();
     boolean isRotationLocked();
     void setRotationLocked(boolean locked);
-    void addRotationLockControllerCallback(RotationLockControllerCallback callback);
-    void removeRotationLockControllerCallback(RotationLockControllerCallback callback);
 
     public interface RotationLockControllerCallback {
         void onRotationLockStateChanged(boolean rotationLocked, boolean affordanceVisible);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java
index c3bcd94..4f96496 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java
@@ -42,12 +42,12 @@
         setListening(true);
     }
 
-    public void addRotationLockControllerCallback(RotationLockControllerCallback callback) {
+    public void addCallback(RotationLockControllerCallback callback) {
         mCallbacks.add(callback);
         notifyChanged(callback);
     }
 
-    public void removeRotationLockControllerCallback(RotationLockControllerCallback callback) {
+    public void removeCallback(RotationLockControllerCallback callback) {
         mCallbacks.remove(callback);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java
index 014afae..43ced48 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java
@@ -15,7 +15,9 @@
  */
 package com.android.systemui.statusbar.policy;
 
-public interface SecurityController {
+import com.android.systemui.statusbar.policy.SecurityController.SecurityControllerCallback;
+
+public interface SecurityController extends CallbackController<SecurityControllerCallback> {
     /** Whether the device has device owner, even if not on this user. */
     boolean isDeviceManaged();
     boolean hasProfileOwner();
@@ -29,9 +31,6 @@
     String getProfileVpnName();
     void onUserSwitched(int newUserId);
 
-    void addCallback(SecurityControllerCallback callback);
-    void removeCallback(SecurityControllerCallback callback);
-
     public interface SecurityControllerCallback {
         void onStateChanged();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoController.java
index 4a6e215..17b22df 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoController.java
@@ -16,7 +16,6 @@
 
 package com.android.systemui.statusbar.policy;
 
-import android.app.ActivityManager;
 import android.app.ActivityManagerNative;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -38,10 +37,11 @@
 import com.android.internal.util.UserIcons;
 import com.android.settingslib.drawable.UserIconDrawable;
 import com.android.systemui.R;
+import com.android.systemui.statusbar.policy.UserInfoController.OnUserInfoChangedListener;
 
 import java.util.ArrayList;
 
-public final class UserInfoController {
+public class UserInfoController implements CallbackController<OnUserInfoChangedListener> {
 
     private static final String TAG = "UserInfoController";
 
@@ -67,12 +67,12 @@
                 null, null);
     }
 
-    public void addListener(OnUserInfoChangedListener callback) {
+    public void addCallback(OnUserInfoChangedListener callback) {
         mCallbacks.add(callback);
         callback.onUserInfoChanged(mUserName, mUserDrawable, mUserAccount);
     }
 
-    public void remListener(OnUserInfoChangedListener callback) {
+    public void removeCallback(OnUserInfoChangedListener callback) {
         mCallbacks.remove(callback);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
index eda46c5..9d9c908 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
@@ -49,15 +49,16 @@
 import android.widget.BaseAdapter;
 
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.UserIcons;
 import com.android.settingslib.RestrictedLockUtils;
 import com.android.systemui.GuestResumeSessionReceiver;
 import com.android.systemui.R;
 import com.android.systemui.SystemUI;
 import com.android.systemui.SystemUISecondaryUserService;
-import com.android.systemui.plugins.qs.QSContainer.DetailAdapter;
+import com.android.systemui.plugins.qs.QS.DetailAdapter;
 import com.android.systemui.qs.tiles.UserDetailView;
-import com.android.systemui.plugins.qs.QSContainer.ActivityStarter;
+import com.android.systemui.plugins.qs.QS.ActivityStarter;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
 
 import java.io.FileDescriptor;
@@ -640,28 +641,43 @@
         refreshUsers(UserHandle.USER_ALL);
     }
 
+    @VisibleForTesting
+    public void addAdapter(WeakReference<BaseUserAdapter> adapter) {
+        mAdapters.add(adapter);
+    }
+
+    @VisibleForTesting
+    public KeyguardMonitor getKeyguardMonitor() {
+        return mKeyguardMonitor;
+    }
+
+    @VisibleForTesting
+    public ArrayList<UserRecord> getUsers() {
+        return mUsers;
+    }
+
     public static abstract class BaseUserAdapter extends BaseAdapter {
 
         final UserSwitcherController mController;
 
         protected BaseUserAdapter(UserSwitcherController controller) {
             mController = controller;
-            controller.mAdapters.add(new WeakReference<>(this));
+            controller.addAdapter(new WeakReference<>(this));
         }
 
         @Override
         public int getCount() {
-            boolean secureKeyguardShowing = mController.mKeyguardMonitor.isShowing()
-                    && mController.mKeyguardMonitor.isSecure()
-                    && !mController.mKeyguardMonitor.canSkipBouncer();
+            boolean secureKeyguardShowing = mController.getKeyguardMonitor().isShowing()
+                    && mController.getKeyguardMonitor().isSecure()
+                    && !mController.getKeyguardMonitor().canSkipBouncer();
             if (!secureKeyguardShowing) {
-                return mController.mUsers.size();
+                return mController.getUsers().size();
             }
             // The lock screen is secure and showing. Filter out restricted records.
-            final int N = mController.mUsers.size();
+            final int N = mController.getUsers().size();
             int count = 0;
             for (int i = 0; i < N; i++) {
-                if (mController.mUsers.get(i).isRestricted) {
+                if (mController.getUsers().get(i).isRestricted) {
                     break;
                 } else {
                     count++;
@@ -672,7 +688,7 @@
 
         @Override
         public UserRecord getItem(int position) {
-            return mController.mUsers.get(position);
+            return mController.getUsers().get(position);
         }
 
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeController.java
index 0e91b0b..bcdb62d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeController.java
@@ -22,9 +22,9 @@
 import android.service.notification.ZenModeConfig;
 import android.service.notification.ZenModeConfig.ZenRule;
 
-public interface ZenModeController {
-    void addCallback(Callback callback);
-    void removeCallback(Callback callback);
+import com.android.systemui.statusbar.policy.ZenModeController.Callback;
+
+public interface ZenModeController extends CallbackController<Callback> {
     void setZen(int zen, Uri conditionId, String reason);
     int getZen();
     ZenRule getManualRule();
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/ThemePreference.java b/packages/SystemUI/src/com/android/systemui/tuner/ThemePreference.java
new file mode 100644
index 0000000..e5bb3d5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/tuner/ThemePreference.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.tuner;
+
+import android.app.AlertDialog;
+import android.app.UiModeManager;
+import android.content.Context;
+import android.os.SystemProperties;
+import android.support.v7.preference.ListPreference;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+
+import com.android.systemui.R;
+
+import libcore.util.Objects;
+
+import com.google.android.collect.Lists;
+
+import java.io.File;
+import java.util.ArrayList;
+
+public class ThemePreference extends ListPreference {
+
+    public ThemePreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    public void onAttached() {
+        super.onAttached();
+        File file = new File("/vendor/overlay");
+        ArrayList<String> options = Lists.newArrayList(file.list());
+        String def = SystemProperties.get("ro.boot.vendor.overlay.theme");
+        if (TextUtils.isEmpty(def)) {
+            def = getContext().getString(R.string.default_theme);
+        }
+        if (!options.contains(def)) {
+            options.add(0, def);
+        }
+        String[] list = options.toArray(new String[options.size()]);
+        setVisible(options.size() > 1);
+        setEntries(list);
+        setEntryValues(list);
+        updateValue();
+    }
+
+    private void updateValue() {
+        setValue(getContext().getSystemService(UiModeManager.class).getTheme());
+    }
+
+    @Override
+    protected void notifyChanged() {
+        super.notifyChanged();
+        if (!Objects.equal(getValue(),
+                getContext().getSystemService(UiModeManager.class).getTheme())) {
+            new AlertDialog.Builder(getContext())
+                    .setTitle(R.string.change_theme_reboot)
+                    .setPositiveButton(com.android.internal.R.string.global_action_restart, (d, i)
+                            -> getContext().getSystemService(UiModeManager.class)
+                            .setTheme(getValue()))
+                    .setNegativeButton(android.R.string.cancel, (d, i) -> updateValue())
+                    .show();
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml
index b5584f3..6e17cf4 100644
--- a/packages/SystemUI/tests/AndroidManifest.xml
+++ b/packages/SystemUI/tests/AndroidManifest.xml
@@ -25,6 +25,7 @@
     <uses-permission android:name="android.permission.MANAGE_USERS" />
     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
     <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+    <uses-permission android:name="android.permission.BIND_QUICK_SETTINGS_TILE" />
 
     <application>
         <uses-library android:name="android.test.runner" />
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaTest.java
index fccb2a2..f3be945 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaTest.java
@@ -16,26 +16,24 @@
 
 package com.android.keyguard;
 
+import static junit.framework.Assert.assertEquals;
+
+import static org.mockito.Mockito.mock;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.support.test.runner.AndroidJUnit4;
+import android.test.suitebuilder.annotation.SmallTest;
+
 import com.android.systemui.SysuiTestCase;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import android.content.Context;
-import android.os.Handler;
-import android.os.Looper;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.runner.AndroidJUnit4;
-import android.test.suitebuilder.annotation.SmallTest;
-
-import static junit.framework.Assert.*;
-import static org.mockito.Mockito.mock;
-
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class KeyguardMessageAreaTest extends SysuiTestCase {
-    private Context mContext = InstrumentationRegistry.getTargetContext();
     private Handler mHandler = new Handler(Looper.getMainLooper());
     private KeyguardMessageArea mMessageArea;
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/BatteryMeterDrawableTest.java b/packages/SystemUI/tests/src/com/android/systemui/BatteryMeterDrawableTest.java
index 5cb5e68..cb0f7a3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/BatteryMeterDrawableTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/BatteryMeterDrawableTest.java
@@ -17,7 +17,7 @@
 package com.android.systemui;
 
 import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertTrue;
+
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyFloat;
 import static org.mockito.Mockito.anyString;
@@ -28,27 +28,24 @@
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
-import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Canvas;
-import android.support.test.InstrumentationRegistry;
 import android.support.test.runner.AndroidJUnit4;
 import android.test.suitebuilder.annotation.SmallTest;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class BatteryMeterDrawableTest {
+public class BatteryMeterDrawableTest extends SysuiTestCase {
 
-    private Context mContext;
     private Resources mResources;
     private BatteryMeterDrawable mBatteryMeter;
 
     @Before
     public void setUp() throws Exception {
-        mContext = InstrumentationRegistry.getTargetContext();
         mResources = mContext.getResources();
         mBatteryMeter = new BatteryMeterDrawable(mContext, 0);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/FragmentTestCase.java b/packages/SystemUI/tests/src/com/android/systemui/FragmentTestCase.java
new file mode 100644
index 0000000..f87336c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/FragmentTestCase.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui;
+
+import android.annotation.Nullable;
+import android.app.Fragment;
+import android.app.FragmentController;
+import android.app.FragmentHostCallback;
+import android.app.FragmentManagerNonConfig;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Parcelable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * Base class for fragment class tests.  Just adding one for any fragment will push it through
+ * general lifecycle events and ensure no basic leaks are happening.  This class also implements
+ * the host for subclasses, so they can push it into desired states and do any unit testing
+ * required.
+ */
+public abstract class FragmentTestCase extends LeakCheckedTest {
+
+    private static final int VIEW_ID = 42;
+    private final Class<? extends Fragment> mCls;
+    private HandlerThread mHandlerThread;
+    private Handler mHandler;
+    private FrameLayout mView;
+    protected FragmentController mFragments;
+    protected Fragment mFragment;
+
+    public FragmentTestCase(Class<? extends Fragment> cls) {
+        mCls = cls;
+    }
+
+    @Before
+    public void setupFragment() throws IllegalAccessException, InstantiationException {
+        mView = new FrameLayout(mContext);
+        mView.setId(VIEW_ID);
+        mHandlerThread = new HandlerThread("FragmentTestThread");
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+        mFragment = mCls.newInstance();
+        postAndWait(() -> {
+            mFragments = FragmentController.createController(new HostCallbacks());
+            mFragments.attachHost(null);
+            mFragments.getFragmentManager().beginTransaction()
+                    .replace(VIEW_ID, mFragment)
+                    .commit();
+        });
+    }
+
+    @After
+    public void tearDown() {
+        if (mFragments != null) {
+            // Set mFragments to null to let it know not to destroy.
+            postAndWait(() -> mFragments.dispatchDestroy());
+        }
+        mHandlerThread.quit();
+    }
+
+    @Test
+    public void testCreateDestroy() {
+        postAndWait(() -> mFragments.dispatchCreate());
+        destroyFragments();
+    }
+
+    @Test
+    public void testStartStop() {
+        postAndWait(() -> mFragments.dispatchStart());
+        postAndWait(() -> mFragments.dispatchStop());
+    }
+
+    @Test
+    public void testResumePause() {
+        postAndWait(() -> mFragments.dispatchResume());
+        postAndWait(() -> mFragments.dispatchPause());
+    }
+
+    @Test
+    public void testRecreate() {
+        postAndWait(() -> mFragments.dispatchResume());
+        postAndWait(() -> {
+            mFragments.dispatchPause();
+            Parcelable p = mFragments.saveAllState();
+            mFragments.dispatchDestroy();
+
+            mFragments = FragmentController.createController(new HostCallbacks());
+            mFragments.attachHost(null);
+            mFragments.restoreAllState(p, (FragmentManagerNonConfig) null);
+            mFragments.dispatchResume();
+        });
+    }
+
+    @Test
+    public void testMultipleResumes() {
+        postAndWait(() -> mFragments.dispatchResume());
+        postAndWait(() -> mFragments.dispatchStop());
+        postAndWait(() -> mFragments.dispatchResume());
+    }
+
+    protected void destroyFragments() {
+        postAndWait(() -> mFragments.dispatchDestroy());
+        mFragments = null;
+    }
+
+    protected void postAndWait(Runnable r) {
+        mHandler.post(r);
+        waitForFragments();
+    }
+
+    protected void waitForFragments() {
+        waitForIdleSync(mHandler);
+    }
+
+    private View findViewById(int id) {
+        return mView.findViewById(id);
+    }
+
+    private class HostCallbacks extends FragmentHostCallback<FragmentTestCase> {
+        public HostCallbacks() {
+            super(getTrackedContext(), FragmentTestCase.this.mHandler, 0);
+        }
+
+        @Override
+        public FragmentTestCase onGetHost() {
+            return FragmentTestCase.this;
+        }
+
+        @Override
+        public void onDump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+        }
+
+        @Override
+        public boolean onShouldSaveFragmentState(Fragment fragment) {
+            return true; // True for now.
+        }
+
+        @Override
+        public LayoutInflater onGetLayoutInflater() {
+            return LayoutInflater.from(mContext);
+        }
+
+        @Override
+        public boolean onUseFragmentManagerInflaterFactory() {
+            return true;
+        }
+
+        @Override
+        public boolean onHasWindowAnimations() {
+            return false;
+        }
+
+        @Override
+        public int onGetWindowAnimations() {
+            return 0;
+        }
+
+        @Override
+        public void onAttachFragment(Fragment fragment) {
+        }
+
+        @Nullable
+        @Override
+        public View onFindViewById(int id) {
+            return FragmentTestCase.this.findViewById(id);
+        }
+
+        @Override
+        public boolean onHasView() {
+            return true;
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/LeakCheckedTest.java b/packages/SystemUI/tests/src/com/android/systemui/LeakCheckedTest.java
new file mode 100644
index 0000000..d64669d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/LeakCheckedTest.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentCallbacks;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.systemui.statusbar.phone.ManagedProfileController;
+import com.android.systemui.statusbar.policy.BatteryController;
+import com.android.systemui.statusbar.policy.BluetoothController;
+import com.android.systemui.statusbar.policy.CastController;
+import com.android.systemui.statusbar.policy.DataSaverController;
+import com.android.systemui.statusbar.policy.FlashlightController;
+import com.android.systemui.statusbar.policy.HotspotController;
+import com.android.systemui.statusbar.policy.KeyguardMonitor;
+import com.android.systemui.statusbar.policy.LocationController;
+import com.android.systemui.statusbar.policy.NetworkController;
+import com.android.systemui.statusbar.policy.NextAlarmController;
+import com.android.systemui.statusbar.policy.RotationLockController;
+import com.android.systemui.statusbar.policy.SecurityController;
+import com.android.systemui.statusbar.policy.CallbackController;
+import com.android.systemui.statusbar.policy.UserInfoController;
+import com.android.systemui.statusbar.policy.ZenModeController;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Base class for tests to check if receivers are left registered, services bound, or other
+ * listeners listening.
+ */
+public class LeakCheckedTest extends SysuiTestCase {
+    private static final String TAG = "LeakCheckedTest";
+
+    private final Map<String, Tracker> mTrackers = new HashMap<>();
+    private final Map<Class, Object> mLeakCheckers = new ArrayMap<>();
+    private TrackingContext mTrackedContext;
+
+    @Rule
+    public TestWatcher successWatcher = new TestWatcher() {
+        @Override
+        protected void succeeded(Description description) {
+            verify();
+        }
+    };
+
+    @Before
+    public void setup() {
+        mTrackedContext = new TrackingContext(mContext);
+        addSupportedLeakCheckers();
+    }
+
+    public <T> T getLeakChecker(Class<T> cls) {
+        T obj = (T) mLeakCheckers.get(cls);
+        if (obj == null) {
+            Assert.fail(cls.getName() + " is not supported by LeakCheckedTest yet");
+        }
+        return obj;
+    }
+
+    public Context getTrackedContext() {
+        return mTrackedContext;
+    }
+
+    private Tracker getTracker(String tag) {
+        Tracker t = mTrackers.get(tag);
+        if (t == null) {
+            t = new Tracker();
+            mTrackers.put(tag, t);
+        }
+        return t;
+    }
+
+    public void verify() {
+        mTrackers.values().forEach(Tracker::verify);
+    }
+
+    public static class Tracker {
+        private Map<Object, LeakInfo> mObjects = new ArrayMap<>();
+
+        LeakInfo getLeakInfo(Object object) {
+            LeakInfo leakInfo = mObjects.get(object);
+            if (leakInfo == null) {
+                leakInfo = new LeakInfo();
+                mObjects.put(object, leakInfo);
+            }
+            return leakInfo;
+        }
+
+        private void verify() {
+            mObjects.values().forEach(LeakInfo::verify);
+        }
+    }
+
+    public static class LeakInfo {
+        private List<Throwable> mThrowables = new ArrayList<>();
+
+        private LeakInfo() {
+        }
+
+        private void addAllocation(Throwable t) {
+            // TODO: Drop off the first element in the stack trace here to have a cleaner stack.
+            mThrowables.add(t);
+        }
+
+        private void clearAllocations() {
+            mThrowables.clear();
+        }
+
+        public void verify() {
+            if (mThrowables.size() == 0) return;
+            Log.e(TAG, "Listener or binding not properly released");
+            for (Throwable t : mThrowables) {
+                Log.e(TAG, "Allocation found", t);
+            }
+            StringWriter writer = new StringWriter();
+            mThrowables.get(0).printStackTrace(new PrintWriter(writer));
+            Assert.fail("Listener or binding not properly released\n"
+                    + writer.toString());
+        }
+    }
+
+    private void addSupportedLeakCheckers() {
+        addListening("bluetooth", BluetoothController.class);
+        addListening("location", LocationController.class);
+        addListening("rotation", RotationLockController.class);
+        addListening("zen", ZenModeController.class);
+        addListening("cast", CastController.class);
+        addListening("hotspot", HotspotController.class);
+        addListening("flashlight", FlashlightController.class);
+        addListening("user", UserInfoController.class);
+        addListening("keyguard", KeyguardMonitor.class);
+        addListening("battery", BatteryController.class);
+        addListening("security", SecurityController.class);
+        addListening("profile", ManagedProfileController.class);
+        addListening("alarm", NextAlarmController.class);
+        NetworkController network = addListening("network", NetworkController.class);
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                getTracker("emergency").getLeakInfo(invocation.getArguments()[0])
+                        .addAllocation(new Throwable());
+                return null;
+            }
+        }).when(network).addEmergencyListener(any());
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                getTracker("emergency").getLeakInfo(invocation.getArguments()[0]).clearAllocations();
+                return null;
+            }
+        }).when(network).removeEmergencyListener(any());
+        DataSaverController datasaver = addListening("datasaver", DataSaverController.class);
+        when(network.getDataSaverController()).thenReturn(datasaver);
+    }
+
+    private <T extends CallbackController> T addListening(final String tag, Class<T> cls) {
+        T mock = mock(cls);
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                getTracker(tag).getLeakInfo(invocation.getArguments()[0])
+                        .addAllocation(new Throwable());
+                return null;
+            }
+        }).when(mock).addCallback(any());
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                getTracker(tag).getLeakInfo(invocation.getArguments()[0]).clearAllocations();
+                return null;
+            }
+        }).when(mock).removeCallback(any());
+        mLeakCheckers.put(cls, mock);
+        return mock;
+    }
+
+    class TrackingContext extends ContextWrapper {
+        public TrackingContext(Context base) {
+            super(base);
+        }
+
+        @Override
+        public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
+            getTracker("receiver").getLeakInfo(receiver).addAllocation(new Throwable());
+            return super.registerReceiver(receiver, filter);
+        }
+
+        @Override
+        public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
+                String broadcastPermission, Handler scheduler) {
+            getTracker("receiver").getLeakInfo(receiver).addAllocation(new Throwable());
+            return super.registerReceiver(receiver, filter, broadcastPermission, scheduler);
+        }
+
+        @Override
+        public Intent registerReceiverAsUser(BroadcastReceiver receiver, UserHandle user,
+                IntentFilter filter, String broadcastPermission, Handler scheduler) {
+            getTracker("receiver").getLeakInfo(receiver).addAllocation(new Throwable());
+            return super.registerReceiverAsUser(receiver, user, filter, broadcastPermission,
+                    scheduler);
+        }
+
+        @Override
+        public void unregisterReceiver(BroadcastReceiver receiver) {
+            getTracker("receiver").getLeakInfo(receiver).clearAllocations();
+            super.unregisterReceiver(receiver);
+        }
+
+        @Override
+        public boolean bindService(Intent service, ServiceConnection conn, int flags) {
+            getTracker("service").getLeakInfo(conn).addAllocation(new Throwable());
+            return super.bindService(service, conn, flags);
+        }
+
+        @Override
+        public boolean bindServiceAsUser(Intent service, ServiceConnection conn, int flags,
+                Handler handler, UserHandle user) {
+            getTracker("service").getLeakInfo(conn).addAllocation(new Throwable());
+            return super.bindServiceAsUser(service, conn, flags, handler, user);
+        }
+
+        @Override
+        public boolean bindServiceAsUser(Intent service, ServiceConnection conn, int flags,
+                UserHandle user) {
+            getTracker("service").getLeakInfo(conn).addAllocation(new Throwable());
+            return super.bindServiceAsUser(service, conn, flags, user);
+        }
+
+        @Override
+        public void unbindService(ServiceConnection conn) {
+            getTracker("service").getLeakInfo(conn).clearAllocations();
+            super.unbindService(conn);
+        }
+
+        @Override
+        public void registerComponentCallbacks(ComponentCallbacks callback) {
+            getTracker("component").getLeakInfo(callback).addAllocation(new Throwable());
+            super.registerComponentCallbacks(callback);
+        }
+
+        @Override
+        public void unregisterComponentCallbacks(ComponentCallbacks callback) {
+            getTracker("component").getLeakInfo(callback).clearAllocations();
+            super.unregisterComponentCallbacks(callback);
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java
index d943eb6..5dac8e5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java
@@ -20,6 +20,10 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.MessageQueue;
+
+import com.android.systemui.utils.TestableContext;
+
+import org.junit.After;
 import org.junit.Before;
 
 /**
@@ -28,11 +32,16 @@
 public class SysuiTestCase {
 
     private Handler mHandler;
-    protected Context mContext;
+    protected TestableContext mContext;
 
     @Before
     public void SysuiSetup() throws Exception {
-        mContext = InstrumentationRegistry.getTargetContext();
+        mContext = new TestableContext(InstrumentationRegistry.getTargetContext());
+    }
+
+    @After
+    public void cleanup() throws Exception {
+        mContext.getSettingsProvider().clearOverrides(this);
     }
 
     protected Context getContext() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
index 39b6412..5c87fb0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
@@ -17,9 +17,11 @@
 package com.android.systemui.power;
 
 import static android.test.MoreAsserts.assertNotEqual;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
+
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.anyString;
@@ -29,9 +31,11 @@
 
 import android.app.Notification;
 import android.app.NotificationManager;
-import android.support.test.InstrumentationRegistry;
 import android.support.test.runner.AndroidJUnit4;
 import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -39,7 +43,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class PowerNotificationWarningsTest {
+public class PowerNotificationWarningsTest extends SysuiTestCase {
     private final NotificationManager mMockNotificationManager = mock(NotificationManager.class);
     private PowerNotificationWarnings mPowerNotificationWarnings;
 
@@ -47,7 +51,7 @@
     public void setUp() throws Exception {
         // Test Instance.
         mPowerNotificationWarnings = new PowerNotificationWarnings(
-                InstrumentationRegistry.getTargetContext(), mMockNotificationManager, null);
+                mContext, mMockNotificationManager, null);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
new file mode 100644
index 0000000..6ceaead
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
@@ -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.
+ */
+
+package com.android.systemui.qs;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.os.Handler;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.systemui.FragmentTestCase;
+import com.android.systemui.statusbar.phone.PhoneStatusBar;
+import com.android.systemui.statusbar.phone.QSTileHost;
+import com.android.systemui.statusbar.phone.StatusBarIconController;
+import com.android.systemui.statusbar.policy.BatteryController;
+import com.android.systemui.statusbar.policy.BluetoothController;
+import com.android.systemui.statusbar.policy.CastController;
+import com.android.systemui.statusbar.policy.FlashlightController;
+import com.android.systemui.statusbar.policy.HotspotController;
+import com.android.systemui.statusbar.policy.KeyguardMonitor;
+import com.android.systemui.statusbar.policy.LocationController;
+import com.android.systemui.statusbar.policy.NetworkController;
+import com.android.systemui.statusbar.policy.NextAlarmController;
+import com.android.systemui.statusbar.policy.RotationLockController;
+import com.android.systemui.statusbar.policy.SecurityController;
+import com.android.systemui.statusbar.policy.UserInfoController;
+import com.android.systemui.statusbar.policy.UserSwitcherController;
+import com.android.systemui.statusbar.policy.ZenModeController;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+
+@RunWith(AndroidJUnit4.class)
+public class QSFragmentTest extends FragmentTestCase {
+
+    public QSFragmentTest() {
+        super(QSFragment.class);
+    }
+
+    @Test
+    public void testListening() {
+        QSFragment qs = (QSFragment) mFragment;
+        postAndWait(() -> mFragments.dispatchResume());
+        UserSwitcherController userSwitcher = mock(UserSwitcherController.class);
+        KeyguardMonitor keyguardMonitor = getLeakChecker(KeyguardMonitor.class);
+        when(userSwitcher.getKeyguardMonitor()).thenReturn(keyguardMonitor);
+        when(userSwitcher.getUsers()).thenReturn(new ArrayList<>());
+        QSTileHost host = new QSTileHost(getTrackedContext(),
+                mock(PhoneStatusBar.class),
+                getLeakChecker(BluetoothController.class),
+                getLeakChecker(LocationController.class),
+                getLeakChecker(RotationLockController.class),
+                getLeakChecker(NetworkController.class),
+                getLeakChecker(ZenModeController.class),
+                getLeakChecker(HotspotController.class),
+                getLeakChecker(CastController.class),
+                getLeakChecker(FlashlightController.class),
+                userSwitcher,
+                getLeakChecker(UserInfoController.class),
+                keyguardMonitor,
+                getLeakChecker(SecurityController.class),
+                getLeakChecker(BatteryController.class),
+                mock(StatusBarIconController.class),
+                getLeakChecker(NextAlarmController.class));
+        qs.setHost(host);
+        Handler h = new Handler(host.getLooper());
+
+        qs.setListening(true);
+        waitForIdleSync(h);
+
+        qs.setListening(false);
+        waitForIdleSync(h);
+
+        host.destroy();
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java
index 8eecfcf..5401c30 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java
@@ -18,6 +18,7 @@
 
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertTrue;
+
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyBoolean;
 import static org.mockito.Mockito.anyInt;
@@ -26,11 +27,12 @@
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
-import android.content.Context;
-import android.support.test.InstrumentationRegistry;
 import android.support.test.runner.AndroidJUnit4;
 import android.test.suitebuilder.annotation.SmallTest;
+
 import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -38,13 +40,13 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class TileLayoutTest {
-    private Context mContext = InstrumentationRegistry.getTargetContext();
-    private final TileLayout mTileLayout = new TileLayout(mContext);
+public class TileLayoutTest extends SysuiTestCase {
+    private TileLayout mTileLayout;
     private int mLayoutSizeForOneTile;
 
     @Before
     public void setUp() throws Exception {
+        mTileLayout = new TileLayout(mContext);
         // Layout needs to leave space for the tile margins. Three times the margin size is
         // sufficient for any number of columns.
         mLayoutSizeForOneTile =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
index d7ff04f..782a489 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
@@ -16,7 +16,7 @@
 package com.android.systemui.qs.external;
 
 import static junit.framework.Assert.assertTrue;
-import static junit.framework.Assert.fail;
+
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.anyString;
@@ -25,12 +25,9 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.app.Service;
-import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.IntentFilter;
 import android.content.ServiceConnection;
 import android.content.pm.PackageInfo;
 import android.content.pm.ServiceInfo;
@@ -38,19 +35,16 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.HandlerThread;
-import android.os.IBinder;
-import android.os.Process;
-import android.os.RemoteException;
 import android.os.UserHandle;
 import android.service.quicksettings.IQSService;
 import android.service.quicksettings.IQSTileService;
 import android.service.quicksettings.Tile;
 import android.service.quicksettings.TileService;
-import android.support.test.InstrumentationRegistry;
 import android.support.test.runner.AndroidJUnit4;
 import android.test.suitebuilder.annotation.SmallTest;
-import android.util.ArraySet;
-import android.util.Log;
+
+import com.android.systemui.SysuiTestCase;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -61,7 +55,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class TileLifecycleManagerTest {
+public class TileLifecycleManagerTest extends SysuiTestCase {
     private static final int TEST_FAIL_TIMEOUT = 5000;
 
     private final Context mMockContext = Mockito.mock(Context.class);
@@ -78,8 +72,7 @@
     @Before
     public void setUp() throws Exception {
         setPackageEnabled(true);
-        mTileServiceComponentName = new ComponentName(
-                InstrumentationRegistry.getTargetContext(), "FakeTileService.class");
+        mTileServiceComponentName = new ComponentName(mContext, "FakeTileService.class");
 
         // Stub.asInterface will just return itself.
         when(mMockTileService.queryLocalInterface(anyString())).thenReturn(mMockTileService);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java
index 6c9cfe0..136e7c0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java
@@ -35,7 +35,7 @@
 import com.android.systemui.statusbar.policy.NetworkControllerImpl.Config;
 import com.android.systemui.statusbar.policy.NetworkControllerImpl.SubscriptionDefaults;
 import com.android.systemui.SysuiTestCase;
-import org.junit.After;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.rules.TestWatcher;
@@ -118,7 +118,7 @@
 
         // Trigger blank callbacks to always get the current state (some tests don't trigger
         // changes from default state).
-        mNetworkController.addSignalCallback(mock(SignalCallback.class));
+        mNetworkController.addCallback(mock(SignalCallback.class));
         mNetworkController.addEmergencyListener(null);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/FakeContentResolver.java b/packages/SystemUI/tests/src/com/android/systemui/utils/FakeContentResolver.java
new file mode 100644
index 0000000..34f2e01
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/utils/FakeContentResolver.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.utils;
+
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.IContentProvider;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.util.ArraySet;
+
+import com.google.android.collect.Maps;
+
+import java.util.Map;
+
+/**
+ * Alternative to a MockContentResolver that falls back to real providers.
+ */
+public class FakeContentResolver extends ContentResolver {
+
+    private final Map<String, ContentProvider> mProviders = Maps.newHashMap();
+    private final ContentResolver mParent;
+    private final ArraySet<ContentProvider> mInUse = new ArraySet<>();
+    private boolean mFallbackToExisting;
+
+    public FakeContentResolver(Context context) {
+        super(context);
+        mParent = context.getContentResolver();
+        mFallbackToExisting = true;
+    }
+
+    /**
+     * Sets whether existing providers should be returned when a mock does not exist.
+     * The default is true.
+     */
+    public void setFallbackToExisting(boolean fallbackToExisting) {
+        mFallbackToExisting = fallbackToExisting;
+    }
+
+    /**
+     * Adds access to a provider based on its authority
+     *
+     * @param name The authority name associated with the provider.
+     * @param provider An instance of {@link android.content.ContentProvider} or one of its
+     * subclasses, or null.
+     */
+    public void addProvider(String name, ContentProvider provider) {
+        mProviders.put(name, provider);
+    }
+
+    @Override
+    protected IContentProvider acquireProvider(Context context, String name) {
+        final ContentProvider provider = mProviders.get(name);
+        if (provider != null) {
+            return provider.getIContentProvider();
+        } else {
+            return mFallbackToExisting ? mParent.acquireProvider(name) : null;
+        }
+    }
+
+    @Override
+    protected IContentProvider acquireExistingProvider(Context context, String name) {
+        final ContentProvider provider = mProviders.get(name);
+        if (provider != null) {
+            return provider.getIContentProvider();
+        } else {
+            return mFallbackToExisting ? mParent.acquireExistingProvider(
+                    new Uri.Builder().authority(name).build()) : null;
+        }
+    }
+
+    @Override
+    public boolean releaseProvider(IContentProvider provider) {
+        if (!mFallbackToExisting) return true;
+        if (mInUse.contains(provider)) {
+            mInUse.remove(provider);
+            return true;
+        }
+        return mParent.releaseProvider(provider);
+    }
+
+    @Override
+    protected IContentProvider acquireUnstableProvider(Context c, String name) {
+        final ContentProvider provider = mProviders.get(name);
+        if (provider != null) {
+            return provider.getIContentProvider();
+        } else {
+            return mFallbackToExisting ? mParent.acquireUnstableProvider(name) : null;
+        }
+    }
+
+    @Override
+    public boolean releaseUnstableProvider(IContentProvider icp) {
+        if (!mFallbackToExisting) return true;
+        if (mInUse.contains(icp)) {
+            mInUse.remove(icp);
+            return true;
+        }
+        return mParent.releaseUnstableProvider(icp);
+    }
+
+    @Override
+    public void unstableProviderDied(IContentProvider icp) {
+        if (!mFallbackToExisting) return;
+        if (mInUse.contains(icp)) {
+            return;
+        }
+        mParent.unstableProviderDied(icp);
+    }
+
+    @Override
+    public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) {
+        if (!mFallbackToExisting) return;
+        if (!mProviders.containsKey(uri.getAuthority())) {
+            super.notifyChange(uri, observer, syncToNetwork);
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/FakeSettingsProvider.java b/packages/SystemUI/tests/src/com/android/systemui/utils/FakeSettingsProvider.java
new file mode 100644
index 0000000..f40fe4c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/utils/FakeSettingsProvider.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.utils;
+
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.support.annotation.VisibleForTesting;
+import android.test.mock.MockContentProvider;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.utils.FakeSettingsProvider.SettingOverrider.Builder;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Allows calls to android.provider.Settings to be tested easier.  A SettingOverride
+ * can be acquired and a set of specific settings can be set to a value (and not changed
+ * in the system when set), so that they can be tested without breaking the test device.
+ * <p>
+ * To use, in the before method acquire the override add all settings that will affect if
+ * your test passes or not.
+ *
+ * <pre class="prettyprint">
+ * {@literal
+ * mSettingOverride = mTestableContext.getSettingsProvider().acquireOverridesBuilder()
+ *         .addSetting("secure", Secure.USER_SETUP_COMPLETE, "0")
+ *         .build();
+ * }
+ * </pre>
+ *
+ * Then in the after free up the settings.
+ *
+ * <pre class="prettyprint">
+ * {@literal
+ * mSettingOverride.release();
+ * }
+ * </pre>
+ */
+public class FakeSettingsProvider extends MockContentProvider {
+
+    private static final String TAG = "FakeSettingsProvider";
+    private static final boolean DEBUG = false;
+
+    // Number of times to try to acquire a setting if in use.
+    private static final int MAX_TRIES = 10;
+    // Time to wait for each setting.  WAIT_TIMEOUT * MAX_TRIES will be the maximum wait time
+    // for a setting.
+    private static final long WAIT_TIMEOUT = 1000;
+
+    private final Map<String, SettingOverrider> mOverrideMap = new ArrayMap<>();
+    private final Map<SysuiTestCase, List<SettingOverrider>> mOwners = new ArrayMap<>();
+
+    private static FakeSettingsProvider sInstance;
+    private final ContentProviderClient mSettings;
+    private final ContentResolver mResolver;
+
+    private FakeSettingsProvider(ContentProviderClient settings, ContentResolver resolver) {
+        mSettings = settings;
+        mResolver = resolver;
+    }
+
+    public Builder acquireOverridesBuilder(SysuiTestCase test) {
+        return new Builder(this, test);
+    }
+
+    public void clearOverrides(SysuiTestCase test) {
+        List<SettingOverrider> overrides = mOwners.remove(test);
+        if (overrides != null) {
+            overrides.forEach(override -> override.ensureReleased());
+        }
+    }
+
+    public Bundle call(String method, String arg, Bundle extras) {
+        // Methods are "GET_system", "GET_global", "PUT_secure", etc.
+        final String[] commands = method.split("_", 2);
+        final String op = commands[0];
+        final String table = commands[1];
+
+        synchronized (mOverrideMap) {
+            SettingOverrider overrider = mOverrideMap.get(key(table, arg));
+            if (overrider == null) {
+                // Fall through to real settings.
+                try {
+                    if (DEBUG) Log.d(TAG, "Falling through to real settings " + method);
+                    // TODO: Add our own version of caching to handle this.
+                    Bundle call = mSettings.call(method, arg, extras);
+                    call.remove(Settings.CALL_METHOD_TRACK_GENERATION_KEY);
+                    return call;
+                } catch (RemoteException e) {
+                    throw new RuntimeException(e);
+                }
+            }
+            String value;
+            Bundle out = new Bundle();
+            switch (op) {
+                case "GET":
+                    value = overrider.get(table, arg);
+                    if (value != null) {
+                        out.putString(Settings.NameValueTable.VALUE, value);
+                    }
+                    break;
+                case "PUT":
+                    value = extras.getString(Settings.NameValueTable.VALUE, null);
+                    if (value != null) {
+                        overrider.put(table, arg, value);
+                    } else {
+                        overrider.remove(table, arg);
+                    }
+                    break;
+                default:
+                    throw new UnsupportedOperationException("Unknown command " + method);
+            }
+            return out;
+        }
+    }
+
+    private void acquireSettings(SettingOverrider overridder, Set<String> keys,
+            SysuiTestCase owner) throws AcquireTimeoutException {
+        synchronized (mOwners) {
+            List<SettingOverrider> list = mOwners.get(owner);
+            if (list == null) {
+                list = new ArrayList<>();
+                mOwners.put(owner, list);
+            }
+            list.add(overridder);
+        }
+        synchronized (mOverrideMap) {
+            for (int i = 0; i < MAX_TRIES; i++) {
+                if (checkKeys(keys, false)) break;
+                try {
+                    if (DEBUG) Log.d(TAG, "Waiting for contention to finish");
+                    mOverrideMap.wait(WAIT_TIMEOUT);
+                } catch (InterruptedException e) {
+                }
+            }
+            checkKeys(keys, true);
+            for (String key : keys) {
+                if (DEBUG) Log.d(TAG, "Acquiring " + key);
+                mOverrideMap.put(key, overridder);
+            }
+        }
+    }
+
+    private void releaseSettings(Set<String> keys) {
+        synchronized (mOverrideMap) {
+            for (String key : keys) {
+                if (DEBUG) Log.d(TAG, "Releasing " + key);
+                mOverrideMap.remove(key);
+            }
+            if (DEBUG) Log.d(TAG, "Notifying");
+            mOverrideMap.notify();
+        }
+    }
+
+    @VisibleForTesting
+    public Object getLock() {
+        return mOverrideMap;
+    }
+
+    private boolean checkKeys(Set<String> keys, boolean shouldThrow)
+            throws AcquireTimeoutException {
+        for (String key : keys) {
+            if (mOverrideMap.containsKey(key)) {
+                if (shouldThrow) {
+                    throw new AcquireTimeoutException("Could not acquire " + key);
+                }
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public static class SettingOverrider {
+        private final Set<String> mValidKeys;
+        private final Map<String, String> mValueMap = new ArrayMap<>();
+        private final FakeSettingsProvider mProvider;
+        private boolean mReleased;
+
+        private SettingOverrider(Set<String> keys, FakeSettingsProvider provider) {
+            mValidKeys = new ArraySet<>(keys);
+            mProvider = provider;
+        }
+
+        private void ensureReleased() {
+            if (!mReleased) {
+                release();
+            }
+        }
+
+        public void release() {
+            mProvider.releaseSettings(mValidKeys);
+            mReleased = true;
+        }
+
+        private void putDirect(String key, String value) {
+            mValueMap.put(key, value);
+        }
+
+        public void put(String table, String key, String value) {
+            if (!mValidKeys.contains(key(table, key))) {
+                throw new IllegalArgumentException("Key " + table + " " + key
+                        + " not acquired for this overrider");
+            }
+            mValueMap.put(key(table, key), value);
+        }
+
+        public void remove(String table, String key) {
+            if (!mValidKeys.contains(key(table, key))) {
+                throw new IllegalArgumentException("Key " + table + " " + key
+                        + " not acquired for this overrider");
+            }
+            mValueMap.remove(key(table, key));
+        }
+
+        public String get(String table, String key) {
+            if (!mValidKeys.contains(key(table, key))) {
+                throw new IllegalArgumentException("Key " + table + " " + key
+                        + " not acquired for this overrider");
+            }
+            Log.d(TAG, "Get " + table + " " + key + " " + mValueMap.get(key(table, key)));
+            return mValueMap.get(key(table, key));
+        }
+
+        public static class Builder {
+            private final FakeSettingsProvider mProvider;
+            private final SysuiTestCase mOwner;
+            private Set<String> mKeys = new ArraySet<>();
+            private Map<String, String> mValues = new ArrayMap<>();
+
+            private Builder(FakeSettingsProvider provider, SysuiTestCase test) {
+                mProvider = provider;
+                mOwner = test;
+            }
+
+            public Builder addSetting(String table, String key) {
+                mKeys.add(key(table, key));
+                return this;
+            }
+
+            public Builder addSetting(String table, String key, String value) {
+                addSetting(table, key);
+                mValues.put(key(table, key), value);
+                return this;
+            }
+
+            public SettingOverrider build() throws AcquireTimeoutException {
+                SettingOverrider overrider = new SettingOverrider(mKeys, mProvider);
+                mProvider.acquireSettings(overrider, mKeys, mOwner);
+                mValues.forEach((key, value) -> overrider.putDirect(key, value));
+                return overrider;
+            }
+        }
+    }
+
+    public static class AcquireTimeoutException extends Exception {
+        public AcquireTimeoutException(String str) {
+            super(str);
+        }
+    }
+
+    private static String key(String table, String key) {
+        return table + "_" + key;
+    }
+
+    /**
+     * Since the settings provider is cached inside android.provider.Settings, this must
+     * be gotten statically to ensure there is only one instance referenced.
+     * @param settings
+     */
+    public static FakeSettingsProvider getFakeSettingsProvider(ContentProviderClient settings,
+            ContentResolver resolver) {
+        if (sInstance == null) {
+            sInstance = new FakeSettingsProvider(settings, resolver);
+        }
+        return sInstance;
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/FakeSettingsProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/utils/FakeSettingsProviderTest.java
new file mode 100644
index 0000000..63bb5e7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/utils/FakeSettingsProviderTest.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.utils;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.ContentResolver;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.provider.Settings;
+import android.provider.Settings.Global;
+import android.provider.Settings.Secure;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Log;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.utils.FakeSettingsProvider.AcquireTimeoutException;
+import com.android.systemui.utils.FakeSettingsProvider.SettingOverrider;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class FakeSettingsProviderTest extends SysuiTestCase {
+
+    public static final String NONEXISTENT_SETTING = "nonexistent_setting";
+    private static final String TAG = "FakeSettingsProviderTest";
+    private SettingOverrider mOverrider;
+    private ContentResolver mContentResolver;
+
+    @Before
+    public void setup() throws AcquireTimeoutException {
+        mOverrider = mContext.getSettingsProvider().acquireOverridesBuilder(this)
+                .addSetting("secure", NONEXISTENT_SETTING)
+                .addSetting("global", NONEXISTENT_SETTING, "initial value")
+                .addSetting("global", Global.DEVICE_PROVISIONED)
+                .build();
+        mContentResolver = mContext.getContentResolver();
+    }
+
+    @After
+    public void teardown() {
+        if (mOverrider != null) {
+            mOverrider.release();
+        }
+    }
+
+    @Test
+    public void testInitialValueSecure() {
+        String value = Secure.getString(mContentResolver, NONEXISTENT_SETTING);
+        assertNull(value);
+    }
+
+    @Test
+    public void testInitialValueGlobal() {
+        String value = Global.getString(mContentResolver, NONEXISTENT_SETTING);
+        assertEquals("initial value", value);
+    }
+
+    @Test
+    public void testSeparateTables() {
+        Secure.putString(mContentResolver, NONEXISTENT_SETTING, "something");
+        Global.putString(mContentResolver, NONEXISTENT_SETTING, "else");
+        assertEquals("something", Secure.getString(mContentResolver, NONEXISTENT_SETTING));
+        assertEquals("something", mOverrider.get("secure", NONEXISTENT_SETTING));
+        assertEquals("else", Global.getString(mContentResolver, NONEXISTENT_SETTING));
+        assertEquals("else", mOverrider.get("global", NONEXISTENT_SETTING));
+    }
+
+    @Test
+    public void testPassThrough() {
+        // Grab the value of a setting that is not overridden.
+        assertTrue(Secure.getInt(mContentResolver, Secure.USER_SETUP_COMPLETE, 0) != 0);
+    }
+
+    @Test
+    public void testOverrideExisting() {
+        // Grab the value of a setting that is overridden and will be different than the actual
+        // value.
+        assertNull(Global.getString(mContentResolver, Global.DEVICE_PROVISIONED));
+    }
+
+    @Test
+    public void testRelease() {
+        // Verify different value.
+        assertNull(Global.getString(mContentResolver, Global.DEVICE_PROVISIONED));
+        mOverrider.release();
+        mOverrider = null;
+        // Verify actual value after release.
+        assertEquals("1", Global.getString(mContentResolver, Global.DEVICE_PROVISIONED));
+    }
+
+    @Test
+    public void testAutoRelease() throws Exception {
+        super.cleanup();
+        mContext.getSettingsProvider().acquireOverridesBuilder(this)
+                .addSetting("global", Global.DEVICE_PROVISIONED)
+                .build();
+    }
+
+    @Test
+    public void testContention() throws AcquireTimeoutException, InterruptedException {
+        SettingOverrider[] overriders = new SettingOverrider[2];
+        Object lock = new Object();
+        String secure = "secure";
+        String key = "something shared";
+        String[] result = new String[1];
+        overriders[0] = mContext.getSettingsProvider().acquireOverridesBuilder(this)
+                .addSetting(secure, key, "Some craziness")
+                .build();
+        synchronized (lock) {
+            HandlerThread t = runOnHandler(() -> {
+                try {
+                    // Grab the lock that will be used for the settings ownership to ensure
+                    // we have some contention going on.
+                    synchronized (mContext.getSettingsProvider().getLock()) {
+                        synchronized (lock) {
+                            // Let the other thread know to release the settings, but it won't
+                            // be able to until this thread waits in the build() method.
+                            lock.notify();
+                        }
+                        overriders[1] = mContext.getSettingsProvider()
+                                .acquireOverridesBuilder(FakeSettingsProviderTest.this)
+                                .addSetting(secure, key, "default value")
+                                .build();
+                        // Ensure that the default is the one we set, and not left over from
+                        // the other setting override.
+                        result[0] = Settings.Secure.getString(mContentResolver, key);
+                        synchronized (lock) {
+                            // Let the main thread know we are done.
+                            lock.notify();
+                        }
+                    }
+                } catch (AcquireTimeoutException e) {
+                    Log.e(TAG, "Couldn't acquire setting", e);
+                }
+            });
+            // Wait for the thread to hold the acquire lock, then release the settings.
+            lock.wait();
+            overriders[0].release();
+            // Wait for the thread to be done getting the value.
+            lock.wait();
+            // Quit and cleanup.
+            t.quitSafely();
+            assertNotNull(overriders[1]);
+            overriders[1].release();
+        }
+        // Verify the value was the expected one from the thread's SettingOverride.
+        assertEquals("default value", result[0]);
+    }
+
+    private HandlerThread runOnHandler(Runnable r) {
+        HandlerThread t = new HandlerThread("Test Thread");
+        t.start();
+        new Handler(t.getLooper()).post(r);
+        return t;
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/TestableContext.java b/packages/SystemUI/tests/src/com/android/systemui/utils/TestableContext.java
new file mode 100644
index 0000000..5179823
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/utils/TestableContext.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.utils;
+
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.provider.Settings;
+
+public class TestableContext extends ContextWrapper {
+
+    private final FakeContentResolver mFakeContentResolver;
+    private final FakeSettingsProvider mSettingsProvider;
+
+    public TestableContext(Context base) {
+        super(base);
+        mFakeContentResolver = new FakeContentResolver(base);
+        ContentProviderClient settings = base.getContentResolver()
+                .acquireContentProviderClient(Settings.AUTHORITY);
+        mSettingsProvider = FakeSettingsProvider.getFakeSettingsProvider(settings,
+                mFakeContentResolver);
+        mFakeContentResolver.addProvider(Settings.AUTHORITY, mSettingsProvider);
+    }
+
+    public FakeSettingsProvider getSettingsProvider() {
+        return mSettingsProvider;
+    }
+
+    @Override
+    public FakeContentResolver getContentResolver() {
+        return mFakeContentResolver;
+    }
+
+    @Override
+    public Context getApplicationContext() {
+        // Return this so its always a TestableContext.
+        return this;
+    }
+}
diff --git a/proto/src/metrics_constants.proto b/proto/src/metrics_constants.proto
index 48bc27e..5cf74c7 100644
--- a/proto/src/metrics_constants.proto
+++ b/proto/src/metrics_constants.proto
@@ -2627,6 +2627,11 @@
     // ACTION: Logs the end to end time taken by all provisioning tasks.
     PROVISIONING_TOTAL_TASK_TIME_MS = 627;
 
+    // OPEN: Settings > Privacy
+    // CATEGORY: SETTINGS
+    // OS: O
+    ENTERPRISE_PRIVACY_SETTINGS = 628;
+
     // ---- End O Constants, all O constants go above this line ----
 
     // Add new aosp constants above this line.
diff --git a/services/core/java/com/android/server/BluetoothManagerService.java b/services/core/java/com/android/server/BluetoothManagerService.java
index 9b3fac3..f7068cf 100644
--- a/services/core/java/com/android/server/BluetoothManagerService.java
+++ b/services/core/java/com/android/server/BluetoothManagerService.java
@@ -59,6 +59,7 @@
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -118,7 +119,6 @@
     private static final int SERVICE_IBLUETOOTHGATT = 2;
 
     private final Context mContext;
-    private static int mBleAppCount = 0;
 
     // Locks are not provided for mName and mAddress.
     // They are accessed in handler or broadcast receiver, same thread context.
@@ -212,10 +212,7 @@
 
                     if (isAirplaneModeOn()) {
                         // Clear registered LE apps to force shut-off
-                        synchronized (this) {
-                            mBleAppCount = 0;
-                            mBleApps.clear();
-                        }
+                        clearBleApps();
                         if (st == BluetoothAdapter.STATE_BLE_ON) {
                             //if state is BLE_ON make sure you trigger disableBLE part
                             try {
@@ -460,28 +457,28 @@
     class ClientDeathRecipient implements IBinder.DeathRecipient {
         public void binderDied() {
             if (DBG) Slog.d(TAG, "Binder is dead - unregister Ble App");
-            if (mBleAppCount > 0) --mBleAppCount;
-
-            if (mBleAppCount == 0) {
-                if (DBG) Slog.d(TAG, "Disabling LE only mode after application crash");
-                try {
-                    mBluetoothLock.readLock().lock();
-                    if (mBluetooth != null &&
-                        mBluetooth.getState() == BluetoothAdapter.STATE_BLE_ON) {
-                        mEnable = false;
-                        mBluetooth.onBrEdrDown();
-                    }
-                } catch (RemoteException e) {
-                     Slog.e(TAG,"Unable to call onBrEdrDown", e);
-                } finally {
-                    mBluetoothLock.readLock().unlock();
+            if (isBleAppPresent()) {
+              // Nothing to do, another app is here.
+              return;
+            }
+            if (DBG) Slog.d(TAG, "Disabling LE only mode after application crash");
+            try {
+                mBluetoothLock.readLock().lock();
+                if (mBluetooth != null &&
+                    mBluetooth.getState() == BluetoothAdapter.STATE_BLE_ON) {
+                    mEnable = false;
+                    mBluetooth.onBrEdrDown();
                 }
+            } catch (RemoteException e) {
+                 Slog.e(TAG,"Unable to call onBrEdrDown", e);
+            } finally {
+                mBluetoothLock.readLock().unlock();
             }
         }
     }
 
     /** Internal death rec list */
-    Map<IBinder, ClientDeathRecipient> mBleApps = new HashMap<IBinder, ClientDeathRecipient>();
+    Map<IBinder, ClientDeathRecipient> mBleApps = new ConcurrentHashMap<IBinder, ClientDeathRecipient>();
 
     @Override
     public boolean isBleScanAlwaysAvailable() {
@@ -501,17 +498,20 @@
         ContentObserver contentObserver = new ContentObserver(null) {
             @Override
             public void onChange(boolean selfChange) {
-                if (!isBleScanAlwaysAvailable()) {
-                    disableBleScanMode();
-                    clearBleApps();
-                    try {
-                        mBluetoothLock.readLock().lock();
-                        if (mBluetooth != null) mBluetooth.onBrEdrDown();
-                    } catch (RemoteException e) {
-                        Slog.e(TAG, "error when disabling bluetooth", e);
-                    } finally {
-                        mBluetoothLock.readLock().unlock();
-                    }
+                if (isBleScanAlwaysAvailable()) {
+                  // Nothing to do
+                  return;
+                }
+                // BLE scan is not available.
+                disableBleScanMode();
+                clearBleApps();
+                try {
+                    mBluetoothLock.readLock().lock();
+                    if (mBluetooth != null) mBluetooth.onBrEdrDown();
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "error when disabling bluetooth", e);
+                } finally {
+                    mBluetoothLock.readLock().unlock();
                 }
             }
         };
@@ -547,9 +547,6 @@
                     throw new IllegalArgumentException("Wake lock is already dead.");
                 }
                 mBleApps.put(token, deathRec);
-                synchronized (this) {
-                    ++mBleAppCount;
-                }
                 if (DBG) Slog.d(TAG, "Registered for death Notification");
             }
 
@@ -559,31 +556,26 @@
                 // Unregister death recipient as the app goes away.
                 token.unlinkToDeath(r, 0);
                 mBleApps.remove(token);
-                synchronized (this) {
-                    if (mBleAppCount > 0) --mBleAppCount;
-                }
                 if (DBG) Slog.d(TAG, "Unregistered for death Notification");
             }
         }
-        if (DBG) Slog.d(TAG, "Updated BleAppCount" + mBleAppCount);
-        if (mBleAppCount == 0 && mEnable) {
+        int appCount = mBleApps.size();
+        if (DBG) Slog.d(TAG, appCount + " registered Ble Apps");
+        if (appCount == 0 && mEnable) {
             disableBleScanMode();
         }
-        return mBleAppCount;
+        return appCount;
     }
 
     // Clear all apps using BLE scan only mode.
     private void clearBleApps() {
-        synchronized (this) {
-            mBleApps.clear();
-            mBleAppCount = 0;
-        }
+        mBleApps.clear();
     }
 
     /** @hide*/
     public boolean isBleAppPresent() {
-        if (DBG) Slog.d(TAG, "isBleAppPresent() count: " + mBleAppCount);
-        return (mBleAppCount > 0);
+        if (DBG) Slog.d(TAG, "isBleAppPresent() count: " + mBleApps.size());
+        return mBleApps.size() > 0;
     }
 
     /**
@@ -1449,12 +1441,12 @@
                     if ((prevState == BluetoothAdapter.STATE_BLE_TURNING_ON) &&
                             (newState == BluetoothAdapter.STATE_OFF) &&
                             (mBluetooth != null) && mEnable) {
-                        recoverBluetoothServiceFromError();
+                        recoverBluetoothServiceFromError(false);
                     }
                     if ((prevState == BluetoothAdapter.STATE_TURNING_ON) &&
                             (newState == BluetoothAdapter.STATE_BLE_ON) &&
                             (mBluetooth != null) && mEnable) {
-                        recoverBluetoothServiceFromError();
+                        recoverBluetoothServiceFromError(true);
                     }
                     // If we tried to enable BT while BT was in the process of shutting down,
                     // wait for the BT process to fully tear down and then force a restart
@@ -1870,7 +1862,7 @@
                              quietMode ? 1 : 0, 0));
     }
 
-    private void recoverBluetoothServiceFromError() {
+    private void recoverBluetoothServiceFromError(boolean clearBle) {
         Slog.e(TAG,"recoverBluetoothServiceFromError");
         try {
             mBluetoothLock.readLock().lock();
@@ -1908,6 +1900,10 @@
         mHandler.removeMessages(MESSAGE_BLUETOOTH_STATE_CHANGE);
         mState = BluetoothAdapter.STATE_OFF;
 
+        if (clearBle) {
+          clearBleApps();
+        }
+
         mEnable = false;
 
         if (mErrorRecoveryRetryCounter++ < MAX_ERROR_RESTART_RETRIES) {
diff --git a/services/core/java/com/android/server/InputMethodManagerService.java b/services/core/java/com/android/server/InputMethodManagerService.java
index 2bf5866..d1f07a5 100644
--- a/services/core/java/com/android/server/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/InputMethodManagerService.java
@@ -60,6 +60,7 @@
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
+import android.content.ContentProvider;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.DialogInterface;
@@ -3969,10 +3970,22 @@
                     + mCurAttribute.packageName + " packageName=" + packageName);
                 return null;
             }
+            // This user ID can never bee spoofed.
             final int imeUserId = UserHandle.getUserId(uid);
+            // This user ID can never bee spoofed.
             final int appUserId = UserHandle.getUserId(mCurClient.uid);
-            return new InputContentUriTokenHandler(contentUri, uid, packageName, imeUserId,
-                    appUserId);
+            // This user ID may be invalid if "contentUri" embedded an invalid user ID.
+            final int contentUriOwnerUserId = ContentProvider.getUserIdFromUri(contentUri,
+                    imeUserId);
+            final Uri contentUriWithoutUserId = ContentProvider.getUriWithoutUserId(contentUri);
+            // Note: InputContentUriTokenHandler.take() checks whether the IME (specified by "uid")
+            // actually has the right to grant a read permission for "contentUriWithoutUserId" that
+            // is claimed to belong to "contentUriOwnerUserId".  For example, specifying random
+            // content URI and/or contentUriOwnerUserId just results in a SecurityException thrown
+            // from InputContentUriTokenHandler.take() and can never be allowed beyond what is
+            // actually allowed to "uid", which is guaranteed to be the IME's one.
+            return new InputContentUriTokenHandler(contentUriWithoutUserId, uid,
+                    packageName, contentUriOwnerUserId, appUserId);
         }
     }
 
diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java
index 2825cf9..6268697 100644
--- a/services/core/java/com/android/server/UiModeManagerService.java
+++ b/services/core/java/com/android/server/UiModeManagerService.java
@@ -39,19 +39,24 @@
 import android.os.IBinder;
 import android.os.PowerManager;
 import android.os.RemoteException;
+import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.service.dreams.Sandman;
 import android.util.Slog;
+import android.view.WindowManagerInternal;
+import android.view.WindowManagerPolicy;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 
 import com.android.internal.R;
 import com.android.internal.app.DisableCarModeActivity;
+import com.android.server.power.ShutdownThread;
 import com.android.server.twilight.TwilightListener;
 import com.android.server.twilight.TwilightManager;
 import com.android.server.twilight.TwilightState;
+import com.android.server.wm.WindowManagerService;
 
 final class UiModeManagerService extends SystemService {
     private static final String TAG = UiModeManager.class.getSimpleName();
@@ -297,6 +302,30 @@
         }
 
         @Override
+        public void setTheme(String theme) {
+            if (getContext().checkCallingOrSelfPermission(
+                    android.Manifest.permission.MODIFY_THEME_OVERLAY)
+                    != PackageManager.PERMISSION_GRANTED) {
+                Slog.e(TAG, "setTheme requires MODIFY_THEME_OVERLAY permission");
+                return;
+            }
+            SystemProperties.set("persist.vendor.overlay.theme", theme);
+            mHandler.post(() -> ShutdownThread.reboot(getContext(),
+                    PowerManager.SHUTDOWN_USER_REQUESTED, false));
+        }
+
+        @Override
+        public String getTheme() {
+            if (getContext().checkCallingOrSelfPermission(
+                    android.Manifest.permission.MODIFY_THEME_OVERLAY)
+                    != PackageManager.PERMISSION_GRANTED) {
+                Slog.e(TAG, "setTheme requires MODIFY_THEME_OVERLAY permission");
+                return null;
+            }
+            return SystemProperties.get("persist.vendor.overlay.theme");
+        }
+
+        @Override
         public int getNightMode() {
             synchronized (mLock) {
                 return mNightMode;
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index 8bb0e1a..282ec50 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -2120,6 +2120,14 @@
             final int size = tasks.size();
             if (onTop) {
                 for (int i = 0; i < size; i++) {
+                    final TaskRecord task = tasks.get(i);
+                    if (fromStackId == PINNED_STACK_ID) {
+                        // Update the return-to to reflect where the pinned stack task was moved
+                        // from so that we retain the stack that was previously visible if the
+                        // pinned stack is recreated. See moveActivityToPinnedStackLocked().
+                        task.setTaskToReturnTo(getFocusedStack().getStackId() == HOME_STACK_ID
+                                ? HOME_ACTIVITY_TYPE : APPLICATION_ACTIVITY_TYPE);
+                    }
                     moveTaskToStackLocked(tasks.get(i).taskId,
                             FULLSCREEN_WORKSPACE_STACK_ID, onTop, onTop /*forceFocus*/,
                             "moveTasksToFullscreenStack", ANIMATE, DEFER_RESUME);
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index c7f4d6b..90ede6f 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -2722,7 +2722,7 @@
                     + " id=" + id + " notification=" + notification);
         }
         final NotificationChannel channel =  mRankingHelper.getNotificationChannelWithFallback(pkg,
-                callingUid, notification.getNotificationChannel());
+                callingUid, notification.getChannel());
         final StatusBarNotification n = new StatusBarNotification(
                 pkg, opPkg, channel, id, tag, callingUid, callingPid, notification,
                 user, null, System.currentTimeMillis());
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index 965257c..5eacba6 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -283,7 +283,7 @@
         pw.println(prefix + "  mVisibleSinceMs=" + mVisibleSinceMs);
         pw.println(prefix + "  mUpdateTimeMs=" + mUpdateTimeMs);
         pw.println(prefix + "  mSuppressedVisualEffects= " + mSuppressedVisualEffects);
-        pw.println(prefix + "  notificationChannel= " + notification.getNotificationChannel());
+        pw.println(prefix + "  notificationChannel= " + notification.getChannel());
     }
 
 
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index 869e207..49ffa22 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -649,15 +649,12 @@
 
             private void populateWindowsOnScreenLocked(SparseArray<WindowState> outWindows) {
                 final DisplayContent dc = mWindowManagerService.getDefaultDisplayContentLocked();
-                final ReadOnlyWindowList windowList = dc.getReadOnlyWindowList();
-                final int windowCount = windowList.size();
-                for (int i = 0; i < windowCount; i++) {
-                    final WindowState windowState = windowList.get(i);
-                    if (windowState.isOnScreen() && windowState.isVisibleLw() &&
-                            !windowState.mWinAnimator.mEnterAnimationPending) {
-                        outWindows.put(windowState.mLayer, windowState);
+                dc.forAllWindows((w) -> {
+                    if (w.isOnScreen() && w.isVisibleLw()
+                            && !w.mWinAnimator.mEnterAnimationPending) {
+                        outWindows.put(w.mLayer, w);
                     }
-                }
+                }, false /* traverseTopToBottom */ );
             }
 
             private final class ViewportWindow {
@@ -1296,14 +1293,11 @@
 
         private void populateVisibleWindowsOnScreenLocked(SparseArray<WindowState> outWindows) {
             final DisplayContent dc = mWindowManagerService.getDefaultDisplayContentLocked();
-            final ReadOnlyWindowList windowList = dc.getReadOnlyWindowList();
-            final int windowCount = windowList.size();
-            for (int i = 0; i < windowCount; i++) {
-                final WindowState windowState = windowList.get(i);
-                if (windowState.isVisibleLw()) {
-                    outWindows.put(windowState.mLayer, windowState);
+            dc.forAllWindows((w) -> {
+                if (w.isVisibleLw()) {
+                    outWindows.put(w.mLayer, w);
                 }
-            }
+            }, false /* traverseTopToBottom */ );
         }
 
         private class MyHandler extends Handler {
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 6a625f4..ff39853 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -1400,14 +1400,14 @@
 
     /** Updates the layer assignment of windows on this display. */
     void assignWindowLayers(boolean setLayoutNeeded) {
-        mLayersController.assignWindowLayers(mWindows.getReadOnly());
+        mLayersController.assignWindowLayers(this);
         if (setLayoutNeeded) {
             setLayoutNeeded();
         }
     }
 
     void adjustWallpaperWindows() {
-        if (mWallpaperController.adjustWallpaperWindows(mWindows.getReadOnly())) {
+        if (mWallpaperController.adjustWallpaperWindows(mWindows)) {
             assignWindowLayers(true /*setLayoutNeeded*/);
         }
     }
@@ -2457,14 +2457,6 @@
         }
     }
 
-    ReadOnlyWindowList getReadOnlyWindowList() {
-        return mWindows.getReadOnly();
-    }
-
-    void getWindows(WindowList output) {
-        output.addAll(mWindows);
-    }
-
     // TODO: Super crazy long method that should be broken down...
     boolean applySurfaceChangesTransaction(boolean recoveringMemory) {
 
diff --git a/services/core/java/com/android/server/wm/DockedStackDividerController.java b/services/core/java/com/android/server/wm/DockedStackDividerController.java
index 6326148..c56f6b8 100644
--- a/services/core/java/com/android/server/wm/DockedStackDividerController.java
+++ b/services/core/java/com/android/server/wm/DockedStackDividerController.java
@@ -266,10 +266,8 @@
     }
 
     private void resetDragResizingChangeReported() {
-        final ReadOnlyWindowList windowList = mDisplayContent.getReadOnlyWindowList();
-        for (int i = windowList.size() - 1; i >= 0; i--) {
-            windowList.get(i).resetDragResizingChangeReported();
-        }
+        mDisplayContent.forAllWindows(WindowState::resetDragResizingChangeReported,
+                true /* traverseTopToBottom */ );
     }
 
     void setWindow(WindowState window) {
diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java
index d52168c..4d195e8 100644
--- a/services/core/java/com/android/server/wm/DragState.java
+++ b/services/core/java/com/android/server/wm/DragState.java
@@ -271,11 +271,8 @@
             Slog.d(TAG_WM, "broadcasting DRAG_STARTED at (" + touchX + ", " + touchY + ")");
         }
 
-        final ReadOnlyWindowList windows = mDisplayContent.getReadOnlyWindowList();
-        final int N = windows.size();
-        for (int i = 0; i < N; i++) {
-            sendDragStartedLw(windows.get(i), touchX, touchY, mDataDescription);
-        }
+        mDisplayContent.forAllWindows((w) -> sendDragStartedLw(w, touchX, touchY, mDataDescription),
+                false /* traverseTopToBottom */ );
     }
 
     /* helper - send a ACTION_DRAG_STARTED event, if the
diff --git a/services/core/java/com/android/server/wm/PinnedStackController.java b/services/core/java/com/android/server/wm/PinnedStackController.java
index effb1b2..01a50b7 100644
--- a/services/core/java/com/android/server/wm/PinnedStackController.java
+++ b/services/core/java/com/android/server/wm/PinnedStackController.java
@@ -69,6 +69,7 @@
 
     // States that affect how the PIP can be manipulated
     private boolean mInInteractiveMode;
+    private boolean mIsMinimized;
     private boolean mIsImeShowing;
     private int mImeHeight;
     private ValueAnimator mBoundsAnimator = null;
@@ -103,6 +104,13 @@
         }
 
         @Override
+        public void setIsMinimized(final boolean isMinimized) {
+            mHandler.post(() -> {
+                mIsMinimized = isMinimized;
+            });
+        }
+
+        @Override
         public void setSnapToEdge(final boolean snapToEdge) {
             mHandler.post(() -> {
                 mSnapAlgorithm.setSnapToEdge(snapToEdge);
@@ -248,6 +256,12 @@
                     false /* adjustForIme */);
             mSnapAlgorithm.applySnapFraction(postChangeStackBounds, postChangeMovementBounds,
                     snapFraction);
+            if (mIsMinimized) {
+                final Point displaySize = new Point(mDisplayInfo.logicalWidth,
+                        mDisplayInfo.logicalHeight);
+                mSnapAlgorithm.applyMinimizedOffset(postChangeStackBounds, postChangeMovementBounds,
+                        displaySize);
+            }
         }
         return postChangeStackBounds;
     }
@@ -277,7 +291,12 @@
             final Rect toBounds = new Rect(stackBounds);
             if (adjustedForIme) {
                 // IME visible
-                toBounds.offset(0, Math.min(0, movementBounds.bottom - stackBounds.top));
+                if (stackBounds.top == prevMovementBounds.bottom) {
+                    // If the PIP is resting on top of the IME, then adjust it with the hiding IME
+                    toBounds.offsetTo(toBounds.left, movementBounds.bottom);
+                } else {
+                    toBounds.offset(0, Math.min(0, movementBounds.bottom - stackBounds.top));
+                }
             } else {
                 // IME hidden
                 if (stackBounds.top == prevMovementBounds.bottom) {
@@ -335,5 +354,6 @@
         pw.println();
         pw.println(prefix + "  mIsImeShowing=" + mIsImeShowing);
         pw.println(prefix + "  mInInteractiveMode=" + mInInteractiveMode);
+        pw.println(prefix + "  mIsMinimized=" + mIsMinimized);
     }
 }
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 299fa05..88986e3 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -219,28 +219,6 @@
         return false;
     }
 
-    void getWindows(WindowList output) {
-        final int count = mChildren.size();
-        for (int i = 0; i < count; ++i) {
-            final DisplayContent dc = mChildren.get(i);
-            dc.getWindows(output);
-        }
-    }
-
-    void getWindows(WindowList output, boolean visibleOnly, boolean appsOnly) {
-        final int numDisplays = mChildren.size();
-        for (int displayNdx = 0; displayNdx < numDisplays; ++displayNdx) {
-            final ReadOnlyWindowList windowList = mChildren.get(displayNdx).getReadOnlyWindowList();
-            for (int winNdx = windowList.size() - 1; winNdx >= 0; --winNdx) {
-                final WindowState w = windowList.get(winNdx);
-                if ((!visibleOnly || w.mWinAnimator.getShown())
-                        && (!appsOnly || w.mAppToken != null)) {
-                    output.add(w);
-                }
-            }
-        }
-    }
-
     void getWindowsByName(WindowList output, String name) {
         int objectId = 0;
         // See if this is an object ID.
@@ -249,36 +227,20 @@
             name = null;
         } catch (RuntimeException e) {
         }
-        final int numDisplays = mChildren.size();
-        for (int displayNdx = 0; displayNdx < numDisplays; ++displayNdx) {
-            final ReadOnlyWindowList windowList = mChildren.get(displayNdx).getReadOnlyWindowList();
-            for (int winNdx = windowList.size() - 1; winNdx >= 0; --winNdx) {
-                final WindowState w = windowList.get(winNdx);
-                if (name != null) {
-                    if (w.mAttrs.getTitle().toString().contains(name)) {
-                        output.add(w);
-                    }
-                } else if (System.identityHashCode(w) == objectId) {
-                    output.add(w);
-                }
-            }
-        }
+
+        getWindowsByName(output, name, objectId);
     }
 
-    WindowState findWindow(int hashCode) {
-        final int numDisplays = mChildren.size();
-        for (int displayNdx = 0; displayNdx < numDisplays; ++displayNdx) {
-            final ReadOnlyWindowList windows = mChildren.get(displayNdx).getReadOnlyWindowList();
-            final int numWindows = windows.size();
-            for (int winNdx = 0; winNdx < numWindows; ++winNdx) {
-                final WindowState w = windows.get(winNdx);
-                if (System.identityHashCode(w) == hashCode) {
-                    return w;
+    private void getWindowsByName(WindowList output, String name, int objectId) {
+        forAllWindows((w) -> {
+            if (name != null) {
+                if (w.mAttrs.getTitle().toString().contains(name)) {
+                    output.add(w);
                 }
+            } else if (System.identityHashCode(w) == objectId) {
+                output.add(w);
             }
-        }
-
-        return null;
+        }, true /* traverseTopToBottom */);
     }
 
     /**
@@ -399,81 +361,50 @@
     }
 
     void setSecureSurfaceState(int userId, boolean disabled) {
-        for (int i = mChildren.size() - 1; i >= 0; --i) {
-            final ReadOnlyWindowList windows = mChildren.get(i).getReadOnlyWindowList();
-            for (int winNdx = windows.size() - 1; winNdx >= 0; --winNdx) {
-                final WindowState win = windows.get(winNdx);
-                if (win.mHasSurface && userId == UserHandle.getUserId(win.mOwnerUid)) {
-                    win.mWinAnimator.setSecureLocked(disabled);
-                }
+        forAllWindows((w) -> {
+            if (w.mHasSurface && userId == UserHandle.getUserId(w.mOwnerUid)) {
+                w.mWinAnimator.setSecureLocked(disabled);
             }
-        }
+        }, true /* traverseTopToBottom */);
     }
 
     void updateAppOpsState() {
-        final int count = mChildren.size();
-        for (int i = 0; i < count; ++i) {
-            final ReadOnlyWindowList windows = mChildren.get(i).getReadOnlyWindowList();
-            final int numWindows = windows.size();
-            for (int winNdx = 0; winNdx < numWindows; ++winNdx) {
-                final WindowState win = windows.get(winNdx);
-                if (win.mAppOp == OP_NONE) {
-                    continue;
-                }
-                final int mode = mService.mAppOps.checkOpNoThrow(win.mAppOp, win.getOwningUid(),
-                        win.getOwningPackage());
-                win.setAppOpVisibilityLw(mode == MODE_ALLOWED || mode == MODE_DEFAULT);
+        forAllWindows((w) -> {
+            if (w.mAppOp == OP_NONE) {
+                return;
             }
-        }
+            final int mode = mService.mAppOps.checkOpNoThrow(w.mAppOp, w.getOwningUid(),
+                    w.getOwningPackage());
+            w.setAppOpVisibilityLw(mode == MODE_ALLOWED || mode == MODE_DEFAULT);
+        }, false /* traverseTopToBottom */);
     }
 
     boolean canShowStrictModeViolation(int pid) {
-        final int count = mChildren.size();
-        for (int i = 0; i < count; ++i) {
-            final ReadOnlyWindowList windows = mChildren.get(i).getReadOnlyWindowList();
-            final int numWindows = windows.size();
-            for (int winNdx = 0; winNdx < numWindows; ++winNdx) {
-                final WindowState ws = windows.get(winNdx);
-                if (ws.mSession.mPid == pid && ws.isVisibleLw()) {
-                    return true;
-                }
-            }
-        }
-        return false;
+        final WindowState win = getWindow((w) -> w.mSession.mPid == pid && w.isVisibleLw());
+        return win != null;
     }
 
     void closeSystemDialogs(String reason) {
-        final int count = mChildren.size();
-        for (int i = 0; i < count; ++i) {
-            final ReadOnlyWindowList windows = mChildren.get(i).getReadOnlyWindowList();
-            final int numWindows = windows.size();
-            for (int j = 0; j < numWindows; ++j) {
-                final WindowState w = windows.get(j);
-                if (w.mHasSurface) {
-                    try {
-                        w.mClient.closeSystemDialogs(reason);
-                    } catch (RemoteException e) {
-                    }
+        forAllWindows((w) -> {
+            if (w.mHasSurface) {
+                try {
+                    w.mClient.closeSystemDialogs(reason);
+                } catch (RemoteException e) {
                 }
             }
-        }
+        }, false /* traverseTopToBottom */);
     }
 
     void removeReplacedWindows() {
         if (SHOW_TRANSACTIONS) Slog.i(TAG, ">>> OPEN TRANSACTION removeReplacedWindows");
         mService.openSurfaceTransaction();
         try {
-            for (int i = mChildren.size() - 1; i >= 0; i--) {
-                DisplayContent dc = mChildren.get(i);
-                final ReadOnlyWindowList windows = mChildren.get(i).getReadOnlyWindowList();
-                for (int j = windows.size() - 1; j >= 0; j--) {
-                    final WindowState win = windows.get(j);
-                    final AppWindowToken aToken = win.mAppToken;
-                    if (aToken != null) {
-                        aToken.removeReplacedWindowIfNeeded(win);
-                    }
+            forAllWindows((w) -> {
+                final AppWindowToken aToken = w.mAppToken;
+                if (aToken != null) {
+                    aToken.removeReplacedWindowIfNeeded(w);
                 }
-            }
+            }, true /* traverseTopToBottom */);
         } finally {
             mService.closeSurfaceTransaction();
             if (SHOW_TRANSACTIONS) Slog.i(TAG, "<<< CLOSE TRANSACTION removeReplacedWindows");
@@ -530,19 +461,15 @@
                 Slog.w(TAG_WM, "No leaked surfaces; killing applications!");
                 final SparseIntArray pidCandidates = new SparseIntArray();
                 for (int displayNdx = 0; displayNdx < numDisplays; ++displayNdx) {
-                    final ReadOnlyWindowList windows =
-                            mChildren.get(displayNdx).getReadOnlyWindowList();
-                    final int numWindows = windows.size();
-                    for (int winNdx = 0; winNdx < numWindows; ++winNdx) {
-                        final WindowState ws = windows.get(winNdx);
-                        if (mService.mForceRemoves.contains(ws)) {
-                            continue;
+                    mChildren.get(displayNdx).forAllWindows((w) -> {
+                        if (mService.mForceRemoves.contains(w)) {
+                            return;
                         }
-                        final WindowStateAnimator wsa = ws.mWinAnimator;
+                        final WindowStateAnimator wsa = w.mWinAnimator;
                         if (wsa.mSurfaceController != null) {
                             pidCandidates.append(wsa.mSession.mPid, wsa.mSession.mPid);
                         }
-                    }
+                    }, false /* traverseTopToBottom */);
 
                     if (pidCandidates.size() > 0) {
                         int[] pids = new int[pidCandidates.size()];
@@ -1078,17 +1005,14 @@
     }
 
     void dumpWindowsNoHeader(PrintWriter pw, boolean dumpAll, ArrayList<WindowState> windows) {
-        final int numDisplays = mChildren.size();
-        for (int displayNdx = 0; displayNdx < numDisplays; ++displayNdx) {
-            final ReadOnlyWindowList windowList = mChildren.get(displayNdx).getReadOnlyWindowList();
-            for (int winNdx = windowList.size() - 1; winNdx >= 0; --winNdx) {
-                final WindowState w = windowList.get(winNdx);
-                if (windows == null || windows.contains(w)) {
-                    pw.println("  Window #" + winNdx + " " + w + ":");
-                    w.dump(pw, "    ", dumpAll || windows != null);
-                }
+        final int[] index = new int[1];
+        forAllWindows((w) -> {
+            if (windows == null || windows.contains(w)) {
+                pw.println("  Window #" + index[0] + " " + w + ":");
+                w.dump(pw, "    ", dumpAll || windows != null);
+                index[0] = index[0] + 1;
             }
-        }
+        }, true /* traverseTopToBottom */);
     }
 
     void dumpTokens(PrintWriter pw, boolean dumpAll) {
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index 178fbe7..d3e8e8e 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -388,7 +388,7 @@
         return mWallpaperAnimLayerAdjustment;
     }
 
-    private void findWallpaperTarget(ReadOnlyWindowList windows, FindWallpaperTargetResult result) {
+    private void findWallpaperTarget(WindowList windows, FindWallpaperTargetResult result) {
         final WindowAnimator winAnimator = mService.mAnimator;
         result.reset();
         WindowState w = null;
@@ -489,7 +489,7 @@
 
     /** Updates the target wallpaper if needed and returns true if an update happened. */
     private boolean updateWallpaperWindowsTarget(
-            ReadOnlyWindowList windows, FindWallpaperTargetResult result) {
+            WindowList windows, FindWallpaperTargetResult result) {
 
         WindowState wallpaperTarget = result.wallpaperTarget;
         int wallpaperTargetIndex = result.wallpaperTargetIndex;
@@ -590,7 +590,7 @@
         return true;
     }
 
-    private boolean updateWallpaperWindowsTargetByLayer(ReadOnlyWindowList windows,
+    private boolean updateWallpaperWindowsTargetByLayer(WindowList windows,
             FindWallpaperTargetResult result) {
 
         WindowState wallpaperTarget = result.wallpaperTarget;
@@ -641,7 +641,7 @@
         return visible;
     }
 
-    private boolean updateWallpaperWindowsPlacement(ReadOnlyWindowList windows,
+    private boolean updateWallpaperWindowsPlacement(WindowList windows,
             WindowState wallpaperTarget, int wallpaperTargetIndex, boolean visible) {
 
         // TODO(multidisplay): Wallpapers on main screen only.
@@ -660,7 +660,7 @@
         return changed;
     }
 
-    boolean adjustWallpaperWindows(ReadOnlyWindowList windows) {
+    boolean adjustWallpaperWindows(WindowList windows) {
         mService.mRoot.mWallpaperMayChange = false;
 
         // First find top-most window that has asked to be on top of the wallpaper;
diff --git a/services/core/java/com/android/server/wm/WallpaperWindowToken.java b/services/core/java/com/android/server/wm/WallpaperWindowToken.java
index fdefcfe..3a76cd4 100644
--- a/services/core/java/com/android/server/wm/WallpaperWindowToken.java
+++ b/services/core/java/com/android/server/wm/WallpaperWindowToken.java
@@ -119,7 +119,7 @@
         }
     }
 
-    boolean updateWallpaperWindowsPlacement(ReadOnlyWindowList windowList,
+    boolean updateWallpaperWindowsPlacement(WindowList windowList,
             WindowState wallpaperTarget, int wallpaperTargetIndex, boolean visible, int dw, int dh,
             int wallpaperAnimLayerAdj) {
 
@@ -193,7 +193,7 @@
      * @return The index in {@param windows} of the lowest window that is currently on screen and
      *         not hidden by the policy.
      */
-    private int findLowestWindowOnScreen(ReadOnlyWindowList windowList) {
+    private int findLowestWindowOnScreen(WindowList windowList) {
         final int size = windowList.size();
         for (int index = 0; index < size; index++) {
             final WindowState win = windowList.get(index);
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 62ad217..150160c 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -23,6 +23,7 @@
 import java.util.Comparator;
 import java.util.LinkedList;
 import java.util.function.Consumer;
+import java.util.function.Predicate;
 
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
@@ -508,6 +509,17 @@
         }
     }
 
+    WindowState getWindow(Predicate<WindowState> callback) {
+        for (int i = mChildren.size() - 1; i >= 0; --i) {
+            final WindowState w = mChildren.get(i).getWindow(callback);
+            if (w != null) {
+                return w;
+            }
+        }
+
+        return null;
+    }
+
     /**
      * Returns 1, 0, or -1 depending on if this container is greater than, equal to, or lesser than
      * the input container in terms of z-order.
diff --git a/services/core/java/com/android/server/wm/WindowLayersController.java b/services/core/java/com/android/server/wm/WindowLayersController.java
index d94094a..c06e5cc 100644
--- a/services/core/java/com/android/server/wm/WindowLayersController.java
+++ b/services/core/java/com/android/server/wm/WindowLayersController.java
@@ -60,33 +60,32 @@
     private ArrayDeque<WindowState> mOnTopLauncherWindows = new ArrayDeque<>();
     private WindowState mDockDivider = null;
     private ArrayDeque<WindowState> mReplacingWindows = new ArrayDeque<>();
+    private int mCurBaseLayer;
+    private int mCurLayer;
+    private boolean mAnyLayerChanged;
 
-    final void assignWindowLayers(ReadOnlyWindowList windows) {
-        if (DEBUG_LAYERS) Slog.v(TAG_WM, "Assigning layers based on windows=" + windows,
+    final void assignWindowLayers(DisplayContent dc) {
+        if (DEBUG_LAYERS) Slog.v(TAG_WM, "Assigning layers based",
                 new RuntimeException("here").fillInStackTrace());
 
         clear();
-        int curBaseLayer = 0;
-        int curLayer = 0;
-        boolean anyLayerChanged = false;
-        for (int i = 0, windowCount = windows.size(); i < windowCount; i++) {
-            final WindowState w = windows.get(i);
+        dc.forAllWindows((w) -> {
             boolean layerChanged = false;
 
             int oldLayer = w.mLayer;
-            if (w.mBaseLayer == curBaseLayer || w.mIsImWindow || (i > 0 && w.mIsWallpaper)) {
-                curLayer += WINDOW_LAYER_MULTIPLIER;
+            if (w.mBaseLayer == mCurBaseLayer || w.mIsImWindow) {
+                mCurLayer += WINDOW_LAYER_MULTIPLIER;
             } else {
-                curBaseLayer = curLayer = w.mBaseLayer;
+                mCurBaseLayer = mCurLayer = w.mBaseLayer;
             }
-            assignAnimLayer(w, curLayer);
+            assignAnimLayer(w, mCurLayer);
 
-            // TODO: Preserved old behavior of code here but not sure comparing
-            // oldLayer to mAnimLayer and mLayer makes sense...though the
-            // worst case would be unintentional layer reassignment.
+            // TODO: Preserved old behavior of code here but not sure comparing oldLayer to
+            // mAnimLayer and mLayer makes sense...though the worst case would be unintentional
+            // layer reassignment.
             if (w.mLayer != oldLayer || w.mWinAnimator.mAnimLayer != oldLayer) {
                 layerChanged = true;
-                anyLayerChanged = true;
+                mAnyLayerChanged = true;
             }
 
             if (w.mAppToken != null) {
@@ -98,28 +97,27 @@
             if (layerChanged) {
                 w.scheduleAnimationIfDimming();
             }
-        }
+        }, false /* traverseTopToBottom */);
 
         adjustSpecialWindows();
 
         //TODO (multidisplay): Magnification is supported only for the default display.
-        if (mService.mAccessibilityController != null && anyLayerChanged
-                && windows.get(windows.size() - 1).getDisplayId() == Display.DEFAULT_DISPLAY) {
+        if (mService.mAccessibilityController != null && mAnyLayerChanged
+                && dc.getDisplayId() == Display.DEFAULT_DISPLAY) {
             mService.mAccessibilityController.onWindowLayersChangedLocked();
         }
 
-        if (DEBUG_LAYERS) logDebugLayers(windows);
+        if (DEBUG_LAYERS) logDebugLayers(dc);
     }
 
-    private void logDebugLayers(ReadOnlyWindowList windows) {
-        for (int i = 0, n = windows.size(); i < n; i++) {
-            final WindowState w = windows.get(i);
+    private void logDebugLayers(DisplayContent dc) {
+        dc.forAllWindows((w) -> {
             final WindowStateAnimator winAnimator = w.mWinAnimator;
             Slog.v(TAG_WM, "Assign layer " + w + ": " + "mBase=" + w.mBaseLayer
                     + " mLayer=" + w.mLayer + (w.mAppToken == null
                     ? "" : " mAppLayer=" + w.mAppToken.mAppAnimator.animLayerAdjustment)
                     + " =mAnimLayer=" + winAnimator.mAnimLayer);
-        }
+        }, false /* traverseTopToBottom */);
     }
 
     private void clear() {
@@ -130,6 +128,10 @@
         mOnTopLauncherWindows.clear();
         mReplacingWindows.clear();
         mDockDivider = null;
+
+        mCurBaseLayer = 0;
+        mCurLayer = 0;
+        mAnyLayerChanged = false;
     }
 
     private void collectSpecialWindows(WindowState w) {
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 5c9dc10..c4c4bcd 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -4787,37 +4787,42 @@
             return false;
         }
 
-        final DisplayContent displayContent = mRoot.getDisplayContent(displayId);
-        final ReadOnlyWindowList windows = displayContent.getReadOnlyWindowList();
+        final DisplayContent dc = mRoot.getDisplayContent(displayId);
 
         final int oldRotation = mRotation;
         int rotation = mPolicy.rotationForOrientationLw(mLastOrientation, mRotation);
-        boolean rotateSeamlessly = mPolicy.shouldRotateSeamlessly(oldRotation, rotation);
+        final boolean rotateSeamlessly;
 
-        if (rotateSeamlessly) {
-            for (int i = windows.size() - 1; i >= 0; i--) {
-                WindowState w = windows.get(i);
+        if (mPolicy.shouldRotateSeamlessly(oldRotation, rotation)) {
+            final WindowState seamlessRotated = dc.getWindow((w) -> w.mSeamlesslyRotated);
+            if (seamlessRotated != null) {
                 // We can't rotate (seamlessly or not) while waiting for the last seamless rotation
                 // to complete (that is, waiting for windows to redraw). It's tempting to check
-                // w.mSeamlessRotationCount but that could be incorrect in the case of window-removal.
-                if (w.mSeamlesslyRotated) {
-                    return false;
-                }
-                // In what can only be called an unfortunate workaround we require
-                // seamlessly rotated child windows to have the TRANSFORM_TO_DISPLAY_INVERSE
-                // flag. Due to limitations in the client API, there is no way for
-                // the client to set this flag in a race free fashion. If we seamlessly rotate
-                // a window which does not have this flag, but then gains it, we will get
-                // an incorrect visual result (rotated viewfinder). This means if we want to
-                // support seamlessly rotating windows which could gain this flag, we can't
-                // rotate windows without it. This limits seamless rotation in N to camera framework
-                // users, windows without children, and native code. This is unfortunate but
-                // having the camera work is our primary goal.
-                if (w.isChildWindow() & w.isVisibleNow() &&
-                        !w.mWinAnimator.mSurfaceController.getTransformToDisplayInverse()) {
-                    rotateSeamlessly = false;
-                }
+                // w.mSeamlessRotationCount but that could be incorrect in the case of
+                // window-removal.
+                return false;
             }
+
+            final WindowState cantSeamlesslyRotate = dc.getWindow((w) ->
+                    w.isChildWindow() && w.isVisibleNow()
+                            && !w.mWinAnimator.mSurfaceController.getTransformToDisplayInverse());
+            if (cantSeamlesslyRotate != null) {
+                // In what can only be called an unfortunate workaround we require seamlessly
+                // rotated child windows to have the TRANSFORM_TO_DISPLAY_INVERSE flag. Due to
+                // limitations in the client API, there is no way for the client to set this flag in
+                // a race free fashion. If we seamlessly rotate a window which does not have this
+                // flag, but then gains it, we will get an incorrect visual result
+                // (rotated viewfinder). This means if we want to support seamlessly rotating
+                // windows which could gain this flag, we can't rotate windows without it. This
+                // limits seamless rotation in N to camera framework users, windows without
+                // children, and native code. This is unfortunate but having the camera work is our
+                // primary goal.
+                rotateSeamlessly = false;
+            } else {
+                rotateSeamlessly = true;
+            }
+        } else {
+            rotateSeamlessly = false;
         }
 
         // TODO: Implement forced rotation changes.
@@ -4849,9 +4854,9 @@
         mH.removeMessages(H.WINDOW_FREEZE_TIMEOUT);
         mH.sendEmptyMessageDelayed(H.WINDOW_FREEZE_TIMEOUT, WINDOW_FREEZE_TIMEOUT_DURATION);
         mWaitingForConfig = true;
-        displayContent.setLayoutNeeded();
+        dc.setLayoutNeeded();
         final int[] anim = new int[2];
-        if (displayContent.isDimming()) {
+        if (dc.isDimming()) {
             anim[0] = anim[1] = 0;
         } else {
             mPolicy.selectRotationAnimationLw(anim);
@@ -4860,8 +4865,7 @@
         if (!rotateSeamlessly) {
             startFreezingDisplayLocked(inTransaction, anim[0], anim[1]);
             // startFreezingDisplayLocked can reset the ScreenRotationAnimation.
-            screenRotationAnimation =
-                mAnimator.getScreenRotationAnimationLocked(displayId);
+            screenRotationAnimation = mAnimator.getScreenRotationAnimationLocked(displayId);
         } else {
             // The screen rotation animation uses a screenshot to freeze the screen
             // while windows resize underneath.
@@ -4879,9 +4883,9 @@
         // the top of the method, the caller is obligated to call computeNewConfigurationLocked().
         // By updating the Display info here it will be available to
         // computeScreenConfigurationLocked later.
-        updateDisplayAndOrientationLocked(displayContent.getConfiguration().uiMode, displayId);
+        updateDisplayAndOrientationLocked(dc.getConfiguration().uiMode, displayId);
 
-        final DisplayInfo displayInfo = displayContent.getDisplayInfo();
+        final DisplayInfo displayInfo = dc.getDisplayInfo();
         if (!inTransaction) {
             if (SHOW_TRANSACTIONS) {
                 Slog.i(TAG_WM, ">>> OPEN TRANSACTION setRotationUnchecked");
@@ -4902,10 +4906,9 @@
             }
 
             if (rotateSeamlessly) {
-                for (int i = windows.size() - 1; i >= 0; i--) {
-                    WindowState w = windows.get(i);
-                    w.mWinAnimator.seamlesslyRotateWindow(oldRotation, mRotation);
-                }
+                dc.forAllWindows((w) ->
+                        w.mWinAnimator.seamlesslyRotateWindow(oldRotation, mRotation),
+                        true /* traverseTopToBottom */);
             }
 
             mDisplayManagerInternal.performTraversalInTransactionFromWindowManager();
@@ -4918,8 +4921,7 @@
             }
         }
 
-        for (int i = windows.size() - 1; i >= 0; i--) {
-            WindowState w = windows.get(i);
+        dc.forAllWindows((w) -> {
             // Discard surface after orientation change, these can't be reused.
             if (w.mAppToken != null) {
                 w.mAppToken.destroySavedSurfaces();
@@ -4930,7 +4932,8 @@
                 mRoot.mOrientationChangeComplete = false;
                 w.mLastFreezeDuration = 0;
             }
-        }
+
+        }, true /* traverseTopToBottom */);
 
         if (rotateSeamlessly) {
             mH.removeMessages(H.SEAMLESS_ROTATION_TIMEOUT);
@@ -4948,7 +4951,7 @@
         // Announce rotation only if we will not animate as we already have the
         // windows in final state. Otherwise, we make this call at the rotation end.
         if (screenRotationAnimation == null && mAccessibilityController != null
-                && displayContent.getDisplayId() == DEFAULT_DISPLAY) {
+                && dc.getDisplayId() == DEFAULT_DISPLAY) {
             mAccessibilityController.onRotationChangedLocked(getDefaultDisplayContentLocked(),
                     rotation);
         }
@@ -5181,7 +5184,7 @@
 
         final WindowList windows = new WindowList();
         synchronized (mWindowMap) {
-            mRoot.getWindows(windows);
+            mRoot.forAllWindows(windows::add, false /* traverseTopToBottom */);
         }
 
         BufferedWriter out = null;
@@ -5408,7 +5411,7 @@
         }
 
         synchronized (mWindowMap) {
-            return mRoot.findWindow(hashCode);
+            return mRoot.getWindow((w) -> System.identityHashCode(w) == hashCode);
         }
     }
 
@@ -8091,7 +8094,12 @@
                     mRoot.dumpDisplayContents(pw);
                 }
 
-                mRoot.getWindows(windows, visibleOnly, appsOnly);
+                mRoot.forAllWindows((w) -> {
+                    if ((!visibleOnly || w.mWinAnimator.getShown())
+                            && (!appsOnly || w.mAppToken != null)) {
+                        windows.add(w);
+                    }
+                }, true /* traverseTopToBottom */);
             }
         } else {
             synchronized(mWindowMap) {
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 1a56518..5e65aec 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -60,6 +60,7 @@
 import java.util.Comparator;
 import java.util.LinkedList;
 import java.util.function.Consumer;
+import java.util.function.Predicate;
 
 import static android.app.ActivityManager.StackId;
 import static android.app.ActivityManager.StackId.DOCKED_STACK_ID;
@@ -138,52 +139,6 @@
 import static com.android.server.wm.WindowStateAnimator.READY_TO_SHOW;
 
 class WindowList extends ArrayList<WindowState> {
-
-    /**
-     * Read-only interface for the window list that the creator of the window list can pass-out to
-     * other users to prevent them from modifying the window list.
-     */
-    private ReadOnlyWindowList mReadOnly;
-
-    WindowList() {
-        mReadOnly = new ReadOnlyWindowList(this);
-    }
-
-    /** Returns the read-only interface for this window list. */
-    ReadOnlyWindowList getReadOnly() {
-        return mReadOnly;
-    }
-}
-
-/**
- * Read-only interface for a list of windows. It is common for the owner of a list of windows to
- * want to provide a way for external classes to iterate of its windows, but prevent them from
- * modifying the list in any way. This call provides a way for them to do that by wrapping the
- * original window list and only exposing the read-only APIs.
- */
-final class ReadOnlyWindowList {
-    // List of windows this read-only class is tied to.
-    private final WindowList mWindows;
-
-    ReadOnlyWindowList(WindowList windows) {
-        mWindows = windows;
-    }
-
-    WindowState get(int index) {
-        return mWindows.get(index);
-    }
-
-    int indexOf(WindowState w) {
-        return mWindows.indexOf(w);
-    }
-
-    int size() {
-        return mWindows.size();
-    }
-
-    boolean isEmpty() {
-        return mWindows.isEmpty();
-    }
 }
 
 /** A window in the window manager. */
@@ -3949,6 +3904,13 @@
         }
     }
 
+    WindowState getWindow(Predicate<WindowState> callback) {
+        if (callback.test(this)) {
+            return this;
+        }
+        return super.getWindow(callback);
+    }
+
     boolean isWindowAnimationSet() {
         if (mWinAnimator.isWindowAnimationSet()) {
             return true;
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 7f8b9b8..b687e09 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -490,9 +490,12 @@
              * to listen for events.
              */
             if (Intent.ACTION_USER_STARTED.equals(action)
-                    && userHandle == mOwners.getDeviceOwnerUserId()
-                    && isNetworkLoggingEnabledInternal()) {
-                setNetworkLoggingActiveInternal(true);
+                    && userHandle == mOwners.getDeviceOwnerUserId()) {
+                synchronized (DevicePolicyManagerService.this) {
+                    if (isNetworkLoggingEnabledInternalLocked()) {
+                        setNetworkLoggingActiveInternal(true);
+                    }
+                }
             }
             if (Intent.ACTION_BOOT_COMPLETED.equals(action)
                     && userHandle == mOwners.getDeviceOwnerUserId()
@@ -9515,7 +9518,7 @@
         Preconditions.checkNotNull(admin);
         ensureDeviceOwnerManagingSingleUser(admin);
 
-        if (enabled == isNetworkLoggingEnabledInternal()) {
+        if (enabled == isNetworkLoggingEnabledInternalLocked()) {
             // already in the requested state
             return;
         }
@@ -9556,11 +9559,11 @@
         Preconditions.checkNotNull(admin);
         synchronized (this) {
             getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
-            return isNetworkLoggingEnabledInternal();
+            return isNetworkLoggingEnabledInternalLocked();
         }
     }
 
-    private synchronized boolean isNetworkLoggingEnabledInternal() {
+    private boolean isNetworkLoggingEnabledInternalLocked() {
         ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
         return (deviceOwner != null) && deviceOwner.isNetworkLoggingEnabled;
     }
@@ -9571,7 +9574,8 @@
      * Ideally this would be done with ParceledList, however it only supports homogeneous types.
      */
     @Override
-    public synchronized List<NetworkEvent> retrieveNetworkLogs(ComponentName admin) {
+    public synchronized List<NetworkEvent> retrieveNetworkLogs(ComponentName admin,
+            long batchToken) {
         if (!mHasFeature) {
             return null;
         }
@@ -9581,6 +9585,8 @@
         if (mNetworkLogger == null) {
             return null;
         }
-        return isNetworkLoggingEnabledInternal() ? mNetworkLogger.retrieveLogs() : null;
+        return isNetworkLoggingEnabledInternalLocked()
+                ? mNetworkLogger.retrieveLogs(batchToken)
+                : null;
     }
 }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLogger.java b/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLogger.java
index 185ccc2..8cb13da 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLogger.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLogger.java
@@ -147,7 +147,7 @@
         }
     }
 
-    List<NetworkEvent> retrieveLogs() {
-        return mNetworkLoggingHandler.retrieveFullLogBatch();
+    List<NetworkEvent> retrieveLogs(long batchToken) {
+        return mNetworkLoggingHandler.retrieveFullLogBatch(batchToken);
     }
 }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLoggingHandler.java b/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLoggingHandler.java
index 96884e6..98e5570 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLoggingHandler.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLoggingHandler.java
@@ -55,11 +55,15 @@
     private final DevicePolicyManagerService mDpm;
 
     // threadsafe as it's Handler's thread confined
+    @GuardedBy("this")
     private ArrayList<NetworkEvent> mNetworkEvents = new ArrayList<NetworkEvent>();
 
     @GuardedBy("this")
     private ArrayList<NetworkEvent> mFullBatch;
 
+    @GuardedBy("this")
+    private long currentFullBatchToken;
+
     NetworkLoggingHandler(Looper looper, DevicePolicyManagerService dpm) {
         super(looper);
         mDpm = dpm;
@@ -97,17 +101,21 @@
         scheduleBatchFinalization(BATCH_FINALIZATION_TIMEOUT_MS);
         // notify DO that there's a new non-empty batch waiting
         if (mFullBatch.size() > 0) {
-            mDpm.sendDeviceOwnerCommand(DeviceAdminReceiver.ACTION_NETWORK_LOGS_AVAILABLE,
-                    /* extras */ null);
+            currentFullBatchToken++;
+            Bundle extras = new Bundle();
+            extras.putLong(DeviceAdminReceiver.EXTRA_NETWORK_LOGS_TOKEN, currentFullBatchToken);
+            extras.putInt(DeviceAdminReceiver.EXTRA_NETWORK_LOGS_COUNT, mFullBatch.size());
+            mDpm.sendDeviceOwnerCommand(DeviceAdminReceiver.ACTION_NETWORK_LOGS_AVAILABLE, extras);
         } else {
             mFullBatch = null;
         }
     }
 
-    synchronized List<NetworkEvent> retrieveFullLogBatch() {
-        List<NetworkEvent> ret = mFullBatch;
-        mFullBatch = null;
-        return ret;
+    synchronized List<NetworkEvent> retrieveFullLogBatch(long batchToken) {
+        if (batchToken != currentFullBatchToken) {
+            return null;
+        }
+        return mFullBatch;
     }
 }
 
diff --git a/telecomm/java/android/telecom/PhoneAccount.java b/telecomm/java/android/telecom/PhoneAccount.java
index 473e394..0457d63 100644
--- a/telecomm/java/android/telecom/PhoneAccount.java
+++ b/telecomm/java/android/telecom/PhoneAccount.java
@@ -114,7 +114,10 @@
     public static final int CAPABILITY_SIM_SUBSCRIPTION = 0x4;
 
     /**
-     * Flag indicating that this {@code PhoneAccount} is capable of placing video calls.
+     * Flag indicating that this {@code PhoneAccount} is currently able to place video calls.
+     * <p>
+     * See also {@link #CAPABILITY_SUPPORTS_VIDEO_CALLING} which indicates whether the
+     * {@code PhoneAccount} supports placing video calls.
      * <p>
      * See {@link #getCapabilities}
      */
@@ -179,6 +182,23 @@
     public static final int CAPABILITY_EMERGENCY_VIDEO_CALLING = 0x200;
 
     /**
+     * Flag indicating that this {@link PhoneAccount} supports video calling.
+     * This is not an indication that the {@link PhoneAccount} is currently able to make a video
+     * call, but rather that it has the ability to make video calls (but not necessarily at this
+     * time).
+     * <p>
+     * Whether a {@link PhoneAccount} can make a video call is ultimately controlled by
+     * {@link #CAPABILITY_VIDEO_CALLING}, which indicates whether the {@link PhoneAccount} is
+     * currently capable of making a video call.  Consider a case where, for example, a
+     * {@link PhoneAccount} supports making video calls (e.g.
+     * {@link #CAPABILITY_SUPPORTS_VIDEO_CALLING}), but a current lack of network connectivity
+     * prevents video calls from being made (e.g. {@link #CAPABILITY_VIDEO_CALLING}).
+     * <p>
+     * See {@link #getCapabilities}
+     */
+    public static final int CAPABILITY_SUPPORTS_VIDEO_CALLING = 0x400;
+
+    /**
      * URI scheme for telephone number URIs.
      */
     public static final String SCHEME_TEL = "tel";
@@ -762,6 +782,9 @@
      */
     private String capabilitiesToString(int capabilities) {
         StringBuilder sb = new StringBuilder();
+        if (hasCapabilities(CAPABILITY_SUPPORTS_VIDEO_CALLING)) {
+            sb.append("SuppVideo ");
+        }
         if (hasCapabilities(CAPABILITY_VIDEO_CALLING)) {
             sb.append("Video ");
         }
diff --git a/telephony/java/android/telephony/CellSignalStrengthLte.java b/telephony/java/android/telephony/CellSignalStrengthLte.java
index 3c0a8d6..434caad 100644
--- a/telephony/java/android/telephony/CellSignalStrengthLte.java
+++ b/telephony/java/android/telephony/CellSignalStrengthLte.java
@@ -168,20 +168,34 @@
     }
 
     /**
-     * @hide
+     * Get reference signal received quality
      */
     public int getRsrq() {
         return mRsrq;
     }
 
     /**
-     * @hide
+     * Get reference signal signal-to-noise ratio
      */
     public int getRssnr() {
         return mRssnr;
     }
 
     /**
+     * Get reference signal received power
+     */
+    public int getRsrp() {
+        return mRsrp;
+    }
+
+    /**
+     * Get channel quality indicator
+     */
+    public int getCqi() {
+        return mCqi;
+    }
+
+    /**
      * Get signal strength as dBm
      */
     @Override
diff --git a/test-runner/src/android/test/mock/MockContentProvider.java b/test-runner/src/android/test/mock/MockContentProvider.java
index 5ef71df..e443911 100644
--- a/test-runner/src/android/test/mock/MockContentProvider.java
+++ b/test-runner/src/android/test/mock/MockContentProvider.java
@@ -147,6 +147,12 @@
         public Uri uncanonicalize(String callingPkg, Uri uri) throws RemoteException {
             return MockContentProvider.this.uncanonicalize(uri);
         }
+
+        @Override
+        public boolean refresh(String callingPkg, Uri url, Bundle args,
+                ICancellationSignal cancellationSignal) throws RemoteException {
+            return MockContentProvider.this.refresh(url, args);
+        }
     }
     private final InversionIContentProvider mIContentProvider = new InversionIContentProvider();
 
@@ -251,6 +257,13 @@
     }
 
     /**
+     * @hide
+     */
+    public boolean refresh(Uri url, Bundle args) {
+        throw new UnsupportedOperationException("unimplemented mock method call");
+    }
+
+    /**
      * Returns IContentProvider which calls back same methods in this class.
      * By overriding this class, we avoid the mechanism hidden behind ContentProvider
      * (IPC, etc.)
diff --git a/test-runner/src/android/test/mock/MockIContentProvider.java b/test-runner/src/android/test/mock/MockIContentProvider.java
index ee8c376..09d45d1 100644
--- a/test-runner/src/android/test/mock/MockIContentProvider.java
+++ b/test-runner/src/android/test/mock/MockIContentProvider.java
@@ -125,4 +125,10 @@
     public Uri uncanonicalize(String callingPkg, Uri uri) throws RemoteException {
         throw new UnsupportedOperationException("unimplemented mock method");
     }
+
+    @Override
+    public boolean refresh(String callingPkg, Uri url, Bundle args,
+            ICancellationSignal cancellationSignal) throws RemoteException {
+        throw new UnsupportedOperationException("unimplemented mock method");
+    }
 }
diff --git a/tools/layoutlib/bridge/src/android/graphics/Bitmap_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Bitmap_Delegate.java
index e1fc5ec..2ae4654 100644
--- a/tools/layoutlib/bridge/src/android/graphics/Bitmap_Delegate.java
+++ b/tools/layoutlib/bridge/src/android/graphics/Bitmap_Delegate.java
@@ -326,7 +326,7 @@
 
     @LayoutlibDelegate
     /*package*/ static void nativeReconfigure(long nativeBitmap, int width, int height,
-            int config, int allocSize, boolean isPremultiplied) {
+            int config, boolean isPremultiplied) {
         Bridge.getLog().error(LayoutLog.TAG_UNSUPPORTED,
                 "Bitmap.reconfigure() is not supported", null /*data*/);
     }
@@ -600,6 +600,22 @@
         return Arrays.equals(argb1, argb2);
     }
 
+    @LayoutlibDelegate
+    /*package*/ static int nativeGetAllocationByteCount(long nativeBitmap) {
+        // get the delegate from the native int.
+        Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap);
+        if (delegate == null) {
+            return 0;
+        }
+        return nativeRowBytes(nativeBitmap) * delegate.mImage.getHeight();
+
+    }
+
+    @LayoutlibDelegate
+    /*package*/ static void nativePrepareToDraw(long nativeBitmap) {
+        // do nothing as Bitmap_Delegate does not have caches
+    }
+
     // ---- Private delegate/helper methods ----
 
     private Bitmap_Delegate(BufferedImage image, Config config) {
diff --git a/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java
index 4a4c6c8..94152cd 100644
--- a/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java
+++ b/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java
@@ -110,17 +110,17 @@
     // ---- native methods ----
 
     @LayoutlibDelegate
-    /*package*/ static void freeCaches() {
+    /*package*/ static void nFreeCaches() {
         // nothing to be done here.
     }
 
     @LayoutlibDelegate
-    /*package*/ static void freeTextLayoutCaches() {
+    /*package*/ static void nFreeTextLayoutCaches() {
         // nothing to be done here yet.
     }
 
     @LayoutlibDelegate
-    /*package*/ static long initRaster(@Nullable Bitmap bitmap) {
+    /*package*/ static long nInitRaster(@Nullable Bitmap bitmap) {
         long nativeBitmapOrZero = 0;
         if (bitmap != null) {
             nativeBitmapOrZero = bitmap.getNativeInstance();
@@ -142,7 +142,7 @@
     }
 
     @LayoutlibDelegate
-    public static void native_setBitmap(long canvas, Bitmap bitmap) {
+    public static void nSetBitmap(long canvas, Bitmap bitmap) {
         Canvas_Delegate canvasDelegate = sManager.getDelegate(canvas);
         Bitmap_Delegate bitmapDelegate = Bitmap_Delegate.getDelegate(bitmap);
         if (canvasDelegate == null || bitmapDelegate==null) {
@@ -153,7 +153,7 @@
     }
 
     @LayoutlibDelegate
-    public static boolean native_isOpaque(long nativeCanvas) {
+    public static boolean nIsOpaque(long nativeCanvas) {
         // get the delegate from the native int.
         Canvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas);
         if (canvasDelegate == null) {
@@ -164,10 +164,10 @@
     }
 
     @LayoutlibDelegate
-    public static void native_setHighContrastText(long nativeCanvas, boolean highContrastText){}
+    public static void nSetHighContrastText(long nativeCanvas, boolean highContrastText){}
 
     @LayoutlibDelegate
-    public static int native_getWidth(long nativeCanvas) {
+    public static int nGetWidth(long nativeCanvas) {
         // get the delegate from the native int.
         Canvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas);
         if (canvasDelegate == null) {
@@ -178,7 +178,7 @@
     }
 
     @LayoutlibDelegate
-    public static int native_getHeight(long nativeCanvas) {
+    public static int nGetHeight(long nativeCanvas) {
         // get the delegate from the native int.
         Canvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas);
         if (canvasDelegate == null) {
@@ -189,7 +189,7 @@
     }
 
     @LayoutlibDelegate
-    public static int native_save(long nativeCanvas, int saveFlags) {
+    public static int nSave(long nativeCanvas, int saveFlags) {
         // get the delegate from the native int.
         Canvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas);
         if (canvasDelegate == null) {
@@ -200,7 +200,7 @@
     }
 
     @LayoutlibDelegate
-    public static int native_saveLayer(long nativeCanvas, float l,
+    public static int nSaveLayer(long nativeCanvas, float l,
                                                float t, float r, float b,
                                                long paint, int layerFlags) {
         // get the delegate from the native int.
@@ -219,7 +219,7 @@
     }
 
     @LayoutlibDelegate
-    public static int native_saveLayerAlpha(long nativeCanvas, float l,
+    public static int nSaveLayerAlpha(long nativeCanvas, float l,
                                                     float t, float r, float b,
                                                     int alpha, int layerFlags) {
         // get the delegate from the native int.
@@ -232,7 +232,7 @@
     }
 
     @LayoutlibDelegate
-    public static void native_restore(long nativeCanvas, boolean throwOnUnderflow) {
+    public static void nRestore(long nativeCanvas, boolean throwOnUnderflow) {
         // FIXME: implement throwOnUnderflow.
         // get the delegate from the native int.
         Canvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas);
@@ -244,7 +244,7 @@
     }
 
     @LayoutlibDelegate
-    public static void native_restoreToCount(long nativeCanvas, int saveCount,
+    public static void nRestoreToCount(long nativeCanvas, int saveCount,
             boolean throwOnUnderflow) {
         // FIXME: implement throwOnUnderflow.
         // get the delegate from the native int.
@@ -257,7 +257,7 @@
     }
 
     @LayoutlibDelegate
-    public static int native_getSaveCount(long nativeCanvas) {
+    public static int nGetSaveCount(long nativeCanvas) {
         // get the delegate from the native int.
         Canvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas);
         if (canvasDelegate == null) {
@@ -268,7 +268,7 @@
     }
 
     @LayoutlibDelegate
-   public static void native_translate(long nativeCanvas, float dx, float dy) {
+   public static void nTranslate(long nativeCanvas, float dx, float dy) {
         // get the delegate from the native int.
         Canvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas);
         if (canvasDelegate == null) {
@@ -279,7 +279,7 @@
     }
 
     @LayoutlibDelegate
-       public static void native_scale(long nativeCanvas, float sx, float sy) {
+       public static void nScale(long nativeCanvas, float sx, float sy) {
             // get the delegate from the native int.
             Canvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas);
             if (canvasDelegate == null) {
@@ -290,7 +290,7 @@
         }
 
     @LayoutlibDelegate
-    public static void native_rotate(long nativeCanvas, float degrees) {
+    public static void nRotate(long nativeCanvas, float degrees) {
         // get the delegate from the native int.
         Canvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas);
         if (canvasDelegate == null) {
@@ -301,7 +301,7 @@
     }
 
     @LayoutlibDelegate
-   public static void native_skew(long nativeCanvas, float kx, float ky) {
+   public static void nSkew(long nativeCanvas, float kx, float ky) {
         // get the delegate from the native int.
         Canvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas);
         if (canvasDelegate == null) {
@@ -325,7 +325,7 @@
     }
 
     @LayoutlibDelegate
-    public static void native_concat(long nCanvas, long nMatrix) {
+    public static void nConcat(long nCanvas, long nMatrix) {
         // get the delegate from the native int.
         Canvas_Delegate canvasDelegate = sManager.getDelegate(nCanvas);
         if (canvasDelegate == null) {
@@ -353,7 +353,7 @@
     }
 
     @LayoutlibDelegate
-    public static void native_setMatrix(long nCanvas, long nMatrix) {
+    public static void nSetMatrix(long nCanvas, long nMatrix) {
         // get the delegate from the native int.
         Canvas_Delegate canvasDelegate = sManager.getDelegate(nCanvas);
         if (canvasDelegate == null) {
@@ -383,7 +383,7 @@
     }
 
     @LayoutlibDelegate
-    public static boolean native_clipRect(long nCanvas,
+    public static boolean nClipRect(long nCanvas,
                                                   float left, float top,
                                                   float right, float bottom,
                                                   int regionOp) {
@@ -397,7 +397,7 @@
     }
 
     @LayoutlibDelegate
-    public static boolean native_clipPath(long nativeCanvas,
+    public static boolean nClipPath(long nativeCanvas,
                                                   long nativePath,
                                                   int regionOp) {
         Canvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas);
@@ -414,7 +414,7 @@
     }
 
     @LayoutlibDelegate
-    public static boolean native_clipRegion(long nativeCanvas,
+    public static boolean nClipRegion(long nativeCanvas,
                                                     long nativeRegion,
                                                     int regionOp) {
         Canvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas);
@@ -431,7 +431,7 @@
     }
 
     @LayoutlibDelegate
-    public static void nativeSetDrawFilter(long nativeCanvas, long nativeFilter) {
+    public static void nSetDrawFilter(long nativeCanvas, long nativeFilter) {
         Canvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas);
         if (canvasDelegate == null) {
             return;
@@ -446,7 +446,7 @@
     }
 
     @LayoutlibDelegate
-    public static boolean native_getClipBounds(long nativeCanvas,
+    public static boolean nGetClipBounds(long nativeCanvas,
                                                        Rect bounds) {
         // get the delegate from the native int.
         Canvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas);
@@ -467,7 +467,7 @@
     }
 
     @LayoutlibDelegate
-    public static void native_getCTM(long canvas, long matrix) {
+    public static void nGetCTM(long canvas, long matrix) {
         // get the delegate from the native int.
         Canvas_Delegate canvasDelegate = sManager.getDelegate(canvas);
         if (canvasDelegate == null) {
@@ -484,13 +484,13 @@
     }
 
     @LayoutlibDelegate
-    public static boolean native_quickReject(long nativeCanvas, long path) {
+    public static boolean nQuickReject(long nativeCanvas, long path) {
         // FIXME properly implement quickReject
         return false;
     }
 
     @LayoutlibDelegate
-    public static boolean native_quickReject(long nativeCanvas,
+    public static boolean nQuickReject(long nativeCanvas,
                                                      float left, float top,
                                                      float right, float bottom) {
         // FIXME properly implement quickReject
@@ -498,7 +498,7 @@
     }
 
     @LayoutlibDelegate
-    public static void native_drawColor(long nativeCanvas, final int color, final int mode) {
+    public static void nDrawColor(long nativeCanvas, final int color, final int mode) {
         // get the delegate from the native int.
         Canvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas);
         if (canvasDelegate == null) {
@@ -529,14 +529,14 @@
     }
 
     @LayoutlibDelegate
-    public static void native_drawPaint(long nativeCanvas, long paint) {
+    public static void nDrawPaint(long nativeCanvas, long paint) {
         // FIXME
         Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
                 "Canvas.drawPaint is not supported.", null, null /*data*/);
     }
 
     @LayoutlibDelegate
-    public static void native_drawPoint(long nativeCanvas, float x, float y,
+    public static void nDrawPoint(long nativeCanvas, float x, float y,
             long nativePaint) {
         // FIXME
         Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
@@ -544,7 +544,7 @@
     }
 
     @LayoutlibDelegate
-    public static void native_drawPoints(long nativeCanvas, float[] pts, int offset, int count,
+    public static void nDrawPoints(long nativeCanvas, float[] pts, int offset, int count,
             long nativePaint) {
         // FIXME
         Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
@@ -552,7 +552,7 @@
     }
 
     @LayoutlibDelegate
-    public static void native_drawLine(long nativeCanvas,
+    public static void nDrawLine(long nativeCanvas,
             final float startX, final float startY, final float stopX, final float stopY,
             long paint) {
         draw(nativeCanvas, paint, false /*compositeOnly*/, false /*forceSrcMode*/,
@@ -565,7 +565,7 @@
     }
 
     @LayoutlibDelegate
-    public static void native_drawLines(long nativeCanvas,
+    public static void nDrawLines(long nativeCanvas,
             final float[] pts, final int offset, final int count,
             long nativePaint) {
         draw(nativeCanvas, nativePaint, false /*compositeOnly*/,
@@ -581,7 +581,7 @@
     }
 
     @LayoutlibDelegate
-    public static void native_drawRect(long nativeCanvas,
+    public static void nDrawRect(long nativeCanvas,
             final float left, final float top, final float right, final float bottom, long paint) {
 
         draw(nativeCanvas, paint, false /*compositeOnly*/, false /*forceSrcMode*/,
@@ -607,7 +607,7 @@
     }
 
     @LayoutlibDelegate
-    public static void native_drawOval(long nativeCanvas, final float left,
+    public static void nDrawOval(long nativeCanvas, final float left,
             final float top, final float right, final float bottom, long paint) {
         if (right > left && bottom > top) {
             draw(nativeCanvas, paint, false /*compositeOnly*/, false /*forceSrcMode*/,
@@ -634,15 +634,15 @@
     }
 
     @LayoutlibDelegate
-    public static void native_drawCircle(long nativeCanvas,
+    public static void nDrawCircle(long nativeCanvas,
             float cx, float cy, float radius, long paint) {
-        native_drawOval(nativeCanvas,
+        nDrawOval(nativeCanvas,
                 cx - radius, cy - radius, cx + radius, cy + radius,
                 paint);
     }
 
     @LayoutlibDelegate
-    public static void native_drawArc(long nativeCanvas,
+    public static void nDrawArc(long nativeCanvas,
             final float left, final float top, final float right, final float bottom,
             final float startAngle, final float sweep,
             final boolean useCenter, long paint) {
@@ -674,7 +674,7 @@
     }
 
     @LayoutlibDelegate
-    public static void native_drawRoundRect(long nativeCanvas,
+    public static void nDrawRoundRect(long nativeCanvas,
             final float left, final float top, final float right, final float bottom,
             final float rx, final float ry, long paint) {
         draw(nativeCanvas, paint, false /*compositeOnly*/, false /*forceSrcMode*/,
@@ -704,7 +704,7 @@
     }
 
     @LayoutlibDelegate
-    public static void native_drawPath(long nativeCanvas, long path, long paint) {
+    public static void nDrawPath(long nativeCanvas, long path, long paint) {
         final Path_Delegate pathDelegate = Path_Delegate.getDelegate(path);
         if (pathDelegate == null) {
             return;
@@ -756,7 +756,7 @@
     }
 
     @LayoutlibDelegate
-    public static void native_drawRegion(long nativeCanvas, long nativeRegion,
+    public static void nDrawRegion(long nativeCanvas, long nativeRegion,
             long nativePaint) {
         // FIXME
         Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
@@ -764,7 +764,7 @@
     }
 
     @LayoutlibDelegate
-    public static void native_drawNinePatch(Canvas thisCanvas, long nativeCanvas,
+    public static void nDrawNinePatch(Canvas thisCanvas, long nativeCanvas,
             long nativeBitmap, long ninePatch, final float dstLeft, final float dstTop,
             final float dstRight, final float dstBottom, long nativePaintOrZero,
             final int screenDensity, final int bitmapDensity) {
@@ -811,7 +811,7 @@
     }
 
     @LayoutlibDelegate
-    public static void native_drawBitmap(Canvas thisCanvas, long nativeCanvas, Bitmap bitmap,
+    public static void nDrawBitmap(Canvas thisCanvas, long nativeCanvas, Bitmap bitmap,
                                                  float left, float top,
                                                  long nativePaintOrZero,
                                                  int canvasDensity,
@@ -833,7 +833,7 @@
     }
 
     @LayoutlibDelegate
-    public static void native_drawBitmap(Canvas thisCanvas, long nativeCanvas, Bitmap bitmap,
+    public static void nDrawBitmap(Canvas thisCanvas, long nativeCanvas, Bitmap bitmap,
                                  float srcLeft, float srcTop, float srcRight, float srcBottom,
                                  float dstLeft, float dstTop, float dstRight, float dstBottom,
                                  long nativePaintOrZero, int screenDensity, int bitmapDensity) {
@@ -849,7 +849,7 @@
     }
 
     @LayoutlibDelegate
-    public static void native_drawBitmap(long nativeCanvas, int[] colors,
+    public static void nDrawBitmap(long nativeCanvas, int[] colors,
                                                 int offset, int stride, final float x,
                                                  final float y, int width, int height,
                                                  boolean hasAlpha,
@@ -936,25 +936,25 @@
     }
 
     @LayoutlibDelegate
-    public static void native_drawText(long nativeCanvas, char[] text, int index, int count,
+    public static void nDrawText(long nativeCanvas, char[] text, int index, int count,
             float startX, float startY, int flags, long paint, long typeface) {
         drawText(nativeCanvas, text, index, count, startX, startY, (flags & 1) != 0,
                 paint, typeface);
     }
 
     @LayoutlibDelegate
-    public static void native_drawText(long nativeCanvas, String text,
+    public static void nDrawText(long nativeCanvas, String text,
             int start, int end, float x, float y, final int flags, long paint,
             long typeface) {
         int count = end - start;
         char[] buffer = TemporaryBuffer.obtain(count);
         TextUtils.getChars(text, start, end, buffer, 0);
 
-        native_drawText(nativeCanvas, buffer, 0, count, x, y, flags, paint, typeface);
+        nDrawText(nativeCanvas, buffer, 0, count, x, y, flags, paint, typeface);
     }
 
     @LayoutlibDelegate
-    public static void native_drawTextRun(long nativeCanvas, String text,
+    public static void nDrawTextRun(long nativeCanvas, String text,
             int start, int end, int contextStart, int contextEnd,
             float x, float y, boolean isRtl, long paint, long typeface) {
         int count = end - start;
@@ -965,14 +965,14 @@
     }
 
     @LayoutlibDelegate
-    public static void native_drawTextRun(long nativeCanvas, char[] text,
+    public static void nDrawTextRun(long nativeCanvas, char[] text,
             int start, int count, int contextStart, int contextCount,
             float x, float y, boolean isRtl, long paint, long typeface) {
         drawText(nativeCanvas, text, start, count, x, y, isRtl, paint, typeface);
     }
 
     @LayoutlibDelegate
-    public static void native_drawTextOnPath(long nativeCanvas,
+    public static void nDrawTextOnPath(long nativeCanvas,
                                                      char[] text, int index,
                                                      int count, long path,
                                                      float hOffset,
@@ -984,7 +984,7 @@
     }
 
     @LayoutlibDelegate
-    public static void native_drawTextOnPath(long nativeCanvas,
+    public static void nDrawTextOnPath(long nativeCanvas,
                                                      String text, long path,
                                                      float hOffset,
                                                      float vOffset,
diff --git a/tools/layoutlib/bridge/src/android/graphics/Matrix_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Matrix_Delegate.java
index a503e50..354f919 100644
--- a/tools/layoutlib/bridge/src/android/graphics/Matrix_Delegate.java
+++ b/tools/layoutlib/bridge/src/android/graphics/Matrix_Delegate.java
@@ -27,6 +27,8 @@
 import java.awt.geom.AffineTransform;
 import java.awt.geom.NoninvertibleTransformException;
 
+import libcore.util.NativeAllocationRegistry_Delegate;
+
 /**
  * Delegate implementing the native methods of android.graphics.Matrix
  *
@@ -47,6 +49,7 @@
     // ---- delegate manager ----
     private static final DelegateManager<Matrix_Delegate> sManager =
             new DelegateManager<Matrix_Delegate>(Matrix_Delegate.class);
+    private static long sFinalizer = -1;
 
     // ---- delegate data ----
     private float mValues[] = new float[MATRIX_SIZE];
@@ -174,7 +177,7 @@
     // ---- native methods ----
 
     @LayoutlibDelegate
-    /*package*/ static long native_create(long native_src_or_zero) {
+    /*package*/ static long nCreate(long native_src_or_zero) {
         // create the delegate
         Matrix_Delegate newDelegate = new Matrix_Delegate();
 
@@ -193,7 +196,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static boolean native_isIdentity(long native_object) {
+    /*package*/ static boolean nIsIdentity(long native_object) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d == null) {
             return false;
@@ -203,7 +206,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static boolean native_isAffine(long native_object) {
+    /*package*/ static boolean nIsAffine(long native_object) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d == null) {
             return true;
@@ -213,7 +216,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static boolean native_rectStaysRect(long native_object) {
+    /*package*/ static boolean nRectStaysRect(long native_object) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d == null) {
             return true;
@@ -223,7 +226,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_reset(long native_object) {
+    /*package*/ static void nReset(long native_object) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d == null) {
             return;
@@ -233,7 +236,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_set(long native_object, long other) {
+    /*package*/ static void nSet(long native_object, long other) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d == null) {
             return;
@@ -248,7 +251,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_setTranslate(long native_object, float dx, float dy) {
+    /*package*/ static void nSetTranslate(long native_object, float dx, float dy) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d == null) {
             return;
@@ -258,7 +261,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_setScale(long native_object, float sx, float sy,
+    /*package*/ static void nSetScale(long native_object, float sx, float sy,
             float px, float py) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d == null) {
@@ -269,7 +272,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_setScale(long native_object, float sx, float sy) {
+    /*package*/ static void nSetScale(long native_object, float sx, float sy) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d == null) {
             return;
@@ -287,7 +290,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_setRotate(long native_object, float degrees, float px, float py) {
+    /*package*/ static void nSetRotate(long native_object, float degrees, float px, float py) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d == null) {
             return;
@@ -297,7 +300,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_setRotate(long native_object, float degrees) {
+    /*package*/ static void nSetRotate(long native_object, float degrees) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d == null) {
             return;
@@ -307,7 +310,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_setSinCos(long native_object, float sinValue, float cosValue,
+    /*package*/ static void nSetSinCos(long native_object, float sinValue, float cosValue,
             float px, float py) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d == null) {
@@ -326,7 +329,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_setSinCos(long native_object, float sinValue, float cosValue) {
+    /*package*/ static void nSetSinCos(long native_object, float sinValue, float cosValue) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d == null) {
             return;
@@ -336,7 +339,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_setSkew(long native_object, float kx, float ky,
+    /*package*/ static void nSetSkew(long native_object, float kx, float ky,
             float px, float py) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d == null) {
@@ -347,7 +350,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_setSkew(long native_object, float kx, float ky) {
+    /*package*/ static void nSetSkew(long native_object, float kx, float ky) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d == null) {
             return;
@@ -365,12 +368,12 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_setConcat(long native_object, long a, long b) {
+    /*package*/ static void nSetConcat(long native_object, long a, long b) {
         if (a == native_object) {
-            native_preConcat(native_object, b);
+            nPreConcat(native_object, b);
             return;
         } else if (b == native_object) {
-            native_postConcat(native_object, a);
+            nPostConcat(native_object, a);
             return;
         }
 
@@ -383,7 +386,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_preTranslate(long native_object, float dx, float dy) {
+    /*package*/ static void nPreTranslate(long native_object, float dx, float dy) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d != null) {
             d.preTransform(getTranslate(dx, dy));
@@ -391,7 +394,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_preScale(long native_object, float sx, float sy,
+    /*package*/ static void nPreScale(long native_object, float sx, float sy,
             float px, float py) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d != null) {
@@ -400,7 +403,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_preScale(long native_object, float sx, float sy) {
+    /*package*/ static void nPreScale(long native_object, float sx, float sy) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d != null) {
             d.preTransform(getScale(sx, sy));
@@ -408,7 +411,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_preRotate(long native_object, float degrees,
+    /*package*/ static void nPreRotate(long native_object, float degrees,
             float px, float py) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d != null) {
@@ -417,7 +420,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_preRotate(long native_object, float degrees) {
+    /*package*/ static void nPreRotate(long native_object, float degrees) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d != null) {
 
@@ -430,7 +433,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_preSkew(long native_object, float kx, float ky,
+    /*package*/ static void nPreSkew(long native_object, float kx, float ky,
             float px, float py) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d != null) {
@@ -439,7 +442,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_preSkew(long native_object, float kx, float ky) {
+    /*package*/ static void nPreSkew(long native_object, float kx, float ky) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d != null) {
             d.preTransform(getSkew(kx, ky));
@@ -447,7 +450,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_preConcat(long native_object, long other_matrix) {
+    /*package*/ static void nPreConcat(long native_object, long other_matrix) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         Matrix_Delegate other = sManager.getDelegate(other_matrix);
         if (d != null && other != null) {
@@ -456,7 +459,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_postTranslate(long native_object, float dx, float dy) {
+    /*package*/ static void nPostTranslate(long native_object, float dx, float dy) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d != null) {
             d.postTransform(getTranslate(dx, dy));
@@ -464,7 +467,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_postScale(long native_object, float sx, float sy,
+    /*package*/ static void nPostScale(long native_object, float sx, float sy,
             float px, float py) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d != null) {
@@ -473,7 +476,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_postScale(long native_object, float sx, float sy) {
+    /*package*/ static void nPostScale(long native_object, float sx, float sy) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d != null) {
             d.postTransform(getScale(sx, sy));
@@ -481,7 +484,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_postRotate(long native_object, float degrees,
+    /*package*/ static void nPostRotate(long native_object, float degrees,
             float px, float py) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d != null) {
@@ -490,7 +493,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_postRotate(long native_object, float degrees) {
+    /*package*/ static void nPostRotate(long native_object, float degrees) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d != null) {
             d.postTransform(getRotate(degrees));
@@ -498,7 +501,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_postSkew(long native_object, float kx, float ky,
+    /*package*/ static void nPostSkew(long native_object, float kx, float ky,
             float px, float py) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d != null) {
@@ -507,7 +510,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_postSkew(long native_object, float kx, float ky) {
+    /*package*/ static void nPostSkew(long native_object, float kx, float ky) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d != null) {
             d.postTransform(getSkew(kx, ky));
@@ -515,7 +518,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_postConcat(long native_object, long other_matrix) {
+    /*package*/ static void nPostConcat(long native_object, long other_matrix) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         Matrix_Delegate other = sManager.getDelegate(other_matrix);
         if (d != null && other != null) {
@@ -524,7 +527,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static boolean native_setRectToRect(long native_object, RectF src,
+    /*package*/ static boolean nSetRectToRect(long native_object, RectF src,
             RectF dst, int stf) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d == null) {
@@ -589,7 +592,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static boolean native_setPolyToPoly(long native_object, float[] src, int srcIndex,
+    /*package*/ static boolean nSetPolyToPoly(long native_object, float[] src, int srcIndex,
             float[] dst, int dstIndex, int pointCount) {
         // FIXME
         Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
@@ -599,7 +602,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static boolean native_invert(long native_object, long inverse) {
+    /*package*/ static boolean nInvert(long native_object, long inverse) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d == null) {
             return false;
@@ -627,7 +630,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_mapPoints(long native_object, float[] dst, int dstIndex,
+    /*package*/ static void nMapPoints(long native_object, float[] dst, int dstIndex,
             float[] src, int srcIndex, int ptCount, boolean isPts) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d == null) {
@@ -642,7 +645,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static boolean native_mapRect(long native_object, RectF dst, RectF src) {
+    /*package*/ static boolean nMapRect(long native_object, RectF dst, RectF src) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d == null) {
             return false;
@@ -652,7 +655,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static float native_mapRadius(long native_object, float radius) {
+    /*package*/ static float nMapRadius(long native_object, float radius) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d == null) {
             return 0.f;
@@ -667,7 +670,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_getValues(long native_object, float[] values) {
+    /*package*/ static void nGetValues(long native_object, float[] values) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d == null) {
             return;
@@ -677,7 +680,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void native_setValues(long native_object, float[] values) {
+    /*package*/ static void nSetValues(long native_object, float[] values) {
         Matrix_Delegate d = sManager.getDelegate(native_object);
         if (d == null) {
             return;
@@ -687,7 +690,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static boolean native_equals(long native_a, long native_b) {
+    /*package*/ static boolean nEquals(long native_a, long native_b) {
         Matrix_Delegate a = sManager.getDelegate(native_a);
         if (a == null) {
             return false;
@@ -708,8 +711,13 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void finalizer(long native_instance) {
-        sManager.removeJavaReferenceFor(native_instance);
+    /*package*/ static long nGetNativeFinalizer() {
+        synchronized (Matrix_Delegate.class) {
+            if (sFinalizer == -1) {
+                sFinalizer = NativeAllocationRegistry_Delegate.createFinalizer(sManager::removeJavaReferenceFor);
+            }
+        }
+        return sFinalizer;
     }
 
     // ---- Private helper methods ----
diff --git a/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java
index 33296e1..0bbe33d 100644
--- a/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java
+++ b/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java
@@ -261,7 +261,7 @@
     // ---- native methods ----
 
     @LayoutlibDelegate
-    /*package*/ static int nGetFlags(Paint thisPaint, long nativePaint) {
+    /*package*/ static int nGetFlags(long nativePaint) {
         // get the delegate from the native int.
         Paint_Delegate delegate = sManager.getDelegate(nativePaint);
         if (delegate == null) {
@@ -274,7 +274,7 @@
 
 
     @LayoutlibDelegate
-    /*package*/ static void nSetFlags(Paint thisPaint, long nativePaint, int flags) {
+    /*package*/ static void nSetFlags(long nativePaint, int flags) {
         // get the delegate from the native int.
         Paint_Delegate delegate = sManager.getDelegate(nativePaint);
         if (delegate == null) {
@@ -285,12 +285,12 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void nSetFilterBitmap(Paint thisPaint, long nativePaint, boolean filter) {
+    /*package*/ static void nSetFilterBitmap(long nativePaint, boolean filter) {
         setFlag(nativePaint, Paint.FILTER_BITMAP_FLAG, filter);
     }
 
     @LayoutlibDelegate
-    /*package*/ static int nGetHinting(Paint thisPaint, long nativePaint) {
+    /*package*/ static int nGetHinting(long nativePaint) {
         // get the delegate from the native int.
         Paint_Delegate delegate = sManager.getDelegate(nativePaint);
         if (delegate == null) {
@@ -301,7 +301,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void nSetHinting(Paint thisPaint, long nativePaint, int mode) {
+    /*package*/ static void nSetHinting(long nativePaint, int mode) {
         // get the delegate from the native int.
         Paint_Delegate delegate = sManager.getDelegate(nativePaint);
         if (delegate == null) {
@@ -312,46 +312,46 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void nSetAntiAlias(Paint thisPaint, long nativePaint, boolean aa) {
+    /*package*/ static void nSetAntiAlias(long nativePaint, boolean aa) {
         setFlag(nativePaint, Paint.ANTI_ALIAS_FLAG, aa);
     }
 
     @LayoutlibDelegate
-    /*package*/ static void nSetSubpixelText(Paint thisPaint, long nativePaint,
+    /*package*/ static void nSetSubpixelText(long nativePaint,
             boolean subpixelText) {
         setFlag(nativePaint, Paint.SUBPIXEL_TEXT_FLAG, subpixelText);
     }
 
     @LayoutlibDelegate
-    /*package*/ static void nSetUnderlineText(Paint thisPaint, long nativePaint,
+    /*package*/ static void nSetUnderlineText(long nativePaint,
             boolean underlineText) {
         setFlag(nativePaint, Paint.UNDERLINE_TEXT_FLAG, underlineText);
     }
 
     @LayoutlibDelegate
-    /*package*/ static void nSetStrikeThruText(Paint thisPaint, long nativePaint,
+    /*package*/ static void nSetStrikeThruText(long nativePaint,
             boolean strikeThruText) {
         setFlag(nativePaint, Paint.STRIKE_THRU_TEXT_FLAG, strikeThruText);
     }
 
     @LayoutlibDelegate
-    /*package*/ static void nSetFakeBoldText(Paint thisPaint, long nativePaint,
+    /*package*/ static void nSetFakeBoldText(long nativePaint,
             boolean fakeBoldText) {
         setFlag(nativePaint, Paint.FAKE_BOLD_TEXT_FLAG, fakeBoldText);
     }
 
     @LayoutlibDelegate
-    /*package*/ static void nSetDither(Paint thisPaint, long nativePaint, boolean dither) {
+    /*package*/ static void nSetDither(long nativePaint, boolean dither) {
         setFlag(nativePaint, Paint.DITHER_FLAG, dither);
     }
 
     @LayoutlibDelegate
-    /*package*/ static void nSetLinearText(Paint thisPaint, long nativePaint, boolean linearText) {
+    /*package*/ static void nSetLinearText(long nativePaint, boolean linearText) {
         setFlag(nativePaint, Paint.LINEAR_TEXT_FLAG, linearText);
     }
 
     @LayoutlibDelegate
-    /*package*/ static int nGetColor(Paint thisPaint, long nativePaint) {
+    /*package*/ static int nGetColor(long nativePaint) {
         // get the delegate from the native int.
         Paint_Delegate delegate = sManager.getDelegate(nativePaint);
         if (delegate == null) {
@@ -362,7 +362,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void nSetColor(Paint thisPaint, long nativePaint, int color) {
+    /*package*/ static void nSetColor(long nativePaint, int color) {
         // get the delegate from the native int.
         Paint_Delegate delegate = sManager.getDelegate(nativePaint);
         if (delegate == null) {
@@ -373,7 +373,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static int nGetAlpha(Paint thisPaint, long nativePaint) {
+    /*package*/ static int nGetAlpha(long nativePaint) {
         // get the delegate from the native int.
         Paint_Delegate delegate = sManager.getDelegate(nativePaint);
         if (delegate == null) {
@@ -384,7 +384,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void nSetAlpha(Paint thisPaint, long nativePaint, int a) {
+    /*package*/ static void nSetAlpha(long nativePaint, int a) {
         // get the delegate from the native int.
         Paint_Delegate delegate = sManager.getDelegate(nativePaint);
         if (delegate == null) {
@@ -395,7 +395,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static float nGetStrokeWidth(Paint thisPaint, long nativePaint) {
+    /*package*/ static float nGetStrokeWidth(long nativePaint) {
         // get the delegate from the native int.
         Paint_Delegate delegate = sManager.getDelegate(nativePaint);
         if (delegate == null) {
@@ -406,7 +406,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void nSetStrokeWidth(Paint thisPaint, long nativePaint, float width) {
+    /*package*/ static void nSetStrokeWidth(long nativePaint, float width) {
         // get the delegate from the native int.
         Paint_Delegate delegate = sManager.getDelegate(nativePaint);
         if (delegate == null) {
@@ -417,7 +417,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static float nGetStrokeMiter(Paint thisPaint, long nativePaint) {
+    /*package*/ static float nGetStrokeMiter(long nativePaint) {
         // get the delegate from the native int.
         Paint_Delegate delegate = sManager.getDelegate(nativePaint);
         if (delegate == null) {
@@ -428,7 +428,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void nSetStrokeMiter(Paint thisPaint, long nativePaint, float miter) {
+    /*package*/ static void nSetStrokeMiter(long nativePaint, float miter) {
         // get the delegate from the native int.
         Paint_Delegate delegate = sManager.getDelegate(nativePaint);
         if (delegate == null) {
@@ -455,14 +455,14 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static boolean nIsElegantTextHeight(Paint thisPaint, long nativePaint) {
+    /*package*/ static boolean nIsElegantTextHeight(long nativePaint) {
         // get the delegate from the native int.
         Paint_Delegate delegate = sManager.getDelegate(nativePaint);
         return delegate != null && delegate.mFontVariant == FontVariant.ELEGANT;
     }
 
     @LayoutlibDelegate
-    /*package*/ static void nSetElegantTextHeight(Paint thisPaint, long nativePaint,
+    /*package*/ static void nSetElegantTextHeight(long nativePaint,
             boolean elegant) {
         // get the delegate from the native int.
         Paint_Delegate delegate = sManager.getDelegate(nativePaint);
@@ -474,7 +474,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static float nGetTextSize(Paint thisPaint, long nativePaint) {
+    /*package*/ static float nGetTextSize(long nativePaint) {
         // get the delegate from the native int.
         Paint_Delegate delegate = sManager.getDelegate(nativePaint);
         if (delegate == null) {
@@ -485,7 +485,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void nSetTextSize(Paint thisPaint, long nativePaint, float textSize) {
+    /*package*/ static void nSetTextSize(long nativePaint, float textSize) {
         // get the delegate from the native int.
         Paint_Delegate delegate = sManager.getDelegate(nativePaint);
         if (delegate == null) {
@@ -499,7 +499,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static float nGetTextScaleX(Paint thisPaint, long nativePaint) {
+    /*package*/ static float nGetTextScaleX(long nativePaint) {
         // get the delegate from the native int.
         Paint_Delegate delegate = sManager.getDelegate(nativePaint);
         if (delegate == null) {
@@ -510,7 +510,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void nSetTextScaleX(Paint thisPaint, long nativePaint, float scaleX) {
+    /*package*/ static void nSetTextScaleX(long nativePaint, float scaleX) {
         // get the delegate from the native int.
         Paint_Delegate delegate = sManager.getDelegate(nativePaint);
         if (delegate == null) {
@@ -524,7 +524,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static float nGetTextSkewX(Paint thisPaint, long nativePaint) {
+    /*package*/ static float nGetTextSkewX(long nativePaint) {
         // get the delegate from the native int.
         Paint_Delegate delegate = sManager.getDelegate(nativePaint);
         if (delegate == null) {
@@ -535,7 +535,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static void nSetTextSkewX(Paint thisPaint, long nativePaint, float skewX) {
+    /*package*/ static void nSetTextSkewX(long nativePaint, float skewX) {
         // get the delegate from the native int.
         Paint_Delegate delegate = sManager.getDelegate(nativePaint);
         if (delegate == null) {
@@ -549,7 +549,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static float nAscent(Paint thisPaint, long nativePaint, long nativeTypeface) {
+    /*package*/ static float nAscent(long nativePaint, long nativeTypeface) {
         // get the delegate
         Paint_Delegate delegate = sManager.getDelegate(nativePaint);
         if (delegate == null) {
@@ -566,7 +566,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static float nDescent(Paint thisPaint, long nativePaint, long nativeTypeface) {
+    /*package*/ static float nDescent(long nativePaint, long nativeTypeface) {
         // get the delegate
         Paint_Delegate delegate = sManager.getDelegate(nativePaint);
         if (delegate == null) {
@@ -583,7 +583,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static float nGetFontMetrics(Paint thisPaint, long nativePaint, long nativeTypeface,
+    /*package*/ static float nGetFontMetrics(long nativePaint, long nativeTypeface,
             FontMetrics metrics) {
         // get the delegate
         Paint_Delegate delegate = sManager.getDelegate(nativePaint);
@@ -595,7 +595,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static int nGetFontMetricsInt(Paint thisPaint, long nativePaint,
+    /*package*/ static int nGetFontMetricsInt(long nativePaint,
             long nativeTypeface, FontMetricsInt fmi) {
         // get the delegate
         Paint_Delegate delegate = sManager.getDelegate(nativePaint);
@@ -998,7 +998,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static int nGetTextRunCursor(Paint thisPaint, long native_object, char[] text,
+    /*package*/ static int nGetTextRunCursor(long native_object, char[] text,
             int contextStart, int contextLength, int flags, int offset, int cursorOpt) {
         // FIXME
         Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
@@ -1007,7 +1007,7 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static int nGetTextRunCursor(Paint thisPaint, long native_object, String text,
+    /*package*/ static int nGetTextRunCursor(long native_object, String text,
             int contextStart, int contextEnd, int flags, int offset, int cursorOpt) {
         // FIXME
         Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
diff --git a/tools/layoutlib/bridge/src/android/graphics/drawable/VectorDrawable_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/drawable/VectorDrawable_Delegate.java
index 3e81e83..9904263 100644
--- a/tools/layoutlib/bridge/src/android/graphics/drawable/VectorDrawable_Delegate.java
+++ b/tools/layoutlib/bridge/src/android/graphics/drawable/VectorDrawable_Delegate.java
@@ -141,12 +141,12 @@
             long colorFilterPtr, Rect bounds, boolean needsMirroring, boolean canReuseCache) {
         VPathRenderer_Delegate nativePathRenderer = VNativeObject.getDelegate(rendererPtr);
 
-        Canvas_Delegate.native_save(canvasWrapperPtr, MATRIX_SAVE_FLAG | CLIP_SAVE_FLAG);
-        Canvas_Delegate.native_translate(canvasWrapperPtr, bounds.left, bounds.top);
+        Canvas_Delegate.nSave(canvasWrapperPtr, MATRIX_SAVE_FLAG | CLIP_SAVE_FLAG);
+        Canvas_Delegate.nTranslate(canvasWrapperPtr, bounds.left, bounds.top);
 
         if (needsMirroring) {
-            Canvas_Delegate.native_translate(canvasWrapperPtr, bounds.width(), 0);
-            Canvas_Delegate.native_scale(canvasWrapperPtr, -1.0f, 1.0f);
+            Canvas_Delegate.nTranslate(canvasWrapperPtr, bounds.width(), 0);
+            Canvas_Delegate.nScale(canvasWrapperPtr, -1.0f, 1.0f);
         }
 
         // At this point, canvas has been translated to the right position.
@@ -155,7 +155,7 @@
         bounds.offsetTo(0, 0);
         nativePathRenderer.draw(canvasWrapperPtr, colorFilterPtr, bounds.width(), bounds.height());
 
-        Canvas_Delegate.native_restore(canvasWrapperPtr, true);
+        Canvas_Delegate.nRestore(canvasWrapperPtr, true);
 
         return bounds.width() * bounds.height();
     }
@@ -1108,7 +1108,7 @@
             currentGroup.mStackedMatrix.preConcat(currentGroup.mLocalMatrix);
 
             // Save the current clip information, which is local to this group.
-            Canvas_Delegate.native_save(canvasPtr, MATRIX_SAVE_FLAG | CLIP_SAVE_FLAG);
+            Canvas_Delegate.nSave(canvasPtr, MATRIX_SAVE_FLAG | CLIP_SAVE_FLAG);
             // Draw the group tree in the same order as the XML file.
             for (int i = 0; i < currentGroup.mChildren.size(); i++) {
                 Object child = currentGroup.mChildren.get(i);
@@ -1121,7 +1121,7 @@
                     drawPath(currentGroup, childPath, canvasPtr, w, h, filterPtr);
                 }
             }
-            Canvas_Delegate.native_restore(canvasPtr, true);
+            Canvas_Delegate.nRestore(canvasPtr, true);
         }
 
         public void draw(long canvasPtr, long filterPtr, int w, int h) {
@@ -1153,7 +1153,7 @@
 
             if (VPath.isClipPath()) {
                 mRenderPath.addPath(path, mFinalPathMatrix);
-                Canvas_Delegate.native_clipPath(canvasPtr, mRenderPath.mNativePath, Op
+                Canvas_Delegate.nClipPath(canvasPtr, mRenderPath.mNativePath, Op
                         .INTERSECT.nativeInt);
             } else {
                 VFullPath_Delegate fullPath = (VFullPath_Delegate) VPath;
@@ -1197,7 +1197,7 @@
                     fillPaintDelegate.setColorFilter(filterPtr);
                     fillPaintDelegate.setShader(fullPath.mFillGradient);
                     Path_Delegate.native_setFillType(mRenderPath.mNativePath, fullPath.mFillType);
-                    Canvas_Delegate.native_drawPath(canvasPtr, mRenderPath.mNativePath, fillPaint
+                    Canvas_Delegate.nDrawPath(canvasPtr, mRenderPath.mNativePath, fillPaint
                             .getNativeInstance());
                 }
 
@@ -1228,7 +1228,7 @@
                     final float finalStrokeScale = minScale * matrixScale;
                     strokePaint.setStrokeWidth(fullPath.mStrokeWidth * finalStrokeScale);
                     strokePaintDelegate.setShader(fullPath.mStrokeGradient);
-                    Canvas_Delegate.native_drawPath(canvasPtr, mRenderPath.mNativePath, strokePaint
+                    Canvas_Delegate.nDrawPath(canvasPtr, mRenderPath.mNativePath, strokePaint
                             .getNativeInstance());
                 }
             }
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContentProvider.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContentProvider.java
index e4cbb2f..3471165 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContentProvider.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContentProvider.java
@@ -145,4 +145,10 @@
     public Uri uncanonicalize(String callingPkg, Uri uri) throws RemoteException {
         return null;
     }
+
+    @Override
+    public boolean refresh(String callingPkg, Uri url, Bundle args,
+                    ICancellationSignal cancellationSignal) throws RemoteException {
+        return false;
+    }
 }
diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/activity.png b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/activity.png
index 9bf302a..7b58539 100644
--- a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/activity.png
+++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/activity.png
Binary files differ
diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/java/com/android/layoutlib/test/myapplication/CustomCalendar.java b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/java/com/android/layoutlib/test/myapplication/CustomCalendar.java
index 80bbaf1..5c53af9 100644
--- a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/java/com/android/layoutlib/test/myapplication/CustomCalendar.java
+++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/java/com/android/layoutlib/test/myapplication/CustomCalendar.java
@@ -26,6 +26,6 @@
     }
 
     private void init() {
-        setDate(871703200000L, false, true);
+        setDate(871732800000L, false, true);
     }
 }
diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/values/styles.xml b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/values/styles.xml
index 88c9cbc..c8a5fec 100644
--- a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/values/styles.xml
+++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/values/styles.xml
@@ -3,7 +3,7 @@
     <!-- Base application theme. -->
     <style name="AppTheme" parent="android:Theme.Material.Light.DarkActionBar">
         <item name="myattr">@integer/ten</item>
-        <!-- Customize your theme here. -->
+        <item name="android:animateFirstView">false</item>
     </style>
 
 </resources>
diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java
index e7c6cc9..55cfd6d 100644
--- a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java
+++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java
@@ -309,15 +309,7 @@
     /** Test activity.xml */
     @Test
     public void testActivity() throws ClassNotFoundException {
-        try {
-            renderAndVerify("activity.xml", "activity.png");
-        } catch (AssertionError e) {
-            // This is a KI in CalendarWidget and DatePicker rendering.
-            // Tracker bug: http://b.android.com/214370
-            if (!e.getLocalizedMessage().startsWith("Images differ (by 6.5%)")) {
-                throw e;
-            }
-        }
+        renderAndVerify("activity.xml", "activity.png");
     }
 
     @Test