Merge "Fix RTL placement of "Silent Notifications" section header" into qt-dev
diff --git a/cmds/bootanimation/BootAnimation.cpp b/cmds/bootanimation/BootAnimation.cpp
index 16af071..30f4ad4 100644
--- a/cmds/bootanimation/BootAnimation.cpp
+++ b/cmds/bootanimation/BootAnimation.cpp
@@ -69,6 +69,7 @@
 static const char PRODUCT_BOOTANIMATION_DARK_FILE[] = "/product/media/bootanimation-dark.zip";
 static const char PRODUCT_BOOTANIMATION_FILE[] = "/product/media/bootanimation.zip";
 static const char SYSTEM_BOOTANIMATION_FILE[] = "/system/media/bootanimation.zip";
+static const char APEX_BOOTANIMATION_FILE[] = "/apex/com.android.bootanimation/etc/bootanimation.zip";
 static const char PRODUCT_ENCRYPTED_BOOTANIMATION_FILE[] = "/product/media/bootanimation-encrypted.zip";
 static const char SYSTEM_ENCRYPTED_BOOTANIMATION_FILE[] = "/system/media/bootanimation-encrypted.zip";
 static const char OEM_SHUTDOWNANIMATION_FILE[] = "/oem/media/shutdownanimation.zip";
@@ -358,10 +359,10 @@
 
     const bool playDarkAnim = android::base::GetIntProperty("ro.boot.theme", 0) == 1;
     static const char* bootFiles[] =
-        {playDarkAnim ? PRODUCT_BOOTANIMATION_DARK_FILE : PRODUCT_BOOTANIMATION_FILE,
+        {APEX_BOOTANIMATION_FILE, playDarkAnim ? PRODUCT_BOOTANIMATION_DARK_FILE : PRODUCT_BOOTANIMATION_FILE,
          OEM_BOOTANIMATION_FILE, SYSTEM_BOOTANIMATION_FILE};
     static const char* shutdownFiles[] =
-        {PRODUCT_SHUTDOWNANIMATION_FILE, OEM_SHUTDOWNANIMATION_FILE, SYSTEM_SHUTDOWNANIMATION_FILE};
+        {PRODUCT_SHUTDOWNANIMATION_FILE, OEM_SHUTDOWNANIMATION_FILE, SYSTEM_SHUTDOWNANIMATION_FILE, ""};
 
     for (const char* f : (!mShuttingDown ? bootFiles : shutdownFiles)) {
         if (access(f, R_OK) == 0) {
diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto
index 2899d49..495a09f 100644
--- a/cmds/statsd/src/atoms.proto
+++ b/cmds/statsd/src/atoms.proto
@@ -48,6 +48,7 @@
 import "frameworks/base/core/proto/android/stats/enums.proto";
 import "frameworks/base/core/proto/android/stats/intelligence/enums.proto";
 import "frameworks/base/core/proto/android/stats/launcher/launcher.proto";
+import "frameworks/base/core/proto/android/stats/location/location_enums.proto";
 import "frameworks/base/core/proto/android/stats/mediametrics/mediametrics.proto";
 import "frameworks/base/core/proto/android/stats/storage/storage_enums.proto";
 import "frameworks/base/core/proto/android/stats/style/style_enums.proto";
@@ -301,6 +302,7 @@
         ContentCaptureServiceEvents content_capture_service_events = 207;
         ContentCaptureSessionEvents content_capture_session_events = 208;
         ContentCaptureFlushed content_capture_flushed = 209;
+        LocationManagerApiUsageReported location_manager_api_usage_reported = 210;
     }
 
     // Pulled events will start at field 10000.
@@ -6485,3 +6487,60 @@
     // while the app was in the background (only for trusted requests)
     optional int64 trusted_background_duration_millis = 9;
 }
+
+/**
+ * Location Manager API Usage information(e.g. API under usage,
+ * API call's parameters).
+ * Logged from:
+ *  frameworks/base/services/core/java/com/android/server/LocationManagerService.java
+ */
+message LocationManagerApiUsageReported {
+
+    // Indicating if usage starts or usage ends.
+    optional android.stats.location.UsageState state = 1;
+
+    // LocationManagerService's API in use.
+    // We can identify which API from LocationManager is
+    // invoking current LMS API by the combination of
+    // API parameter(e.g. is_listener_null, is_intent_null,
+    // is_location_request_null)
+    optional android.stats.location.LocationManagerServiceApi api_in_use = 2;
+
+    // Name of the package calling the API.
+    optional string calling_package_name = 3;
+
+    // Type of the location provider.
+    optional android.stats.location.ProviderType provider = 4;
+
+    // Quality of the location request
+    optional android.stats.location.LocationRequestQuality quality = 5;
+
+    // The desired interval for active location updates, in milliseconds.
+    // Bucketized to reduce cardinality.
+    optional android.stats.location.LocationRequestIntervalBucket bucketized_interval = 6;
+
+    // Minimum distance between location updates, in meters.
+    // Bucketized to reduce cardinality.
+    optional android.stats.location.SmallestDisplacementBucket
+            bucketized_smallest_displacement = 7;
+
+    // The number of location updates.
+    optional int64 num_updates = 8;
+
+    // The request expiration time, in millisecond since boot.
+    // Bucketized to reduce cardinality.
+    optional android.stats.location.ExpirationBucket
+            bucketized_expire_in = 9;
+
+    // Type of Callback passed in for this API.
+    optional android.stats.location.CallbackType callback_type = 10;
+
+    // The radius of the central point of the alert
+    // region, in meters. Only for API REQUEST_GEOFENCE.
+    // Bucketized to reduce cardinality.
+    optional android.stats.location.GeofenceRadiusBucket bucketized_radius = 11;
+
+    // Activity Importance of API caller.
+    // Categorized to 3 types that are interesting from location's perspective.
+    optional android.stats.location.ActivityImportance activiy_importance = 12;
+}
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index ab630fd..82d4d1d 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -472,8 +472,12 @@
          */
         @MainThread
         @Override
-        public final void initializeInternal(IBinder token, int displayId,
+        public final void initializeInternal(@NonNull IBinder token, int displayId,
                 IInputMethodPrivilegedOperations privilegedOperations) {
+            if (InputMethodPrivilegedOperationsRegistry.isRegistered(token)) {
+                Log.w(TAG, "The token has already registered, ignore this initialization.");
+                return;
+            }
             mPrivOps.set(privilegedOperations);
             InputMethodPrivilegedOperationsRegistry.put(token, mPrivOps);
             updateInputMethodDisplay(displayId);
diff --git a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperationsRegistry.java b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperationsRegistry.java
index 1436aed..049f952 100644
--- a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperationsRegistry.java
+++ b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperationsRegistry.java
@@ -132,4 +132,21 @@
             }
         }
     }
+
+    /**
+     * Check the given IME token registration status.
+     *
+     * @param token IME token
+     * @return {@code true} when the IME token has already registered
+     *         {@link InputMethodPrivilegedOperations}, {@code false} otherwise.
+     */
+    @AnyThread
+    public static boolean isRegistered(IBinder token) {
+        synchronized (sLock) {
+            if (sRegistry == null) {
+                return false;
+            }
+            return sRegistry.containsKey(token);
+        }
+    }
 }
diff --git a/core/proto/android/stats/location/location_enums.proto b/core/proto/android/stats/location/location_enums.proto
new file mode 100644
index 0000000..553c01c
--- /dev/null
+++ b/core/proto/android/stats/location/location_enums.proto
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+syntax = "proto2";
+
+package android.stats.location;
+option java_outer_classname = "LocationStatsEnums";
+
+
+// APIs from LocationManagerService
+enum LocationManagerServiceApi {
+    API_UNKNOWN = 0;
+    API_REQUEST_LOCATION_UPDATES = 1;
+    API_ADD_GNSS_MEASUREMENTS_LISTENER = 2;
+    API_REGISTER_GNSS_STATUS_CALLBACK = 3;
+    API_REQUEST_GEOFENCE = 4;
+    API_SEND_EXTRA_COMMAND = 5;
+}
+
+enum UsageState {
+    USAGE_STARTED = 0;
+    USAGE_ENDED = 1;
+}
+
+// Type of location providers
+enum ProviderType {
+    PROVIDER_UNKNOWN = 0;
+    PROVIDER_NETWORK = 1;
+    PROVIDER_GPS = 2;
+    PROVIDER_PASSIVE = 3;
+    PROVIDER_FUSED = 4;
+}
+
+// Type of Callback passed in for this API
+enum CallbackType {
+    CALLBACK_UNKNOWN = 0;
+    // Current API does not need a callback, e.g. sendExtraCommand
+    CALLBACK_NOT_APPLICABLE = 1;
+    CALLBACK_LISTENER = 2;
+    CALLBACK_PENDING_INTENT = 3;
+}
+
+// Possible values for mQuality field in
+// frameworks/base/location/java/android/location/LocationRequest.java
+enum LocationRequestQuality {
+    QUALITY_UNKNOWN = 0;
+    ACCURACY_FINE = 100;
+    ACCURACY_BLOCK = 102;
+    ACCURACY_CITY = 104;
+    POWER_NONE = 200;
+    POWER_LOW = 201;
+    POWER_HIGH = 203;
+}
+
+// Bucketized values for interval field in
+// frameworks/base/location/java/android/location/LocationRequest.java
+enum LocationRequestIntervalBucket {
+    INTERVAL_UNKNOWN = 0;
+    INTERVAL_BETWEEN_0_SEC_AND_1_SEC = 1;
+    INTERVAL_BETWEEN_1_SEC_AND_5_SEC = 2;
+    INTERVAL_BETWEEN_5_SEC_AND_1_MIN = 3;
+    INTERVAL_BETWEEN_1_MIN_AND_10_MIN = 4;
+    INTERVAL_BETWEEN_10_MIN_AND_1_HOUR = 5;
+    INTERVAL_LARGER_THAN_1_HOUR = 6;
+}
+
+// Bucketized values for small displacement field in
+// frameworks/base/location/java/android/location/LocationRequest.java
+// Value in meters.
+enum SmallestDisplacementBucket {
+    DISTANCE_UNKNOWN = 0;
+    DISTANCE_ZERO = 1;
+    DISTANCE_BETWEEN_0_AND_100 = 2;
+    DISTANCE_LARGER_THAN_100 = 3;
+}
+
+// Bucketized values for expire_in field in
+// frameworks/base/location/java/android/location/LocationRequest.java
+enum ExpirationBucket {
+    EXPIRATION_UNKNOWN = 0;
+    EXPIRATION_BETWEEN_0_AND_20_SEC = 1;
+    EXPIRATION_BETWEEN_20_SEC_AND_1_MIN = 2;
+    EXPIRATION_BETWEEN_1_MIN_AND_10_MIN = 3;
+    EXPIRATION_BETWEEN_10_MIN_AND_1_HOUR = 4;
+    EXPIRATION_LARGER_THAN_1_HOUR = 5;
+    EXPIRATION_NO_EXPIRY = 6;
+}
+
+// Bucketized values for radius field in
+// frameworks/base/location/java/android/location/Geofence.java
+// Value in meters.
+enum GeofenceRadiusBucket {
+    RADIUS_UNKNOWN = 0;
+    RADIUS_BETWEEN_0_AND_100 = 1;
+    RADIUS_BETWEEN_100_AND_200 = 2;
+    RADIUS_BETWEEN_200_AND_300 = 3;
+    RADIUS_BETWEEN_300_AND_1000 = 4;
+    RADIUS_BETWEEN_1000_AND_10000 = 5;
+    RADIUS_LARGER_THAN_100000 = 6;
+    RADIUS_NEGATIVE = 7;
+}
+
+// Caller Activity Importance.
+enum ActivityImportance {
+    IMPORTANCE_UNKNOWN = 0;
+    IMPORTANCE_TOP = 1;
+    IMPORTANCE_FORGROUND_SERVICE = 2;
+    IMPORTANCE_BACKGROUND = 3;
+}
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index fb58569..2beef42 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1276,9 +1276,14 @@
          meanings. -->
     <integer name="config_defaultRingVibrationIntensity">2</integer>
 
+    <!-- Whether to use the strict phone number matcher by default. -->
     <bool name="config_use_strict_phone_number_comparation">false</bool>
 
-    <bool name="config_use_strict_phone_number_comparation_for_russian">true</bool>
+    <!-- Whether to use the strict phone number matcher in Russia. -->
+    <bool name="config_use_strict_phone_number_comparation_for_russia">true</bool>
+
+    <!-- Whether to use the strict phone number matcher in Kazakhstan. -->
+    <bool name="config_use_strict_phone_number_comparation_for_kazakhstan">true</bool>
 
     <!-- Display low battery warning when battery level dips to this value.
          Also, the battery stats are flushed to disk when we hit this level.  -->
@@ -3306,6 +3311,10 @@
          (which normally prevents seamless rotation). -->
     <bool name="config_allowSeamlessRotationDespiteNavBarMoving">false</bool>
 
+    <!-- Controls whether hints for gestural navigation are shown when the device is setup.
+         This should only be set when the device has gestural navigation enabled by default. -->
+    <bool name="config_showGesturalNavigationHints">false</bool>
+
     <!-- 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">16x16</string>
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index e0ab6c8..5363ef92 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -59,9 +59,7 @@
     <!-- Height of the bottom navigation bar frame in landscape -->
     <dimen name="navigation_bar_frame_height_landscape">@dimen/navigation_bar_frame_height</dimen>
 
-    <!-- The height of the navigation gesture area; if the size is larger than the navigation bar
-        frame width/height, then the difference is the spacing from the navigation bar window to
-        the area that detects gestures. -->
+    <!-- The height of the navigation gesture area if the gesture is starting from the bottom. -->
     <dimen name="navigation_bar_gesture_height">@dimen/navigation_bar_frame_height</dimen>
 
     <!-- Height of the bottom navigation / system bar in car mode. -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 3d2d969..4af05f6 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -313,7 +313,8 @@
   <java-symbol type="bool" name="config_ui_enableFadingMarquee" />
   <java-symbol type="bool" name="config_enableHapticTextHandle" />
   <java-symbol type="bool" name="config_use_strict_phone_number_comparation" />
-  <java-symbol type="bool" name="config_use_strict_phone_number_comparation_for_russian" />
+  <java-symbol type="bool" name="config_use_strict_phone_number_comparation_for_russia" />
+  <java-symbol type="bool" name="config_use_strict_phone_number_comparation_for_kazakhstan" />
   <java-symbol type="bool" name="config_single_volume" />
   <java-symbol type="bool" name="config_voice_capable" />
   <java-symbol type="bool" name="config_requireCallCapableAccountForHandle" />
@@ -2880,6 +2881,7 @@
   <java-symbol type="bool" name="config_allowSeamlessRotationDespiteNavBarMoving" />
   <java-symbol type="dimen" name="config_backGestureInset" />
   <java-symbol type="color" name="system_bar_background_semi_transparent" />
+  <java-symbol type="bool" name="config_showGesturalNavigationHints" />
 
   <!-- EditText suggestion popup. -->
   <java-symbol type="id" name="suggestionWindowContainer" />
diff --git a/core/res/res/values/themes_device_defaults.xml b/core/res/res/values/themes_device_defaults.xml
index b901048..6289262 100644
--- a/core/res/res/values/themes_device_defaults.xml
+++ b/core/res/res/values/themes_device_defaults.xml
@@ -1473,14 +1473,10 @@
         <item name="colorError">@color/error_color_device_default_light</item>
         <item name="colorEdgeEffect">@color/edge_effect_device_default_light</item>
 
-        <!-- Add divider that matches material -->
+        <!-- Add white nav bar with divider that matches material -->
         <item name="navigationBarDividerColor">@color/navigation_bar_divider_device_default_settings</item>
-
-        <!-- Add transparent nav and status bars with light icons to support drawing edge-to-edge
-        for Q gestural navigation-->
-        <item name="navigationBarColor">@android:color/transparent</item>
+        <item name="navigationBarColor">@android:color/white</item>
         <item name="windowLightNavigationBar">true</item>
-        <item name="statusBarColor">@android:color/transparent</item>
 
         <!-- Dialog attributes -->
         <item name="dialogCornerRadius">@dimen/config_dialogCornerRadius</item>
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index b3856d5..a640122 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -196,6 +196,7 @@
         <permission name="android.permission.USE_RESERVED_DISK"/>
         <permission name="android.permission.WRITE_MEDIA_STORAGE"/>
         <permission name="android.permission.WATCH_APPOPS"/>
+        <permission name="android.permission.UPDATE_DEVICE_STATS"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.providers.telephony">
diff --git a/graphics/java/android/graphics/Outline.java b/graphics/java/android/graphics/Outline.java
index 98c990a..1fc056c 100644
--- a/graphics/java/android/graphics/Outline.java
+++ b/graphics/java/android/graphics/Outline.java
@@ -273,8 +273,12 @@
     }
 
     /**
-     * Sets the Constructs an Outline from a
+     * Sets the Outline to a
      * {@link android.graphics.Path#isConvex() convex path}.
+     *
+     * @param convexPath used to construct the Outline. As of
+     * {@link android.os.Build.VERSION_CODES#Q}, it is no longer required to be
+     * convex.
      */
     public void setConvexPath(@NonNull Path convexPath) {
         if (convexPath.isEmpty()) {
@@ -282,10 +286,6 @@
             return;
         }
 
-        if (!convexPath.isConvex()) {
-            throw new IllegalArgumentException("path must be convex");
-        }
-
         if (mPath == null) {
             mPath = new Path();
         }
diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
index 0a28949..16c8b89 100644
--- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
+++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
@@ -161,8 +161,7 @@
 // Recording Canvas draw operations: Bitmaps
 // ----------------------------------------------------------------------------
 
-SkiaCanvas::PaintCoW&& SkiaRecordingCanvas::filterBitmap(PaintCoW&& paint,
-                                                         sk_sp<SkColorFilter> colorSpaceFilter) {
+SkiaCanvas::PaintCoW&& SkiaRecordingCanvas::filterBitmap(PaintCoW&& paint) {
     bool fixBlending = false;
     bool fixAA = false;
     if (paint) {
@@ -172,23 +171,13 @@
         fixAA = paint->isAntiAlias();
     }
 
-    if (fixBlending || fixAA || colorSpaceFilter) {
+    if (fixBlending || fixAA) {
         SkPaint& tmpPaint = paint.writeable();
 
         if (fixBlending) {
             tmpPaint.setBlendMode(SkBlendMode::kDstOut);
         }
 
-        if (colorSpaceFilter) {
-            if (tmpPaint.getColorFilter()) {
-                tmpPaint.setColorFilter(SkColorFilter::MakeComposeFilter(
-                        tmpPaint.refColorFilter(), std::move(colorSpaceFilter)));
-            } else {
-                tmpPaint.setColorFilter(std::move(colorSpaceFilter));
-            }
-            LOG_ALWAYS_FATAL_IF(!tmpPaint.getColorFilter());
-        }
-
         // disabling AA on bitmap draws matches legacy HWUI behavior
         tmpPaint.setAntiAlias(false);
     }
@@ -198,7 +187,7 @@
 
 void SkiaRecordingCanvas::drawBitmap(Bitmap& bitmap, float left, float top, const SkPaint* paint) {
     sk_sp<SkImage> image = bitmap.makeImage();
-    mRecorder.drawImage(image, left, top, filterPaint(paint), bitmap.palette());
+    mRecorder.drawImage(image, left, top, filterBitmap(paint), bitmap.palette());
     // if image->unique() is true, then mRecorder.drawImage failed for some reason. It also means
     // it is not safe to store a raw SkImage pointer, because the image object will be destroyed
     // when this function ends.
@@ -212,7 +201,7 @@
     concat(matrix);
 
     sk_sp<SkImage> image = bitmap.makeImage();
-    mRecorder.drawImage(image, 0, 0, filterPaint(paint), bitmap.palette());
+    mRecorder.drawImage(image, 0, 0, filterBitmap(paint), bitmap.palette());
     if (!bitmap.isImmutable() && image.get() && !image->unique()) {
         mDisplayList->mMutableImages.push_back(image.get());
     }
@@ -225,7 +214,7 @@
     SkRect dstRect = SkRect::MakeLTRB(dstLeft, dstTop, dstRight, dstBottom);
 
     sk_sp<SkImage> image = bitmap.makeImage();
-    mRecorder.drawImageRect(image, srcRect, dstRect, filterPaint(paint),
+    mRecorder.drawImageRect(image, srcRect, dstRect, filterBitmap(paint),
                             SkCanvas::kFast_SrcRectConstraint, bitmap.palette());
     if (!bitmap.isImmutable() && image.get() && !image->unique() && !srcRect.isEmpty() &&
         !dstRect.isEmpty()) {
@@ -263,7 +252,7 @@
         filteredPaint.writeable().setFilterQuality(kLow_SkFilterQuality);
     }
     sk_sp<SkImage> image = bitmap.makeImage();
-    mRecorder.drawImageLattice(image, lattice, dst, filterPaint(std::move(filteredPaint)),
+    mRecorder.drawImageLattice(image, lattice, dst, filterBitmap(std::move(filteredPaint)),
                                bitmap.palette());
     if (!bitmap.isImmutable() && image.get() && !image->unique() && !dst.isEmpty()) {
         mDisplayList->mMutableImages.push_back(image.get());
diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h
index afeccea..c42cea3 100644
--- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h
+++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h
@@ -90,7 +90,7 @@
      */
     void initDisplayList(uirenderer::RenderNode* renderNode, int width, int height);
 
-    PaintCoW&& filterBitmap(PaintCoW&& paint, sk_sp<SkColorFilter> colorSpaceFilter);
+    PaintCoW&& filterBitmap(PaintCoW&& paint);
 };
 
 }  // namespace skiapipeline
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 2541982..2d6cd24 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -4274,38 +4274,6 @@
         }
     }
 
-     /**
-     * Indicate A2DP source or sink active device change and eventually suppress
-     * the {@link AudioManager.ACTION_AUDIO_BECOMING_NOISY} intent.
-     * This operation is asynchronous but its execution will still be sequentially scheduled
-     * relative to calls to {@link #setBluetoothHearingAidDeviceConnectionState(BluetoothDevice,
-     * int, boolean, int)} and
-     * {@link #handleBluetoothA2dpDeviceConfigChange(BluetoothDevice)}.
-     * @param device Bluetooth device connected/disconnected
-     * @param state  new connection state (BluetoothProfile.STATE_xxx)
-     * @param profile profile for the A2DP device
-     * (either {@link android.bluetooth.BluetoothProfile.A2DP} or
-     * {@link android.bluetooth.BluetoothProfile.A2DP_SINK})
-     * @param a2dpVolume New volume for the connecting device. Does nothing if
-     * disconnecting. Pass value -1 in case you want this field to be ignored
-     * @param suppressNoisyIntent if true the
-     * {@link AudioManager.ACTION_AUDIO_BECOMING_NOISY} intent will not be sent.
-     * @return a delay in ms that the caller should wait before broadcasting
-     * BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED intent.
-     * {@hide}
-     */
-    public void handleBluetoothA2dpActiveDeviceChange(
-                BluetoothDevice device, int state, int profile,
-                boolean suppressNoisyIntent, int a2dpVolume) {
-        final IAudioService service = getService();
-        try {
-            service.handleBluetoothA2dpActiveDeviceChange(device,
-                state, profile, suppressNoisyIntent, a2dpVolume);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
     /** {@hide} */
     public IRingtonePlayer getRingtonePlayer() {
         try {
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index a790441..71f52a1 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -178,9 +178,6 @@
 
     void handleBluetoothA2dpDeviceConfigChange(in BluetoothDevice device);
 
-    void handleBluetoothA2dpActiveDeviceChange(in BluetoothDevice device,
-            int state, int profile, boolean suppressNoisyIntent, int a2dpVolume);
-
     @UnsupportedAppUsage
     AudioRoutesInfo startWatchingRoutes(in IAudioRoutesObserver observer);
 
diff --git a/media/java/android/media/MediaFile.java b/media/java/android/media/MediaFile.java
index 710b503..0b1ae34 100644
--- a/media/java/android/media/MediaFile.java
+++ b/media/java/android/media/MediaFile.java
@@ -220,7 +220,6 @@
             case "audio/mpegurl":
             case "application/x-mpegurl":
             case "application/vnd.apple.mpegurl":
-            case "video/x-ms-asf":
             case "audio/x-scpls":
                 return true;
             default:
diff --git a/packages/CarSystemUI/res/layout/car_ongoing_privacy_chip.xml b/packages/CarSystemUI/res/layout/car_ongoing_privacy_chip.xml
deleted file mode 100644
index 918abd9..0000000
--- a/packages/CarSystemUI/res/layout/car_ongoing_privacy_chip.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2019 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.
--->
-
-<com.android.systemui.statusbar.car.privacy.OngoingPrivacyChip
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/car_privacy_chip"
-    android:layout_width="wrap_content"
-    android:layout_height="match_parent"
-    android:layout_margin="@dimen/ongoing_appops_chip_margin"
-    android:layout_toStartOf="@+id/clock_container"
-    android:focusable="true"
-    android:gravity="center_vertical|end"
-    android:orientation="horizontal"
-    android:paddingEnd="@dimen/ongoing_appops_chip_side_padding"
-    android:paddingStart="@dimen/ongoing_appops_chip_side_padding"
-    android:visibility="visible">
-
-    <LinearLayout
-        android:id="@+id/icons_container"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:gravity="center_vertical|start"/>
-</com.android.systemui.statusbar.car.privacy.OngoingPrivacyChip>
\ No newline at end of file
diff --git a/packages/CarSystemUI/res/layout/car_top_navigation_bar.xml b/packages/CarSystemUI/res/layout/car_top_navigation_bar.xml
index cae89c1..925ccb4 100644
--- a/packages/CarSystemUI/res/layout/car_top_navigation_bar.xml
+++ b/packages/CarSystemUI/res/layout/car_top_navigation_bar.xml
@@ -65,8 +65,6 @@
             />
         </FrameLayout>
 
-        <include layout="@layout/car_ongoing_privacy_chip"/>
-
         <FrameLayout
             android:id="@+id/clock_container"
             android:layout_width="wrap_content"
diff --git a/packages/CarSystemUI/res/values/dimens.xml b/packages/CarSystemUI/res/values/dimens.xml
index 7027ce3..0358357b 100644
--- a/packages/CarSystemUI/res/values/dimens.xml
+++ b/packages/CarSystemUI/res/values/dimens.xml
@@ -59,16 +59,6 @@
     <dimen name="car_keyline_1">24dp</dimen>
     <dimen name="car_keyline_2">96dp</dimen>
     <dimen name="car_keyline_3">128dp</dimen>
-    <dimen name="privacy_chip_icon_height">36dp</dimen>
-    <dimen name="privacy_chip_icon_padding_left">0dp</dimen>
-    <dimen name="privacy_chip_icon_padding_right">0dp</dimen>
-    <dimen name="privacy_chip_icon_padding_top">0dp</dimen>
-    <dimen name="privacy_chip_icon_padding_bottom">0dp</dimen>
-
-    <dimen name="privacy_chip_text_padding_left">0dp</dimen>
-    <dimen name="privacy_chip_text_padding_right">0dp</dimen>
-    <dimen name="privacy_chip_text_padding_top">0dp</dimen>
-    <dimen name="privacy_chip_text_padding_bottom">0dp</dimen>
 
     <dimen name="privacy_chip_icon_max_height">100dp</dimen>
 
@@ -86,16 +76,6 @@
     <dimen name="ongoing_appops_chip_bg_padding">4dp</dimen>
     <!-- Radius of Ongoing App Ops chip corners -->
     <dimen name="ongoing_appops_chip_bg_corner_radius">12dp</dimen>
-    <!-- Start padding for the app icon displayed in the dialog -->
-    <dimen name="privacy_dialog_app_icon_padding_start">40dp</dimen>
-    <!-- End padding for the app opps icon displayed in the dialog -->
-    <dimen name="privacy_dialog_app_ops_icon_padding_end">40dp</dimen>
-    <!-- Top padding for the list of application displayed in the dialog -->
-    <dimen name="privacy_dialog_app_list_padding_top">20dp</dimen>
-    <!-- Top padding for the dialog container-->
-    <dimen name="privacy_dialog_container_padding_top">10dp</dimen>
-    <!-- Top padding for the dialog title-->
-    <dimen name="privacy_dialog_title_padding_start">10dp</dimen>
 
     <!-- Car volume dimens. -->
     <dimen name="car_volume_item_height">@*android:dimen/car_single_line_list_item_height</dimen>
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/OngoingPrivacyChip.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/OngoingPrivacyChip.java
deleted file mode 100644
index ead1de2..0000000
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/OngoingPrivacyChip.java
+++ /dev/null
@@ -1,228 +0,0 @@
-/*
- * Copyright (C) 2019 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.car.privacy;
-
-import android.app.ActivityManager;
-import android.app.AppOpsManager;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-
-import com.android.systemui.Dependency;
-import com.android.systemui.R;
-import com.android.systemui.appops.AppOpItem;
-import com.android.systemui.appops.AppOpsController;
-import com.android.systemui.plugins.ActivityStarter;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-import java.util.stream.Collectors;
-
-/**
- * Layout defining the privacy chip that will be displayed in CarStatusRar with the information for
- * which applications are using AppOpps permission fpr camera, mic and location.
- */
-public class OngoingPrivacyChip extends LinearLayout implements View.OnClickListener {
-
-    private Context mContext;
-
-    private LinearLayout mIconsContainer;
-    private List<PrivacyItem> mPrivacyItems;
-    private static AppOpsController sAppOpsController;
-    private UserManager mUserManager;
-    private int mCurrentUser;
-    private List<Integer> mCurrentUserIds;
-    private boolean mListening = false;
-    PrivacyDialogBuilder mPrivacyDialogBuilder;
-    private LinearLayout mPrivacyChip;
-    private ActivityStarter mActivityStarter;
-
-    protected static final int[] OPS = new int[]{
-            AppOpsManager.OP_CAMERA,
-            AppOpsManager.OP_RECORD_AUDIO,
-            AppOpsManager.OP_COARSE_LOCATION,
-            AppOpsManager.OP_FINE_LOCATION
-    };
-
-    public OngoingPrivacyChip(Context context) {
-        super(context, null);
-        init(context);
-    }
-
-    public OngoingPrivacyChip(Context context, AttributeSet attr) {
-        super(context, attr);
-        init(context);
-    }
-
-    public OngoingPrivacyChip(Context context, AttributeSet attr, int defStyle) {
-        super(context, attr, defStyle);
-        init(context);
-    }
-
-    public OngoingPrivacyChip(Context context, AttributeSet attr, int defStyle, int a) {
-        super(context, attr, defStyle, a);
-        init(context);
-    }
-
-    private void init(Context context) {
-        mContext = context;
-        mPrivacyItems = new ArrayList<>();
-        sAppOpsController = Dependency.get(AppOpsController.class);
-        mUserManager = mContext.getSystemService(UserManager.class);
-        mActivityStarter = Dependency.get(ActivityStarter.class);
-        mCurrentUser = ActivityManager.getCurrentUser();
-        mCurrentUserIds = mUserManager.getProfiles(mCurrentUser).stream().map(
-                userInfo -> userInfo.id).collect(Collectors.toList());
-
-        mPrivacyDialogBuilder = new PrivacyDialogBuilder(context, mPrivacyItems);
-    }
-
-    private AppOpsController.Callback mCallback = new AppOpsController.Callback() {
-
-        @Override
-        public void onActiveStateChanged(int code, int uid, String packageName, boolean active) {
-            int userId = UserHandle.getUserId(uid);
-            if (mCurrentUserIds.contains(userId)) {
-                updatePrivacyList();
-            }
-        }
-    };
-
-    @Override
-    public void onFinishInflate() {
-        mIconsContainer = findViewById(R.id.icons_container);
-        mPrivacyChip = (LinearLayout) findViewById(R.id.car_privacy_chip);
-        if (mPrivacyChip != null) {
-            mPrivacyChip.setOnClickListener(this);
-            setListening(true);
-        }
-    }
-
-    @Override
-    public void onDetachedFromWindow() {
-        if (mPrivacyChip != null) {
-            setListening(false);
-        }
-        super.onDetachedFromWindow();
-    }
-
-    @Override
-    public void onClick(View v) {
-        updatePrivacyList();
-        Handler mUiHandler = new Handler(Looper.getMainLooper());
-        mUiHandler.post(() -> {
-            mActivityStarter.postStartActivityDismissingKeyguard(
-                    new Intent(Intent.ACTION_REVIEW_ONGOING_PERMISSION_USAGE), 0);
-        });
-    }
-
-    private void setListening(boolean listen) {
-        if (mListening == listen) {
-            return;
-        }
-        mListening = listen;
-        if (mListening) {
-            sAppOpsController.addCallback(OPS, mCallback);
-            updatePrivacyList();
-        } else {
-            sAppOpsController.removeCallback(OPS, mCallback);
-        }
-    }
-
-    private void updatePrivacyList() {
-        mPrivacyItems = mCurrentUserIds.stream()
-                .flatMap(item -> sAppOpsController.getActiveAppOpsForUser(item).stream())
-                .filter(Objects::nonNull)
-                .map(item -> toPrivacyItem(item))
-                .filter(Objects::nonNull)
-                .collect(Collectors.toList());
-        mPrivacyDialogBuilder = new PrivacyDialogBuilder(mContext, mPrivacyItems);
-
-        Handler refresh = new Handler(Looper.getMainLooper());
-        refresh.post(new Runnable() {
-            @Override
-            public void run() {
-                updateView();
-            }
-        });
-    }
-
-    private PrivacyItem toPrivacyItem(AppOpItem appOpItem) {
-        PrivacyType type;
-        switch (appOpItem.getCode()) {
-            case AppOpsManager.OP_CAMERA:
-                type = PrivacyType.TYPE_CAMERA;
-                break;
-            case AppOpsManager.OP_COARSE_LOCATION:
-                type = PrivacyType.TYPE_LOCATION;
-                break;
-            case AppOpsManager.OP_FINE_LOCATION:
-                type = PrivacyType.TYPE_LOCATION;
-                break;
-            case AppOpsManager.OP_RECORD_AUDIO:
-                type = PrivacyType.TYPE_MICROPHONE;
-                break;
-            default:
-                return null;
-        }
-        PrivacyApplication app = new PrivacyApplication(appOpItem.getPackageName(), mContext);
-        return new PrivacyItem(type, app, appOpItem.getTimeStarted());
-    }
-
-    // Should only be called if the mPrivacyDialogBuilder icons or app changed
-    private void updateView() {
-        if (mPrivacyItems.isEmpty()) {
-            mPrivacyChip.setVisibility(GONE);
-            return;
-        }
-        mPrivacyChip.setVisibility(VISIBLE);
-        setIcons(mPrivacyDialogBuilder);
-
-        requestLayout();
-    }
-
-    private void setIcons(PrivacyDialogBuilder dialogBuilder) {
-        mIconsContainer.removeAllViews();
-        dialogBuilder.generateIcons().forEach(item -> {
-            int size = mContext.getResources().getDimensionPixelSize(
-                    R.dimen.privacy_chip_icon_height);
-            ImageView image = new ImageView(mContext);
-            image.setImageDrawable(item);
-            LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(size, size);
-
-            int leftPadding = mContext.getResources().getDimensionPixelSize(
-                    R.dimen.privacy_chip_icon_padding_left);
-            int topPadding = mContext.getResources().getDimensionPixelSize(
-                    R.dimen.privacy_chip_icon_padding_top);
-            int rightPadding = mContext.getResources().getDimensionPixelSize(
-                    R.dimen.privacy_chip_icon_padding_right);
-            int bottomPadding = mContext.getResources().getDimensionPixelSize(
-                    R.dimen.privacy_chip_icon_padding_bottom);
-            image.setLayoutParams(layoutParams);
-            image.setPadding(leftPadding, topPadding, rightPadding, bottomPadding);
-            mIconsContainer.addView(image);
-        });
-    }
-}
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyApplication.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyApplication.java
deleted file mode 100644
index 5ec7a77..0000000
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyApplication.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2019 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.car.privacy;
-
-import android.car.userlib.CarUserManagerHelper;
-import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.graphics.drawable.Drawable;
-import android.util.Log;
-
-/**
- * Class to hold the data for the applications that are using the AppOps permissions.
- */
-public class PrivacyApplication {
-    private static final String TAG = "PrivacyApplication";
-
-    private Drawable mIcon;
-    private String mApplicationName;
-
-    public PrivacyApplication(String packageName, Context context) {
-        try {
-            CarUserManagerHelper carUserManagerHelper = new CarUserManagerHelper(context);
-            ApplicationInfo app = context.getPackageManager()
-                    .getApplicationInfoAsUser(packageName, 0,
-                            carUserManagerHelper.getCurrentForegroundUserId());
-            mIcon = context.getPackageManager().getApplicationIcon(app);
-            mApplicationName = context.getPackageManager().getApplicationLabel(app).toString();
-        } catch (PackageManager.NameNotFoundException e) {
-            mApplicationName = packageName;
-            Log.e(TAG, "Failed to to find package name", e);
-        }
-    }
-
-    /**
-     * Gets the application name.
-     */
-    public Drawable getIcon() {
-        return mIcon;
-    }
-
-    /**
-     * Gets the application name.
-     */
-    public String getApplicationName() {
-        return mApplicationName;
-    }
-}
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyDialogBuilder.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyDialogBuilder.java
deleted file mode 100644
index 3b83e7c..0000000
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyDialogBuilder.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2019 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.car.privacy;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.stream.Collectors;
-
-/**
- * Helper class to build the {@link OngoingPrivacyDialog}
- */
-public class PrivacyDialogBuilder {
-
-    private Map<PrivacyType, List<PrivacyItem>> mItemsByType;
-    private PrivacyApplication mApplication;
-    private Context mContext;
-
-    public PrivacyDialogBuilder(Context context, List<PrivacyItem> itemsList) {
-        mContext = context;
-        mItemsByType = itemsList.stream().filter(Objects::nonNull).collect(
-                Collectors.groupingBy(PrivacyItem::getPrivacyType));
-        List<PrivacyApplication> apps = itemsList.stream().filter(Objects::nonNull).map(
-                PrivacyItem::getPrivacyApplication).distinct().collect(Collectors.toList());
-        mApplication = apps.size() == 1 ? apps.get(0) : null;
-    }
-
-    /**
-     * Gets the icon id for all the {@link PrivacyItem} in the same order as of itemList.
-     */
-    public List<Drawable> generateIcons() {
-        return mItemsByType.keySet().stream().map(item -> item.getIconId(mContext)).collect(
-                Collectors.toList());
-    }
-
-    /**
-     * Gets the application object.
-     */
-    public PrivacyApplication getApplication() {
-        return mApplication;
-    }
-}
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyItem.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyItem.java
deleted file mode 100644
index fca1373..0000000
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyItem.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2019 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.car.privacy;
-
-/**
- * Class for holding the data of each privacy item displayed in {@link OngoingPrivacyDialog}
- */
-public class PrivacyItem {
-
-    private PrivacyType mPrivacyType;
-    private PrivacyApplication mPrivacyApplication;
-
-    public PrivacyItem(PrivacyType privacyType, PrivacyApplication privacyApplication,
-            long timeStarted) {
-        this.mPrivacyType = privacyType;
-        this.mPrivacyApplication = privacyApplication;
-    }
-
-    /**
-     * Gets the application object.
-     */
-    public PrivacyApplication getPrivacyApplication() {
-        return mPrivacyApplication;
-    }
-
-    /**
-     * Gets the privacy type for the application.
-     */
-    public PrivacyType getPrivacyType() {
-        return mPrivacyType;
-    }
-}
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyType.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyType.java
deleted file mode 100644
index 8955c87..0000000
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyType.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (C) 2019 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.car.privacy;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-
-import com.android.systemui.R;
-
-/**
- * Enum for storing data for camera, mic and location.
- */
-public enum PrivacyType {
-    TYPE_CAMERA(R.string.privacy_type_camera, com.android.internal.R.drawable.ic_camera),
-    TYPE_LOCATION(R.string.privacy_type_location, R.drawable.stat_sys_location),
-    TYPE_MICROPHONE(R.string.privacy_type_microphone, R.drawable.ic_mic_white);
-
-    private int mNameId;
-    private int mIconId;
-
-    PrivacyType(int nameId, int iconId) {
-        mNameId = nameId;
-        mIconId = iconId;
-    }
-
-    /**
-     * Get the icon Id.
-     */
-    public Drawable getIconId(Context context) {
-        return context.getResources().getDrawable(mIconId, null);
-    }
-
-    /**
-     * Get the name Id.
-     */
-    public String getNameId(Context context) {
-        return context.getResources().getString(mNameId);
-    }
-}
diff --git a/packages/SystemUI/res/layout/notification_info.xml b/packages/SystemUI/res/layout/notification_info.xml
index 8ffa2d8..87de9d4 100644
--- a/packages/SystemUI/res/layout/notification_info.xml
+++ b/packages/SystemUI/res/layout/notification_info.xml
@@ -219,7 +219,7 @@
             android:gravity="center"
             android:orientation="vertical">
 
-            <LinearLayout
+            <com.android.systemui.statusbar.notification.row.ButtonLinearLayout
                 android:id="@+id/alert"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
@@ -268,9 +268,9 @@
                     android:ellipsize="end"
                     android:maxLines="2"
                     android:textAppearance="@style/TextAppearance.NotificationImportanceDetail"/>
-            </LinearLayout>
+            </com.android.systemui.statusbar.notification.row.ButtonLinearLayout>
 
-            <LinearLayout
+            <com.android.systemui.statusbar.notification.row.ButtonLinearLayout
                 android:id="@+id/silence"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
@@ -321,7 +321,7 @@
                     android:ellipsize="end"
                     android:maxLines="2"
                     android:textAppearance="@style/TextAppearance.NotificationImportanceDetail"/>
-            </LinearLayout>
+            </com.android.systemui.statusbar.notification.row.ButtonLinearLayout>
 
         </LinearLayout>
 
diff --git a/packages/SystemUI/res/values-night/colors.xml b/packages/SystemUI/res/values-night/colors.xml
index dd3073f..c5f4052 100644
--- a/packages/SystemUI/res/values-night/colors.xml
+++ b/packages/SystemUI/res/values-night/colors.xml
@@ -31,7 +31,10 @@
     <color name="notification_divider_color">#212121</color>
 
     <!-- The background color of the notification shade -->
-    <color name="notification_shade_background_color">#181818</color>
+    <color name="notification_shade_background_color">@color/GM2_grey_900</color>
+
+    <!-- The color of the gear shown behind a notification -->
+    <color name="notification_gear_color">@color/GM2_grey_500</color>
 
     <!-- The color of the ripples on the untinted notifications -->
     <color name="notification_ripple_untinted_color">#30ffffff</color>
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index abf4fdf..e7a1a66 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -74,7 +74,7 @@
     <color name="notification_divider_color">#FF616161</color>
 
     <!-- The background color of the notification shade -->
-    <color name="notification_shade_background_color">#ffeeeeee</color>
+    <color name="notification_shade_background_color">@color/GM2_grey_200</color>
 
     <!-- The color of the ripples on the untinted notifications -->
     <color name="notification_ripple_untinted_color">#28000000</color>
@@ -83,7 +83,7 @@
     <color name="notification_ripple_tinted_color">#30ffffff</color>
 
     <!-- The color of the gear shown behind a notification -->
-    <color name="notification_gear_color">#ff757575</color>
+    <color name="notification_gear_color">@color/GM2_grey_700</color>
 
     <!-- The color of the text inside a notification -->
     <color name="notification_primary_text_color">@*android:color/notification_primary_text_color_light</color>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 8bc84c6..8174434 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2459,4 +2459,11 @@
     <string name="bubble_accessibility_action_move_bottom_right">Move bottom right</string>
     <!-- Text used for the bubble dismiss area. Bubbles dragged to, or flung towards, this area will go away. [CHAR LIMIT=20] -->
     <string name="bubble_dismiss_text">Dismiss</string>
+
+    <!-- Notification content text when the system navigation mode changes as a result of changing the default launcher [CHAR LIMIT=NONE] -->
+    <string name="notification_content_system_nav_changed">System navigation updated. To make changes, go to Settings.</string>
+
+    <!-- Notification content text when switching to a default launcher that supports gesture navigation [CHAR LIMIT=NONE] -->
+    <string name="notification_content_gesture_nav_available">Go to Settings to update system navigation</string>
+
 </resources>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InputChannelCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InputChannelCompat.java
index 6567b6a..2b1fce8 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InputChannelCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InputChannelCompat.java
@@ -18,7 +18,6 @@
 
 import android.os.Bundle;
 import android.os.Looper;
-import android.util.Pair;
 import android.view.BatchedInputEventReceiver;
 import android.view.Choreographer;
 import android.view.InputChannel;
@@ -42,19 +41,6 @@
     }
 
     /**
-     * Creates a dispatcher and receiver pair to better handle events across threads.
-     */
-    public static Pair<InputEventDispatcher, InputEventReceiver> createPair(String name,
-            Looper looper, Choreographer choreographer, InputEventListener listener) {
-        InputChannel[] channels = InputChannel.openInputChannelPair(name);
-
-        InputEventDispatcher dispatcher = new InputEventDispatcher(channels[0], looper);
-        InputEventReceiver receiver = new InputEventReceiver(channels[1], looper, choreographer,
-                listener);
-        return Pair.create(dispatcher, receiver);
-    }
-
-    /**
      * Creates a dispatcher from the extras received as part on onInitialize
      */
     public static InputEventReceiver fromBundle(Bundle params, String key,
diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleBehavior.java b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleBehavior.java
index e2c7313..2eda3d7 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleBehavior.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleBehavior.java
@@ -16,31 +16,10 @@
 
 package com.android.systemui.assist;
 
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.systemui.assist.AssistHandleBehaviorController.BehaviorController;
-
 public enum AssistHandleBehavior {
 
-    TEST(new AssistHandleOffBehavior()),
-    OFF(new AssistHandleOffBehavior()),
-    LIKE_HOME(new AssistHandleLikeHomeBehavior()),
-    REMINDER_EXP(new AssistHandleReminderExpBehavior());
-
-    private BehaviorController mController;
-
-    AssistHandleBehavior(BehaviorController controller) {
-        mController = controller;
-    }
-
-    BehaviorController getController() {
-        return mController;
-    }
-
-    @VisibleForTesting
-    void setTestController(BehaviorController controller) {
-        if (this.equals(TEST)) {
-            mController = controller;
-        }
-    }
+    TEST,
+    OFF,
+    LIKE_HOME,
+    REMINDER_EXP;
 }
diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleBehaviorController.java b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleBehaviorController.java
index 1aa47cc..01deb03 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleBehaviorController.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleBehaviorController.java
@@ -16,23 +16,27 @@
 
 package com.android.systemui.assist;
 
+import android.content.ComponentName;
 import android.content.Context;
 import android.os.Handler;
 import android.os.SystemClock;
-import android.os.SystemProperties;
 import android.provider.DeviceConfig;
 import android.util.Log;
 
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.AssistUtils;
 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
+import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.Dependency;
 import com.android.systemui.ScreenDecorations;
 import com.android.systemui.SysUiServiceProvider;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.statusbar.phone.NavigationModeController;
 
+import java.util.EnumMap;
+import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Supplier;
 
@@ -46,10 +50,7 @@
 
     private static final String TAG = "AssistHandleBehavior";
 
-    private static final String SHOWN_FREQUENCY_THRESHOLD_KEY =
-            "ASSIST_HANDLES_SHOWN_FREQUENCY_THRESHOLD_MS";
     private static final long DEFAULT_SHOWN_FREQUENCY_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(10);
-    private static final String SHOW_AND_GO_DURATION_KEY = "ASSIST_HANDLES_SHOW_AND_GO_DURATION_MS";
     private static final long DEFAULT_SHOW_AND_GO_DURATION_MS = TimeUnit.SECONDS.toMillis(3);
 
     /**
@@ -59,9 +60,12 @@
     private static final AssistHandleBehavior DEFAULT_BEHAVIOR = AssistHandleBehavior.REMINDER_EXP;
 
     private final Context mContext;
+    private final AssistUtils mAssistUtils;
     private final Handler mHandler;
     private final Runnable mHideHandles = this::hideHandles;
     private final Supplier<ScreenDecorations> mScreenDecorationsSupplier;
+    private final Map<AssistHandleBehavior, BehaviorController> mBehaviorMap =
+            new EnumMap<>(AssistHandleBehavior.class);
 
     private boolean mHandlesShowing = false;
     private long mHandlesLastHiddenAt;
@@ -72,20 +76,33 @@
     private AssistHandleBehavior mCurrentBehavior = AssistHandleBehavior.OFF;
     private boolean mInGesturalMode;
 
-    AssistHandleBehaviorController(Context context, Handler handler) {
-        this(context, handler, () ->
-                SysUiServiceProvider.getComponent(context, ScreenDecorations.class));
+    AssistHandleBehaviorController(Context context, AssistUtils assistUtils, Handler handler) {
+        this(
+                context,
+                assistUtils,
+                handler, () -> SysUiServiceProvider.getComponent(context, ScreenDecorations.class),
+                /* testBehavior = */ null);
     }
 
     @VisibleForTesting
     AssistHandleBehaviorController(
             Context context,
+            AssistUtils assistUtils,
             Handler handler,
-            Supplier<ScreenDecorations> screenDecorationsSupplier) {
+            Supplier<ScreenDecorations> screenDecorationsSupplier,
+            @Nullable BehaviorController testBehavior) {
         mContext = context;
+        mAssistUtils = assistUtils;
         mHandler = handler;
         mScreenDecorationsSupplier = screenDecorationsSupplier;
 
+        mBehaviorMap.put(AssistHandleBehavior.OFF, new AssistHandleOffBehavior());
+        mBehaviorMap.put(AssistHandleBehavior.LIKE_HOME, new AssistHandleLikeHomeBehavior());
+        mBehaviorMap.put(AssistHandleBehavior.REMINDER_EXP, new AssistHandleReminderExpBehavior());
+        if (testBehavior != null) {
+            mBehaviorMap.put(AssistHandleBehavior.TEST, testBehavior);
+        }
+
         mInGesturalMode = QuickStepContract.isGesturalMode(
                 Dependency.get(NavigationModeController.class)
                         .addListener(this::handleNavigationModeChange));
@@ -126,7 +143,7 @@
     }
 
     void onAssistantGesturePerformed() {
-        mCurrentBehavior.getController().onAssistantGesturePerformed();
+        mBehaviorMap.get(mCurrentBehavior).onAssistantGesturePerformed();
     }
 
     void setBehavior(AssistHandleBehavior behavior) {
@@ -134,9 +151,14 @@
             return;
         }
 
+        if (!mBehaviorMap.containsKey(behavior)) {
+            Log.e(TAG, "Unsupported behavior requested: " + behavior.toString());
+            return;
+        }
+
         if (mInGesturalMode) {
-            mCurrentBehavior.getController().onModeDeactivated();
-            behavior.getController().onModeActivated(mContext, this);
+            mBehaviorMap.get(mCurrentBehavior).onModeDeactivated();
+            mBehaviorMap.get(behavior).onModeActivated(mContext, /* callbacks = */ this);
         }
 
         mCurrentBehavior = behavior;
@@ -150,21 +172,26 @@
         }
     }
 
+    private boolean handlesUnblocked(boolean ignoreThreshold) {
+        long timeSinceHidden = SystemClock.elapsedRealtime() - mHandlesLastHiddenAt;
+        boolean notThrottled = ignoreThreshold || timeSinceHidden > getShownFrequencyThreshold();
+        ComponentName assistantComponent =
+                mAssistUtils.getAssistComponentForUser(KeyguardUpdateMonitor.getCurrentUser());
+        return notThrottled && assistantComponent != null;
+    }
+
     private long getShownFrequencyThreshold() {
-        long configValue = DeviceConfig.getLong(
+        return DeviceConfig.getLong(
                 DeviceConfig.NAMESPACE_SYSTEMUI,
                 SystemUiDeviceConfigFlags.ASSIST_HANDLES_SHOWN_FREQUENCY_THRESHOLD_MS,
                 DEFAULT_SHOWN_FREQUENCY_THRESHOLD_MS);
-        return SystemProperties.getLong(
-                SHOWN_FREQUENCY_THRESHOLD_KEY, configValue);
     }
 
     private long getShowAndGoDuration() {
-        long configValue = DeviceConfig.getLong(
+        return DeviceConfig.getLong(
                 DeviceConfig.NAMESPACE_SYSTEMUI,
                 SystemUiDeviceConfigFlags.ASSIST_HANDLES_SHOW_AND_GO_DURATION_MS,
                 DEFAULT_SHOW_AND_GO_DURATION_MS);
-        return SystemProperties.getLong(SHOW_AND_GO_DURATION_KEY, configValue);
     }
 
     private void maybeShowHandles(boolean ignoreThreshold) {
@@ -172,8 +199,7 @@
             return;
         }
 
-        long timeSinceHidden = SystemClock.elapsedRealtime() - mHandlesLastHiddenAt;
-        if (ignoreThreshold || timeSinceHidden > getShownFrequencyThreshold()) {
+        if (handlesUnblocked(ignoreThreshold)) {
             mHandlesShowing = true;
             ScreenDecorations screenDecorations = mScreenDecorationsSupplier.get();
             if (screenDecorations == null) {
@@ -207,9 +233,9 @@
 
         mInGesturalMode = inGesturalMode;
         if (mInGesturalMode) {
-            mCurrentBehavior.getController().onModeActivated(mContext, this);
+            mBehaviorMap.get(mCurrentBehavior).onModeActivated(mContext, /* callbacks = */ this);
         } else {
-            mCurrentBehavior.getController().onModeDeactivated();
+            mBehaviorMap.get(mCurrentBehavior).onModeDeactivated();
             hide();
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
index 8cf836e..2fc79d6 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
@@ -149,7 +149,8 @@
         mAssistUtils = new AssistUtils(context);
         mAssistDisclosure = new AssistDisclosure(context, new Handler());
         mPhoneStateMonitor = new PhoneStateMonitor(context);
-        mHandleController = new AssistHandleBehaviorController(context, new Handler());
+        mHandleController =
+                new AssistHandleBehaviorController(context, mAssistUtils, new Handler());
 
         registerVoiceInteractionSessionListener();
         mInterestingConfigChanges = new InterestingConfigChanges(ActivityInfo.CONFIG_ORIENTATION
diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java b/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java
index 7ad6dfd..b1be811 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java
@@ -35,6 +35,8 @@
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
+import com.android.systemui.ScreenDecorations;
+import com.android.systemui.SysUiServiceProvider;
 import com.android.systemui.assist.AssistManager;
 
 /**
@@ -92,12 +94,12 @@
         if (progress == 1) {
             animateInvocationCompletion(type, 0);
         } else if (progress == 0) {
-            mInvocationInProgress = false;
             hide();
         } else {
             if (!mInvocationInProgress) {
                 attach();
                 mInvocationInProgress = true;
+                updateAssistHandleVisibility();
             }
             setProgressInternal(type, progress);
         }
@@ -129,6 +131,7 @@
         }
         mInvocationLightsView.hide();
         mInvocationInProgress = false;
+        updateAssistHandleVisibility();
     }
 
     /**
@@ -139,6 +142,12 @@
         mInvocationLightsView.setColors(color1, color2, color3, color4);
     }
 
+    private void updateAssistHandleVisibility() {
+        ScreenDecorations decorations = SysUiServiceProvider.getComponent(mRoot.getContext(),
+                ScreenDecorations.class);
+        decorations.setAssistHintBlocked(mInvocationInProgress);
+    }
+
     private void attach() {
         if (!mAttached) {
             mWindowManager.addView(mRoot, mLayoutParams);
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index c63389a..771df2d 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -330,9 +330,7 @@
         mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER;
 
         mBubbleContainer = new PhysicsAnimationLayout(context);
-        mBubbleContainer.setMaxRenderedChildren(
-                getResources().getInteger(R.integer.bubbles_max_rendered));
-        mBubbleContainer.setController(mStackAnimationController);
+        mBubbleContainer.setActiveController(mStackAnimationController);
         mBubbleContainer.setElevation(elevation);
         mBubbleContainer.setClipChildren(false);
         addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
@@ -728,7 +726,7 @@
     public void updateBubbleOrder(List<Bubble> bubbles) {
         for (int i = 0; i < bubbles.size(); i++) {
             Bubble bubble = bubbles.get(i);
-            mBubbleContainer.moveViewTo(bubble.iconView, i);
+            mBubbleContainer.reorderView(bubble.iconView, i);
         }
     }
 
@@ -908,19 +906,17 @@
             };
 
             if (shouldExpand) {
-                mBubbleContainer.setController(mExpandedAnimationController);
-                mExpandedAnimationController.expandFromStack(
-                        mStackAnimationController.getStackPositionAlongNearestHorizontalEdge()
-                        /* collapseTo */,
-                        () -> {
-                            updatePointerPosition();
-                            updateAfter.run();
-                        } /* after */);
+                mBubbleContainer.setActiveController(mExpandedAnimationController);
+                mExpandedAnimationController.expandFromStack(() -> {
+                    updatePointerPosition();
+                    updateAfter.run();
+                } /* after */);
             } else {
                 mBubbleContainer.cancelAllAnimations();
                 mExpandedAnimationController.collapseBackToStack(
+                        mStackAnimationController.getStackPositionAlongNearestHorizontalEdge(),
                         () -> {
-                            mBubbleContainer.setController(mStackAnimationController);
+                            mBubbleContainer.setActiveController(mStackAnimationController);
                             updateAfter.run();
                         });
             }
@@ -1013,7 +1009,7 @@
         }
 
         mStackAnimationController.cancelStackPositionAnimations();
-        mBubbleContainer.setController(mStackAnimationController);
+        mBubbleContainer.setActiveController(mStackAnimationController);
         hideFlyoutImmediate();
 
         mDraggingInDismissTarget = false;
@@ -1111,6 +1107,10 @@
     /** Called when a gesture is completed or cancelled. */
     void onGestureFinished() {
         mIsGestureInProgress = false;
+
+        if (mIsExpanded) {
+            mExpandedAnimationController.onGestureFinished();
+        }
     }
 
     /** Prepares and starts the desaturate/darken animation on the bubble stack. */
@@ -1201,6 +1201,7 @@
      */
     void magnetToStackIfNeededThenAnimateDismissal(
             View touchedView, float velX, float velY, Runnable after) {
+        final View draggedOutBubble = mExpandedAnimationController.getDraggedOutBubble();
         final Runnable animateDismissal = () -> {
             mAfterMagnet = null;
 
@@ -1218,7 +1219,7 @@
                             resetDesaturationAndDarken();
                         });
             } else {
-                mExpandedAnimationController.dismissDraggedOutBubble(() -> {
+                mExpandedAnimationController.dismissDraggedOutBubble(draggedOutBubble, () -> {
                     mAnimatingMagnet = false;
                     mShowingDismiss = false;
                     mDraggingInDismissTarget = false;
@@ -1385,10 +1386,18 @@
                 };
 
                 // Post in case layout isn't complete and getWidth returns 0.
-                post(() -> mFlyout.showFlyout(
-                        updateMessage, mStackAnimationController.getStackPosition(), getWidth(),
-                        mStackAnimationController.isStackOnLeftSide(),
-                        bubble.iconView.getBadgeColor(), mAfterFlyoutHides));
+                post(() -> {
+                    // An auto-expanding bubble could have been posted during the time it takes to
+                    // layout.
+                    if (isExpanded()) {
+                        return;
+                    }
+
+                    mFlyout.showFlyout(
+                            updateMessage, mStackAnimationController.getStackPosition(), getWidth(),
+                            mStackAnimationController.isStackOnLeftSide(),
+                            bubble.iconView.getBadgeColor(), mAfterFlyoutHides);
+                });
             }
 
             mFlyout.removeCallbacks(mHideFlyout);
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
index 24337a3..1fa0e12 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
@@ -22,6 +22,7 @@
 import android.view.View;
 import android.view.WindowInsets;
 
+import androidx.annotation.Nullable;
 import androidx.dynamicanimation.animation.DynamicAnimation;
 import androidx.dynamicanimation.animation.SpringForce;
 
@@ -63,12 +64,16 @@
     private Point mDisplaySize;
     /** Size of dismiss target at bottom of screen. */
     private float mPipDismissHeight;
-    /** Max number of bubbles shown in row above expanded view.*/
-    private int mBubblesMaxRendered;
 
     /** Whether the dragged-out bubble is in the dismiss target. */
     private boolean mIndividualBubbleWithinDismissTarget = false;
 
+    private boolean mAnimatingExpand = false;
+    private boolean mAnimatingCollapse = false;
+    private Runnable mAfterExpand;
+    private Runnable mAfterCollapse;
+    private PointF mCollapsePoint;
+
     /**
      * Whether the dragged out bubble is springing towards the touch point, rather than using the
      * default behavior of moving directly to the touch point.
@@ -97,56 +102,60 @@
     private View mBubbleDraggingOut;
 
     /**
-     * Drag velocities for the dragging-out bubble when the drag finished. These are used by
-     * {@link #onChildRemoved} to animate out the bubble while respecting touch velocity.
-     */
-    private float mBubbleDraggingOutVelX;
-    private float mBubbleDraggingOutVelY;
-
-    @Override
-    protected void setLayout(PhysicsAnimationLayout layout) {
-        super.setLayout(layout);
-
-        final Resources res = layout.getResources();
-        mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
-        mBubblePaddingPx = res.getDimensionPixelSize(R.dimen.bubble_padding);
-        mBubbleSizePx = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
-        mStatusBarHeight =
-                res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
-        mPipDismissHeight = res.getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height);
-        mBubblesMaxRendered = res.getInteger(R.integer.bubbles_max_rendered);
-    }
-
-    /**
      * Animates expanding the bubbles into a row along the top of the screen.
      */
-    public void expandFromStack(PointF collapseTo, Runnable after) {
-        animationsForChildrenFromIndex(
-                0, /* startIndex */
-                new ChildAnimationConfigurator() {
-                    @Override
-                    public void configureAnimationForChildAtIndex(
-                            int index, PhysicsAnimationLayout.PhysicsPropertyAnimator animation) {
-                        animation.position(getBubbleLeft(index), getExpandedY());
-                    }
-            })
-            .startAll(after);
+    public void expandFromStack(Runnable after) {
+        mAnimatingCollapse = false;
+        mAnimatingExpand = true;
+        mAfterExpand = after;
 
-        mCollapseToPoint = collapseTo;
+        startOrUpdateExpandAnimation();
     }
 
     /** Animate collapsing the bubbles back to their stacked position. */
-    public void collapseBackToStack(Runnable after) {
-        // Stack to the left if we're going to the left, or right if not.
-        final float sideMultiplier = mLayout.isFirstChildXLeftOfCenter(mCollapseToPoint.x) ? -1 : 1;
+    public void collapseBackToStack(PointF collapsePoint, Runnable after) {
+        mAnimatingExpand = false;
+        mAnimatingCollapse = true;
+        mAfterCollapse = after;
+        mCollapsePoint = collapsePoint;
 
+        startOrUpdateCollapseAnimation();
+    }
+
+    private void startOrUpdateExpandAnimation() {
         animationsForChildrenFromIndex(
                 0, /* startIndex */
-                (index, animation) ->
+                (index, animation) -> animation.position(getBubbleLeft(index), getExpandedY()))
+                .startAll(() -> {
+                    mAnimatingExpand = false;
+
+                    if (mAfterExpand != null) {
+                        mAfterExpand.run();
+                    }
+
+                    mAfterExpand = null;
+                });
+    }
+
+    private void startOrUpdateCollapseAnimation() {
+        // Stack to the left if we're going to the left, or right if not.
+        final float sideMultiplier = mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x) ? -1 : 1;
+        animationsForChildrenFromIndex(
+                0, /* startIndex */
+                (index, animation) -> {
                     animation.position(
-                            mCollapseToPoint.x + (sideMultiplier * index * mStackOffsetPx),
-                            mCollapseToPoint.y))
-            .startAll(after /* endAction */);
+                            mCollapsePoint.x + (sideMultiplier * index * mStackOffsetPx),
+                            mCollapsePoint.y);
+                })
+                .startAll(() -> {
+                    mAnimatingCollapse = false;
+
+                    if (mAfterCollapse != null) {
+                        mAfterCollapse.run();
+                    }
+
+                    mAfterCollapse = null;
+                });
     }
 
     /** Prepares the given bubble to be dragged out. */
@@ -190,10 +199,10 @@
     }
 
     /** Plays a dismiss animation on the dragged out bubble. */
-    public void dismissDraggedOutBubble(Runnable after) {
+    public void dismissDraggedOutBubble(View bubble, Runnable after) {
         mIndividualBubbleWithinDismissTarget = false;
 
-        animationForChild(mBubbleDraggingOut)
+        animationForChild(bubble)
                 .withStiffness(SpringForce.STIFFNESS_HIGH)
                 .scaleX(1.1f)
                 .scaleY(1.1f)
@@ -203,6 +212,10 @@
         updateBubblePositions();
     }
 
+    @Nullable public View getDraggedOutBubble() {
+        return mBubbleDraggingOut;
+    }
+
     /** Magnets the given bubble to the dismiss target. */
     public void magnetBubbleToDismiss(
             View bubbleView, float velX, float velY, float destY, Runnable after) {
@@ -241,24 +254,17 @@
         final int index = mLayout.indexOfChild(bubbleView);
 
         animationForChildAtIndex(index)
-            .position(getBubbleLeft(index), getExpandedY())
-            .withPositionStartVelocities(velX, velY)
-            .start(() -> bubbleView.setTranslationZ(0f) /* after */);
+                .position(getBubbleLeft(index), getExpandedY())
+                .withPositionStartVelocities(velX, velY)
+                .start(() -> bubbleView.setTranslationZ(0f) /* after */);
 
-        mBubbleDraggingOut = null;
-        mBubbleDraggedOutEnough = false;
         updateBubblePositions();
     }
 
-    /**
-     * Sets configuration variables so that when the given bubble is removed, the animations are
-     * started with the given velocities.
-     */
-    public void prepareForDismissalWithVelocity(View bubbleView, float velX, float velY) {
-        mBubbleDraggingOut = bubbleView;
-        mBubbleDraggingOutVelX = velX;
-        mBubbleDraggingOutVelY = velY;
+    /** Resets bubble drag out gesture flags. */
+    public void onGestureFinished() {
         mBubbleDraggedOutEnough = false;
+        mBubbleDraggingOut = null;
     }
 
     /**
@@ -297,6 +303,23 @@
     }
 
     @Override
+    void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
+        final Resources res = layout.getResources();
+        mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
+        mBubblePaddingPx = res.getDimensionPixelSize(R.dimen.bubble_padding);
+        mBubbleSizePx = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
+        mStatusBarHeight =
+                res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
+        mPipDismissHeight = res.getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height);
+
+        // Ensure that all child views are at 1x scale, and visible, in case they were animating
+        // in.
+        mLayout.setVisibility(View.VISIBLE);
+        animationsForChildrenFromIndex(0 /* startIndex */, (index, animation) ->
+                animation.scaleX(1f).scaleY(1f).alpha(1f)).startAll();
+    }
+
+    @Override
     Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
         return Sets.newHashSet(
                 DynamicAnimation.TRANSLATION_X,
@@ -325,14 +348,21 @@
 
     @Override
     void onChildAdded(View child, int index) {
-        child.setTranslationX(getXForChildAtIndex(index));
-
-        animationForChild(child)
-                .translationY(
-                        getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR, /* from */
-                        getExpandedY() /* to */)
-                .start();
-        updateBubblePositions();
+        // If a bubble is added while the expand/collapse animations are playing, update the
+        // animation to include the new bubble.
+        if (mAnimatingExpand) {
+            startOrUpdateExpandAnimation();
+        } else if (mAnimatingCollapse) {
+            startOrUpdateCollapseAnimation();
+        } else {
+            child.setTranslationX(getXForChildAtIndex(index));
+            animationForChild(child)
+                    .translationY(
+                            getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR, /* from */
+                            getExpandedY() /* to */)
+                    .start();
+            updateBubblePositions();
+        }
     }
 
     @Override
@@ -357,19 +387,15 @@
     }
 
     @Override
-    protected void setChildVisibility(View child, int index, int visibility) {
-        if (visibility == View.VISIBLE) {
-            // Set alpha to 0 but then become visible immediately so the animation is visible.
-            child.setAlpha(0f);
-            child.setVisibility(View.VISIBLE);
-        }
-
-        animationForChild(child)
-                .alpha(visibility == View.GONE ? 0f : 1f)
-                .start(() -> super.setChildVisibility(child, index, visibility) /* after */);
+    void onChildReordered(View child, int oldIndex, int newIndex) {
+        updateBubblePositions();
     }
 
     private void updateBubblePositions() {
+        if (mAnimatingExpand || mAnimatingCollapse) {
+            return;
+        }
+
         for (int i = 0; i < mLayout.getChildCount(); i++) {
             final View bubble = mLayout.getChildAt(i);
 
@@ -378,6 +404,7 @@
             if (bubble.equals(mBubbleDraggingOut)) {
                 return;
             }
+
             animationForChild(bubble)
                     .translationX(getBubbleLeft(i))
                     .start();
@@ -403,10 +430,7 @@
             return 0;
         }
         int bubbleCount = mLayout.getChildCount();
-        if (bubbleCount > mBubblesMaxRendered) {
-            // Only shown bubbles are relevant for calculating position.
-            bubbleCount = mBubblesMaxRendered;
-        }
+
         // Width calculations.
         double bubble = bubbleCount * mBubbleSizePx;
         float gap = (bubbleCount - 1) * mBubblePaddingPx;
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java
index 997d2c4..3a33392 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java
@@ -17,10 +17,12 @@
 package com.android.systemui.bubbles.animation;
 
 import android.content.Context;
+import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.FrameLayout;
 
+import androidx.annotation.Nullable;
 import androidx.dynamicanimation.animation.DynamicAnimation;
 import androidx.dynamicanimation.animation.SpringAnimation;
 import androidx.dynamicanimation.animation.SpringForce;
@@ -137,12 +139,33 @@
          */
         abstract void onChildRemoved(View child, int index, Runnable finishRemoval);
 
+        /** Called when a child view has been reordered in the view hierachy. */
+        abstract void onChildReordered(View child, int oldIndex, int newIndex);
+
+        /**
+         * Called when the controller is set as the active animation controller for the given
+         * layout. Once active, the controller can start animations using the animator instances
+         * returned by {@link #animationForChild}.
+         *
+         * While all animations started by the previous controller will be cancelled, the new
+         * controller should not make any assumptions about the state of the layout or its children.
+         * Their translation, alpha, scale, etc. values may have been changed by the previous
+         * controller and should be reset here if relevant.
+         */
+        abstract void onActiveControllerForLayout(PhysicsAnimationLayout layout);
+
         protected PhysicsAnimationLayout mLayout;
 
         PhysicsAnimationController() { }
 
+        /** Whether this controller is the currently active controller for its associated layout. */
+        protected boolean isActiveController() {
+            return this == mLayout.mController;
+        }
+
         protected void setLayout(PhysicsAnimationLayout layout) {
             this.mLayout = layout;
+            onActiveControllerForLayout(layout);
         }
 
         protected PhysicsAnimationLayout getLayout() {
@@ -150,15 +173,6 @@
         }
 
         /**
-         * Sets the child's visibility when it moves beyond or within the limits set by a call to
-         * {@link PhysicsAnimationLayout#setMaxRenderedChildren}. This can be overridden to animate
-         * this transition.
-         */
-        protected void setChildVisibility(View child, int index, int visibility) {
-            child.setVisibility(visibility);
-        }
-
-        /**
          * Returns a {@link PhysicsPropertyAnimator} instance for the given child view.
          */
         protected PhysicsPropertyAnimator animationForChild(View child) {
@@ -170,6 +184,9 @@
                 child.setTag(R.id.physics_animator_tag, animator);
             }
 
+            animator.clearAnimator();
+            animator.setAssociatedController(this);
+
             return animator;
         }
 
@@ -235,32 +252,17 @@
             new HashMap<>();
 
     /** The currently active animation controller. */
-    private PhysicsAnimationController mController;
-
-    /**
-     * The maximum number of children to render and animate at a time. See
-     * {@link #setMaxRenderedChildren}.
-     */
-    private int mMaxRenderedChildren = 5;
+    @Nullable protected PhysicsAnimationController mController;
 
     public PhysicsAnimationLayout(Context context) {
         super(context);
     }
 
     /**
-     * The maximum number of children to render and animate at a time. Any child views added beyond
-     * this limit will be set to {@link View#GONE}. If any animations attempt to run on the view,
-     * the corresponding property will be set with no animation.
-     */
-    public void setMaxRenderedChildren(int max) {
-        this.mMaxRenderedChildren = max;
-    }
-
-    /**
      * Sets the animation controller and constructs or reconfigures the layout's physics animations
      * to meet the controller's specifications.
      */
-    public void setController(PhysicsAnimationController controller) {
+    public void setActiveController(PhysicsAnimationController controller) {
         cancelAllAnimations();
         mEndActionForProperty.clear();
 
@@ -312,42 +314,11 @@
 
     @Override
     public void addView(View child, int index, ViewGroup.LayoutParams params) {
-        super.addView(child, index, params);
-
-        // Set up animations for the new view, if the controller is set. If it isn't set, we'll be
-        // setting up animations for all children when setController is called.
-        if (mController != null) {
-            for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) {
-                setUpAnimationForChild(property, child, index);
-            }
-
-            mController.onChildAdded(child, index);
-        }
-
-        setChildrenVisibility();
+        addViewInternal(child, index, params, false /* isReorder */);
     }
 
     @Override
     public void removeView(View view) {
-        removeViewAndThen(view, /* callback */ null);
-    }
-
-    @Override
-    public void removeViewAt(int index) {
-        removeView(getChildAt(index));
-    }
-
-    /** Immediately moves the view from wherever it currently is, to the given index. */
-    public void moveViewTo(View view, int index) {
-        super.removeView(view);
-        addView(view, index);
-    }
-
-    /**
-     * Let the controller know that this view should be removed, and then call the callback once the
-     * controller has finished any removal animations and the view has actually been removed.
-     */
-    public void removeViewAndThen(View view, Runnable callback) {
         if (mController != null) {
             final int index = indexOfChild(view);
 
@@ -355,8 +326,6 @@
             super.removeView(view);
             addTransientView(view, index);
 
-            setChildrenVisibility();
-
             // Tell the controller to animate this view out, and call the callback when it's
             // finished.
             mController.onChildRemoved(view, index, () -> {
@@ -364,19 +333,28 @@
                 // any are still running and then remove it.
                 cancelAnimationsOnView(view);
                 removeTransientView(view);
-
-                if (callback != null) {
-                    callback.run();
-                }
             });
         } else {
             // Without a controller, nobody will animate this view out, so it gets an unceremonious
             // departure.
             super.removeView(view);
+        }
+    }
 
-            if (callback != null) {
-                callback.run();
-            }
+    @Override
+    public void removeViewAt(int index) {
+        removeView(getChildAt(index));
+    }
+
+    /** Immediately re-orders the view to the given index. */
+    public void reorderView(View view, int index) {
+        final int oldIndex = indexOfChild(view);
+
+        super.removeView(view);
+        addViewInternal(view, index, view.getLayoutParams(), true /* isReorder */);
+
+        if (mController != null) {
+            mController.onChildReordered(view, oldIndex, index);
         }
     }
 
@@ -427,6 +405,10 @@
         }
     }
 
+    protected boolean isActiveController(PhysicsAnimationController controller) {
+        return mController == controller;
+    }
+
     /** Whether the first child would be left of center if translated to the given x value. */
     protected boolean isFirstChildXLeftOfCenter(float x) {
         if (getChildCount() > 0) {
@@ -454,6 +436,26 @@
     }
 
     /**
+     * Adds a view to the layout. If this addition is not the result of a call to
+     * {@link #reorderView}, this will also notify the controller via
+     * {@link PhysicsAnimationController#onChildAdded} and set up animations for the view.
+     */
+    private void addViewInternal(
+            View child, int index, ViewGroup.LayoutParams params, boolean isReorder) {
+        super.addView(child, index, params);
+
+        // Set up animations for the new view, if the controller is set. If it isn't set, we'll be
+        // setting up animations for all children when setActiveController is called.
+        if (mController != null && !isReorder) {
+            for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) {
+                setUpAnimationForChild(property, child, index);
+            }
+
+            mController.onChildAdded(child, index);
+        }
+    }
+
+    /**
      * Retrieves the animation of the given property from the view at the given index via the view
      * tag system.
      */
@@ -481,33 +483,16 @@
         SpringAnimation newAnim = new SpringAnimation(child, property);
         newAnim.addUpdateListener((animation, value, velocity) -> {
             final int indexOfChild = indexOfChild(child);
-            final int nextAnimInChain =
-                    mController.getNextAnimationInChain(property, indexOfChild);
+            final int nextAnimInChain = mController.getNextAnimationInChain(property, indexOfChild);
 
             if (nextAnimInChain == PhysicsAnimationController.NONE || indexOfChild < 0) {
                 return;
             }
 
-            final int animIndex = indexOfChild(child);
-            final float offset =
-                    mController.getOffsetForChainedPropertyAnimation(property);
-
-            // If this property's animations should be chained, then check to see if there is a
-            // subsequent animation within the rendering limit, and if so, tell it to animate to
-            // this animation's new value (plus the offset).
-            if (nextAnimInChain < Math.min(getChildCount(), mMaxRenderedChildren)) {
-                getAnimationAtIndex(property, animIndex + 1)
+            final float offset = mController.getOffsetForChainedPropertyAnimation(property);
+            if (nextAnimInChain < getChildCount()) {
+                getAnimationAtIndex(property, nextAnimInChain)
                         .animateToFinalPosition(value + offset);
-            } else if (nextAnimInChain < getChildCount()) {
-                // If the next child view is not rendered, update the property directly without
-                // animating it, so that the view is still in the correct state if it later
-                // becomes visible.
-                for (int i = nextAnimInChain; i < getChildCount(); i++) {
-                    // 'value' here is the value of the last child within the rendering limit,
-                    // not the first child's value - so we want to subtract the last child's
-                    // index when calculating the offset.
-                    property.setValue(getChildAt(i), value + offset * (i - animIndex));
-                }
             }
         });
 
@@ -516,22 +501,6 @@
         child.setTag(getTagIdForProperty(property), newAnim);
     }
 
-    /** Hides children beyond the max rendering count. */
-    private void setChildrenVisibility() {
-        for (int i = 0; i < getChildCount(); i++) {
-            final int targetVisibility = i < mMaxRenderedChildren ? View.VISIBLE : View.GONE;
-            final View targetView = getChildAt(i);
-
-            if (targetView.getVisibility() != targetVisibility) {
-                if (mController != null) {
-                    mController.setChildVisibility(targetView, i, targetVisibility);
-                } else {
-                    targetView.setVisibility(targetVisibility);
-                }
-            }
-        }
-    }
-
     /** Return a stable ID to use as a tag key for the given property's animations. */
     private int getTagIdForProperty(DynamicAnimation.ViewProperty property) {
         if (property.equals(DynamicAnimation.TRANSLATION_X)) {
@@ -592,7 +561,7 @@
         private View mView;
 
         /** Start velocity to use for all property animations. */
-        private float mDefaultStartVelocity = 0f;
+        private float mDefaultStartVelocity = -Float.MAX_VALUE;
 
         /** Start delay to use when start is called. */
         private long mStartDelay = 0;
@@ -625,6 +594,15 @@
          */
         private Map<DynamicAnimation.ViewProperty, Float> mAnimatedProperties = new HashMap<>();
 
+        /**
+         * All of the initial property values that have been set. These values will be instantly set
+         * when {@link #start} is called, just before the animation begins.
+         */
+        private Map<DynamicAnimation.ViewProperty, Float> mInitialPropertyValues = new HashMap<>();
+
+        /** The animation controller that last retrieved this animator instance. */
+        private PhysicsAnimationController mAssociatedController;
+
         protected PhysicsPropertyAnimator(View view) {
             this.mView = view;
         }
@@ -644,7 +622,7 @@
 
         /** Set the view's alpha value to 'from', then animate it to the given value. */
         public PhysicsPropertyAnimator alpha(float from, float to, Runnable... endActions) {
-            mView.setAlpha(from);
+            mInitialPropertyValues.put(DynamicAnimation.ALPHA, from);
             return alpha(to, endActions);
         }
 
@@ -656,7 +634,7 @@
         /** Set the view's translationX value to 'from', then animate it to the given value. */
         public PhysicsPropertyAnimator translationX(
                 float from, float to, Runnable... endActions) {
-            mView.setTranslationX(from);
+            mInitialPropertyValues.put(DynamicAnimation.TRANSLATION_X, from);
             return translationX(to, endActions);
         }
 
@@ -668,7 +646,7 @@
         /** Set the view's translationY value to 'from', then animate it to the given value. */
         public PhysicsPropertyAnimator translationY(
                 float from, float to, Runnable... endActions) {
-            mView.setTranslationY(from);
+            mInitialPropertyValues.put(DynamicAnimation.TRANSLATION_Y, from);
             return translationY(to, endActions);
         }
 
@@ -690,7 +668,7 @@
 
         /** Set the view's scaleX value to 'from', then animate it to the given value. */
         public PhysicsPropertyAnimator scaleX(float from, float to, Runnable... endActions) {
-            mView.setScaleX(from);
+            mInitialPropertyValues.put(DynamicAnimation.SCALE_X, from);
             return scaleX(to, endActions);
         }
 
@@ -701,7 +679,7 @@
 
         /** Set the view's scaleY value to 'from', then animate it to the given value. */
         public PhysicsPropertyAnimator scaleY(float from, float to, Runnable... endActions) {
-            mView.setScaleY(from);
+            mInitialPropertyValues.put(DynamicAnimation.SCALE_Y, from);
             return scaleY(to, endActions);
         }
 
@@ -750,6 +728,13 @@
          * animated property on every child (including chained animations) have ended.
          */
         public void start(Runnable... after) {
+            if (!isActiveController(mAssociatedController)) {
+                Log.w(TAG, "Only the active animation controller is allowed to start animations. "
+                        + "Use PhysicsAnimationLayout#setActiveController to set the active "
+                        + "animation controller.");
+                return;
+            }
+
             final Set<DynamicAnimation.ViewProperty> properties = getAnimatedProperties();
 
             // If there are end actions, set an end listener on the layout for all the properties
@@ -791,6 +776,10 @@
 
             // Actually start the animations.
             for (DynamicAnimation.ViewProperty property : properties) {
+                if (mInitialPropertyValues.containsKey(property)) {
+                    property.setValue(mView, mInitialPropertyValues.get(property));
+                }
+
                 final SpringForce defaultSpringForce = mController.getSpringForce(property, mView);
                 animateValueForChild(
                         property,
@@ -803,14 +792,7 @@
                         mEndActionsForProperty.get(property));
             }
 
-            // Clear out the animator.
-            mAnimatedProperties.clear();
-            mPositionStartVelocities.clear();
-            mDefaultStartVelocity = 0;
-            mStartDelay = 0;
-            mStiffness = -1;
-            mDampingRatio = -1;
-            mEndActionsForProperty.clear();
+            clearAnimator();
         }
 
         /** Returns the set of properties that will animate once {@link #start} is called. */
@@ -847,20 +829,50 @@
                     });
                 }
 
-                animation.getSpring().setStiffness(stiffness);
-                animation.getSpring().setDampingRatio(dampingRatio);
+                final SpringForce animationSpring = animation.getSpring();
 
-                if (startVel > 0) {
-                    animation.setStartVelocity(startVel);
+                if (animationSpring == null) {
+                    return;
                 }
 
+                final Runnable configureAndStartAnimation = () -> {
+                    animationSpring.setStiffness(stiffness);
+                    animationSpring.setDampingRatio(dampingRatio);
+
+                    if (startVel > -Float.MAX_VALUE) {
+                        animation.setStartVelocity(startVel);
+                    }
+
+                    animationSpring.setFinalPosition(value);
+                    animation.start();
+                };
+
                 if (startDelay > 0) {
-                    postDelayed(() -> animation.animateToFinalPosition(value), startDelay);
+                    postDelayed(configureAndStartAnimation, startDelay);
                 } else {
-                    animation.animateToFinalPosition(value);
+                    configureAndStartAnimation.run();
                 }
             }
         }
+
+        private void clearAnimator() {
+            mInitialPropertyValues.clear();
+            mAnimatedProperties.clear();
+            mPositionStartVelocities.clear();
+            mDefaultStartVelocity = -Float.MAX_VALUE;
+            mStartDelay = 0;
+            mStiffness = -1;
+            mDampingRatio = -1;
+            mEndActionsForProperty.clear();
+        }
+
+        /**
+         * Sets the controller that last retrieved this animator instance, so that we can prevent
+         * {@link #start} from actually starting animations if called by a non-active controller.
+         */
+        private void setAssociatedController(PhysicsAnimationController controller) {
+            mAssociatedController = controller;
+        }
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
index b9cdc844..ab8752e4 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
@@ -154,21 +154,6 @@
     /** Height of the status bar. */
     private float mStatusBarHeight;
 
-    @Override
-    protected void setLayout(PhysicsAnimationLayout layout) {
-        super.setLayout(layout);
-
-        Resources res = layout.getResources();
-        mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
-        mIndividualBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
-        mBubblePadding = res.getDimensionPixelSize(R.dimen.bubble_padding);
-        mBubbleOffscreen = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen);
-        mStackStartingVerticalOffset =
-                res.getDimensionPixelSize(R.dimen.bubble_stack_starting_offset_y);
-        mStatusBarHeight =
-                res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
-    }
-
     /**
      * Instantly move the first bubble to the given point, and animate the rest of the stack behind
      * it with the 'following' effect.
@@ -286,6 +271,8 @@
                 },
                 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
 
+        // If we're flinging now, there's no more touch event to catch up to.
+        mFirstBubbleSpringingToTouch = false;
         mIsMovingFromFlinging = true;
         return destinationRelativeX;
     }
@@ -656,19 +643,38 @@
 
         if (mLayout.getChildCount() > 0) {
             animationForChildAtIndex(0).translationX(mStackPosition.x).start();
+        } else {
+            // Set the start position back to the default since we're out of bubbles. New bubbles
+            // will then animate in from the start position.
+            mStackPosition = getDefaultStartPosition();
         }
     }
 
+    @Override
+    void onChildReordered(View child, int oldIndex, int newIndex) {}
+
+    @Override
+    void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
+        Resources res = layout.getResources();
+        mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
+        mIndividualBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
+        mBubblePadding = res.getDimensionPixelSize(R.dimen.bubble_padding);
+        mBubbleOffscreen = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen);
+        mStackStartingVerticalOffset =
+                res.getDimensionPixelSize(R.dimen.bubble_stack_starting_offset_y);
+        mStatusBarHeight =
+                res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
+    }
+
     /** Moves the stack, without any animation, to the starting position. */
     private void moveStackToStartPosition() {
         // Post to ensure that the layout's width and height have been calculated.
         mLayout.setVisibility(View.INVISIBLE);
         mLayout.post(() -> {
+            setStackPosition(mRestingStackPosition == null
+                    ? getDefaultStartPosition()
+                    : mRestingStackPosition);
             mStackMovedToStartPosition = true;
-            setStackPosition(
-                    mRestingStackPosition == null
-                            ? getDefaultStartPosition()
-                            : mRestingStackPosition);
             mLayout.setVisibility(View.VISIBLE);
 
             // Animate in the top bubble now that we're visible.
@@ -707,15 +713,20 @@
         Log.d(TAG, String.format("Setting position to (%f, %f).", pos.x, pos.y));
         mStackPosition.set(pos.x, pos.y);
 
-        mLayout.cancelAllAnimations();
-        cancelStackPositionAnimations();
+        // If we're not the active controller, we don't want to physically move the bubble views.
+        if (isActiveController()) {
+            mLayout.cancelAllAnimations();
+            cancelStackPositionAnimations();
 
-        // Since we're not using the chained animations, apply the offsets manually.
-        final float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
-        final float yOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_Y);
-        for (int i = 0; i < mLayout.getChildCount(); i++) {
-            mLayout.getChildAt(i).setTranslationX(pos.x + (i * xOffset));
-            mLayout.getChildAt(i).setTranslationY(pos.y + (i * yOffset));
+            // Since we're not using the chained animations, apply the offsets manually.
+            final float xOffset = getOffsetForChainedPropertyAnimation(
+                    DynamicAnimation.TRANSLATION_X);
+            final float yOffset = getOffsetForChainedPropertyAnimation(
+                    DynamicAnimation.TRANSLATION_Y);
+            for (int i = 0; i < mLayout.getChildCount(); i++) {
+                mLayout.getChildAt(i).setTranslationX(pos.x + (i * xOffset));
+                mLayout.getChildAt(i).setTranslationY(pos.y + (i * yOffset));
+            }
         }
     }
 
@@ -732,6 +743,10 @@
 
     /** Animates in the given bubble. */
     private void animateInBubble(View child) {
+        if (!isActiveController()) {
+            return;
+        }
+
         child.setTranslationY(mStackPosition.y);
 
         float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ButtonLinearLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ButtonLinearLayout.java
new file mode 100644
index 0000000..94bdd81
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ButtonLinearLayout.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.Button;
+import android.widget.LinearLayout;
+
+public class ButtonLinearLayout extends LinearLayout {
+
+    public ButtonLinearLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return Button.class.getName();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index a517e76..e6f4731 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -939,18 +939,11 @@
             return;
         }
 
-        float alpha =
-                BACKGROUND_ALPHA_DIMMED + (1 - BACKGROUND_ALPHA_DIMMED) * (1.0f - mDimAmount);
-        alpha *= 1f - mInterpolatedDarkAmount;
-        // We need to manually blend in the background color.
-        int scrimColor = mScrimController.getBackgroundColor();
-        int awakeColor = ColorUtils.blendARGB(scrimColor, mBgColor, alpha);
-
         // Interpolate between semi-transparent notification panel background color
         // and white AOD separator.
         float colorInterpolation = MathUtils.smoothStep(0.4f /* start */, 1f /* end */,
                 mLinearDarkAmount);
-        int color = ColorUtils.blendARGB(awakeColor, Color.WHITE, colorInterpolation);
+        int color = ColorUtils.blendARGB(mBgColor, Color.WHITE, colorInterpolation);
 
         if (mCachedBackgroundColor != color) {
             mCachedBackgroundColor = color;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java
index f221865..05a86fa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java
@@ -33,13 +33,14 @@
 import android.os.SystemClock;
 import android.util.Log;
 import android.util.MathUtils;
-import android.view.Choreographer;
 import android.view.Gravity;
 import android.view.IPinnedStackController;
 import android.view.IPinnedStackListener;
 import android.view.ISystemGestureExclusionListener;
+import android.view.InputChannel;
 import android.view.InputDevice;
 import android.view.InputEvent;
+import android.view.InputEventReceiver;
 import android.view.InputMonitor;
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
@@ -53,7 +54,6 @@
 import com.android.systemui.R;
 import com.android.systemui.bubbles.BubbleController;
 import com.android.systemui.recents.OverviewProxyService;
-import com.android.systemui.shared.system.InputChannelCompat.InputEventReceiver;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.WindowManagerWrapper;
 
@@ -165,12 +165,14 @@
         mEdgeWidth = res.getDimensionPixelSize(
                 com.android.internal.R.dimen.config_backGestureInset);
 
-        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+        // Reduce the default touch slop to ensure that we can intercept the gesture
+        // before the app starts to react to it.
+        // TODO(b/130352502) Tune this value and extract into a constant
+        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop() * 0.75f;
         mLongPressTimeout = ViewConfiguration.getLongPressTimeout();
 
         mNavBarHeight = res.getDimensionPixelSize(R.dimen.navigation_bar_frame_height);
-        mMinArrowPosition = res.getDimensionPixelSize(
-                R.dimen.navigation_edge_arrow_min_y);
+        mMinArrowPosition = res.getDimensionPixelSize(R.dimen.navigation_edge_arrow_min_y);
         mFingerOffset = res.getDimensionPixelSize(R.dimen.navigation_edge_finger_offset);
     }
 
@@ -250,9 +252,8 @@
             // Register input event receiver
             mInputMonitor = InputManager.getInstance().monitorGestureInput(
                     "edge-swipe", mDisplayId);
-            mInputEventReceiver = new InputEventReceiver(mInputMonitor.getInputChannel(),
-                    Looper.getMainLooper(), Choreographer.getMainThreadInstance(),
-                    this::onInputEvent);
+            mInputEventReceiver = new SysUiInputEventReceiver(
+                    mInputMonitor.getInputChannel(), Looper.getMainLooper());
 
             // Add a nav bar panel window
             mEdgePanel = new NavigationBarEdgePanel(mContext);
@@ -440,4 +441,15 @@
         }
         InputManager.getInstance().injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
     }
+
+    class SysUiInputEventReceiver extends InputEventReceiver {
+        SysUiInputEventReceiver(InputChannel channel, Looper looper) {
+            super(channel, looper);
+        }
+
+        public void onInputEvent(InputEvent event) {
+            EdgeBackGestureHandler.this.onInputEvent(event);
+            finishInputEvent(event, true);
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
index b7a154d..a0cda69 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
@@ -22,8 +22,6 @@
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Rect;
-import android.graphics.Region;
-import android.graphics.Region.Op;
 import android.util.Log;
 import android.util.Pools;
 import android.view.DisplayCutout;
@@ -78,6 +76,7 @@
     private int[] mTmpTwoArray = new int[2];
     private boolean mHeadsUpGoingAway;
     private int mStatusBarState;
+    private Rect mTouchableRegion = new Rect();
 
     private AnimationStateHandler mAnimationStateHandler;
 
@@ -297,10 +296,13 @@
     @Nullable
     public void updateTouchableRegion(ViewTreeObserver.InternalInsetsInfo info) {
         info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+        info.touchableRegion.set(calculateTouchableRegion());
+    }
 
+    public Rect calculateTouchableRegion() {
         if (!hasPinnedHeadsUp()) {
-            info.touchableRegion.set(0, 0, mStatusBarWindowView.getWidth(), mStatusBarHeight);
-            updateRegionForNotch(info.touchableRegion);
+            mTouchableRegion.set(0, 0, mStatusBarWindowView.getWidth(), mStatusBarHeight);
+            updateRegionForNotch(mTouchableRegion);
         } else {
             NotificationEntry topEntry = getTopEntry();
             if (topEntry.isChildInGroup()) {
@@ -315,11 +317,12 @@
             int minX = mTmpTwoArray[0];
             int maxX = mTmpTwoArray[0] + topRow.getWidth();
             int height = topRow.getIntrinsicHeight();
-            info.touchableRegion.set(minX, 0, maxX, mHeadsUpInset + height);
+            mTouchableRegion.set(minX, 0, maxX, mHeadsUpInset + height);
         }
+        return mTouchableRegion;
     }
 
-    private void updateRegionForNotch(Region region) {
+    private void updateRegionForNotch(Rect region) {
         DisplayCutout cutout = mStatusBarWindowView.getRootWindowInsets().getDisplayCutout();
         if (cutout == null) {
             return;
@@ -330,7 +333,7 @@
         Rect bounds = new Rect();
         ScreenDecorations.DisplayCutoutView.boundsFromDirection(cutout, Gravity.TOP, bounds);
         bounds.offset(0, mDisplayCutoutTouchableRegionSize);
-        region.op(bounds, Op.UNION);
+        region.union(bounds);
     }
 
     @Override
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 7623dee..92aa884 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -100,6 +100,7 @@
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.function.Consumer;
 
@@ -141,6 +142,7 @@
     private static final String COUNTER_PANEL_OPEN_PEEK = "panel_open_peek";
 
     private static final Rect mDummyDirtyRect = new Rect(0, 0, 1, 1);
+    private static final Rect mEmptyRect = new Rect();
 
     private static final AnimationProperties CLOCK_ANIMATION_PROPERTIES = new AnimationProperties()
             .setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
@@ -596,6 +598,25 @@
             mQs.setHeightOverride(mQs.getDesiredHeight());
         }
         updateMaxHeadsUpTranslation();
+        updateGestureExclusionRect();
+    }
+
+    private void updateGestureExclusionRect() {
+        Rect exclusionRect = calculateGestureExclusionRect();
+        setSystemGestureExclusionRects(exclusionRect.isEmpty()
+                ? Collections.EMPTY_LIST
+                : Collections.singletonList(exclusionRect));
+    }
+
+    private Rect calculateGestureExclusionRect() {
+        Rect exclusionRect = null;
+        if (isFullyCollapsed()) {
+            // Note: The heads up manager also calculates the non-pinned touchable region
+            exclusionRect = mHeadsUpManager.calculateTouchableRegion();
+        }
+        return exclusionRect != null
+                ? exclusionRect
+                : mEmptyRect;
     }
 
     private void setIsFullWidth(boolean isFullWidth) {
@@ -1798,6 +1819,7 @@
         updateHeader();
         updateNotificationTranslucency();
         updatePanelExpanded();
+        updateGestureExclusionRect();
         if (DEBUG) {
             invalidate();
         }
@@ -2568,6 +2590,7 @@
             mNotificationStackScroller.runAfterAnimationFinished(
                     mHeadsUpExistenceChangedRunnable);
         }
+        updateGestureExclusionRect();
     }
 
     public void setHeadsUpAnimatingAway(boolean headsUpAnimatingAway) {
@@ -2992,6 +3015,7 @@
     @Override
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         super.dump(fd, pw, args);
+        pw.println("    gestureExclusionRect: " + calculateGestureExclusionRect());
         if (mKeyguardStatusBar != null) {
             mKeyguardStatusBar.dump(fd, pw, args);
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/assist/AssistHandleBehaviorControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/assist/AssistHandleBehaviorControllerTest.java
index 13c92b6..18f114a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/assist/AssistHandleBehaviorControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/assist/AssistHandleBehaviorControllerTest.java
@@ -18,21 +18,27 @@
 
 import static org.mockito.AdditionalAnswers.answerVoid;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
 
+import android.content.ComponentName;
 import android.os.Handler;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper.RunWithLooper;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.app.AssistUtils;
 import com.android.systemui.ScreenDecorations;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.recents.OverviewProxyService;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -46,29 +52,35 @@
 @RunWithLooper
 public class AssistHandleBehaviorControllerTest extends SysuiTestCase {
 
-    private final AssistHandleBehavior mTestBehavior = AssistHandleBehavior.TEST;
+    private static final ComponentName COMPONENT_NAME = new ComponentName("", "");
 
     private AssistHandleBehaviorController mAssistHandleBehaviorController;
 
     @Mock private ScreenDecorations mMockScreenDecorations;
+    @Mock private AssistUtils mMockAssistUtils;
     @Mock private Handler mMockHandler;
     @Mock private AssistHandleBehaviorController.BehaviorController mMockBehaviorController;
 
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
+        mDependency.injectMockDependency(StatusBarStateController.class);
+        mDependency.injectMockDependency(OverviewProxyService.class);
         doAnswer(answerVoid(Runnable::run)).when(mMockHandler).post(any(Runnable.class));
         doAnswer(answerVoid(Runnable::run)).when(mMockHandler)
                 .postDelayed(any(Runnable.class), anyLong());
-        mTestBehavior.setTestController(mMockBehaviorController);
         mAssistHandleBehaviorController =
                 new AssistHandleBehaviorController(
-                        mContext, mMockHandler, () -> mMockScreenDecorations);
+                        mContext,
+                        mMockAssistUtils,
+                        mMockHandler, () -> mMockScreenDecorations,
+                        mMockBehaviorController);
     }
 
     @Test
     public void hide_hidesHandlesWhenShowing() {
         // Arrange
+        when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
         mAssistHandleBehaviorController.showAndStay();
         reset(mMockScreenDecorations);
 
@@ -83,6 +95,7 @@
     @Test
     public void hide_doesNothingWhenHiding() {
         // Arrange
+        when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
         mAssistHandleBehaviorController.hide();
         reset(mMockScreenDecorations);
 
@@ -96,6 +109,7 @@
     @Test
     public void showAndStay_showsHandlesWhenHiding() {
         // Arrange
+        when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
         mAssistHandleBehaviorController.hide();
         reset(mMockScreenDecorations);
 
@@ -110,6 +124,7 @@
     @Test
     public void showAndStay_doesNothingWhenShowing() {
         // Arrange
+        when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
         mAssistHandleBehaviorController.showAndStay();
         reset(mMockScreenDecorations);
 
@@ -121,8 +136,23 @@
     }
 
     @Test
+    public void showAndStay_doesNothingWhenThereIsNoAssistant() {
+        // Arrange
+        when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(null);
+        mAssistHandleBehaviorController.hide();
+        reset(mMockScreenDecorations);
+
+        // Act
+        mAssistHandleBehaviorController.showAndStay();
+
+        // Assert
+        verifyNoMoreInteractions(mMockScreenDecorations);
+    }
+
+    @Test
     public void showAndGo_showsThenHidesHandlesWhenHiding() {
         // Arrange
+        when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
         mAssistHandleBehaviorController.hide();
         reset(mMockScreenDecorations);
 
@@ -139,6 +169,7 @@
     @Test
     public void showAndGo_hidesHandlesAfterTimeoutWhenShowing() {
         // Arrange
+        when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
         mAssistHandleBehaviorController.showAndStay();
         reset(mMockScreenDecorations);
 
@@ -153,6 +184,7 @@
     @Test
     public void showAndGo_doesNothingIfRecentlyHidden() {
         // Arrange
+        when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
         mAssistHandleBehaviorController.showAndGo();
         reset(mMockScreenDecorations);
 
@@ -164,12 +196,27 @@
     }
 
     @Test
+    public void showAndGo_doesNothingWhenThereIsNoAssistant() {
+        // Arrange
+        when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(null);
+        mAssistHandleBehaviorController.hide();
+        reset(mMockScreenDecorations);
+
+        // Act
+        mAssistHandleBehaviorController.showAndGo();
+
+        // Assert
+        verifyNoMoreInteractions(mMockScreenDecorations);
+    }
+
+    @Test
     public void setBehavior_activatesTheBehaviorWhenInGesturalMode() {
         // Arrange
+        when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
         mAssistHandleBehaviorController.setInGesturalModeForTest(true);
 
         // Act
-        mAssistHandleBehaviorController.setBehavior(mTestBehavior);
+        mAssistHandleBehaviorController.setBehavior(AssistHandleBehavior.TEST);
 
         // Assert
         verify(mMockBehaviorController).onModeActivated(mContext, mAssistHandleBehaviorController);
@@ -179,8 +226,10 @@
     @Test
     public void setBehavior_deactivatesThePreviousBehaviorWhenInGesturalMode() {
         // Arrange
-        mAssistHandleBehaviorController.setBehavior(mTestBehavior);
+        when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
+        mAssistHandleBehaviorController.setBehavior(AssistHandleBehavior.TEST);
         mAssistHandleBehaviorController.setInGesturalModeForTest(true);
+        reset(mMockBehaviorController);
 
         // Act
         mAssistHandleBehaviorController.setBehavior(AssistHandleBehavior.OFF);
@@ -193,10 +242,11 @@
     @Test
     public void setBehavior_doesNothingWhenNotInGesturalMode() {
         // Arrange
+        when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
         mAssistHandleBehaviorController.setInGesturalModeForTest(false);
 
         // Act
-        mAssistHandleBehaviorController.setBehavior(mTestBehavior);
+        mAssistHandleBehaviorController.setBehavior(AssistHandleBehavior.TEST);
 
         // Assert
         verifyNoMoreInteractions(mMockBehaviorController);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java
index 756cf3e..b324235 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java
@@ -60,8 +60,8 @@
     @Before
     public void setUp() throws Exception {
         super.setUp();
-        addOneMoreThanRenderLimitBubbles();
-        mLayout.setController(mExpandedController);
+        addOneMoreThanBubbleLimitBubbles();
+        mLayout.setActiveController(mExpandedController);
 
         Resources res = mLayout.getResources();
         mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
@@ -73,14 +73,14 @@
     @Test
     public void testExpansionAndCollapse() throws InterruptedException {
         Runnable afterExpand = Mockito.mock(Runnable.class);
-        mExpandedController.expandFromStack(mExpansionPoint, afterExpand);
+        mExpandedController.expandFromStack(afterExpand);
         waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
 
         testBubblesInCorrectExpandedPositions();
         verify(afterExpand).run();
 
         Runnable afterCollapse = Mockito.mock(Runnable.class);
-        mExpandedController.collapseBackToStack(afterCollapse);
+        mExpandedController.collapseBackToStack(mExpansionPoint, afterCollapse);
         waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
 
         testStackedAtPosition(mExpansionPoint.x, mExpansionPoint.y, -1);
@@ -139,7 +139,6 @@
         assertEquals(500f, draggedBubble.getTranslationY(), 1f);
 
         // Snap it back and make sure it made it back correctly.
-        mExpandedController.prepareForDismissalWithVelocity(draggedBubble, 0f, 0f);
         mLayout.removeView(draggedBubble);
         waitForLayoutMessageQueue();
         waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
@@ -169,7 +168,7 @@
 
         // Dismiss the now-magneted bubble, verify that the callback was called.
         final Runnable afterDismiss = Mockito.mock(Runnable.class);
-        mExpandedController.dismissDraggedOutBubble(afterDismiss);
+        mExpandedController.dismissDraggedOutBubble(draggedOutView, afterDismiss);
         waitForPropertyAnimations(DynamicAnimation.ALPHA);
         verify(after).run();
 
@@ -224,7 +223,7 @@
 
     /** Expand the stack and wait for animations to finish. */
     private void expand() throws InterruptedException {
-        mExpandedController.expandFromStack(mExpansionPoint, Mockito.mock(Runnable.class));
+        mExpandedController.expandFromStack(Mockito.mock(Runnable.class));
         waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
     }
 
@@ -236,26 +235,19 @@
             assertEquals(x + i * offsetMultiplier * mStackOffset,
                     mLayout.getChildAt(i).getTranslationX(), 2f);
             assertEquals(y, mLayout.getChildAt(i).getTranslationY(), 2f);
-
-            if (i < mMaxRenderedBubbles) {
-                assertEquals(1f, mLayout.getChildAt(i).getAlpha(), .01f);
-            }
+            assertEquals(1f, mLayout.getChildAt(i).getAlpha(), .01f);
         }
     }
 
     /** Check that children are in the correct positions for being expanded. */
     private void testBubblesInCorrectExpandedPositions() {
         // Check all the visible bubbles to see if they're in the right place.
-        for (int i = 0; i < Math.min(mLayout.getChildCount(), mMaxRenderedBubbles); i++) {
+        for (int i = 0; i < mLayout.getChildCount(); i++) {
             assertEquals(getBubbleLeft(i),
                     mLayout.getChildAt(i).getTranslationX(),
                     2f);
             assertEquals(mExpandedController.getExpandedY(),
                     mLayout.getChildAt(i).getTranslationY(), 2f);
-
-            if (i < mMaxRenderedBubbles) {
-                assertEquals(1f, mLayout.getChildAt(i).getAlpha(), .01f);
-            }
         }
     }
 
@@ -273,9 +265,7 @@
             return 0;
         }
         int bubbleCount = mLayout.getChildCount();
-        if (bubbleCount > mMaxRenderedBubbles) {
-            bubbleCount = mMaxRenderedBubbles;
-        }
+
         // Width calculations.
         double bubble = bubbleCount * mBubbleSize;
         float gap = (bubbleCount - 1) * mBubblePadding;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTest.java
index eef6ddc..f8b32c2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTest.java
@@ -23,7 +23,6 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
@@ -77,21 +76,9 @@
     }
 
     @Test
-    public void testRenderVisibility() throws InterruptedException {
-        mLayout.setController(mTestableController);
-        addOneMoreThanRenderLimitBubbles();
-
-        // The last child should be GONE, the rest VISIBLE.
-        for (int i = 0; i < mMaxRenderedBubbles + 1; i++) {
-            assertEquals(i == mMaxRenderedBubbles ? View.GONE : View.VISIBLE,
-                    mLayout.getChildAt(i).getVisibility());
-        }
-    }
-
-    @Test
     public void testHierarchyChanges() throws InterruptedException {
-        mLayout.setController(mTestableController);
-        addOneMoreThanRenderLimitBubbles();
+        mLayout.setActiveController(mTestableController);
+        addOneMoreThanBubbleLimitBubbles();
 
         // Make sure the controller was notified of all the views we added.
         for (View mView : mViews) {
@@ -115,8 +102,8 @@
 
     @Test
     public void testUpdateValueNotChained() throws InterruptedException {
-        mLayout.setController(mTestableController);
-        addOneMoreThanRenderLimitBubbles();
+        mLayout.setActiveController(mTestableController);
+        addOneMoreThanBubbleLimitBubbles();
 
         // Don't chain any values.
         mTestableController.setChainedProperties(Sets.newHashSet());
@@ -146,8 +133,8 @@
 
     @Test
     public void testSetEndActions() throws InterruptedException {
-        mLayout.setController(mTestableController);
-        addOneMoreThanRenderLimitBubbles();
+        mLayout.setActiveController(mTestableController);
+        addOneMoreThanBubbleLimitBubbles();
         mTestableController.setChainedProperties(Sets.newHashSet());
 
         final CountDownLatch xLatch = new CountDownLatch(1);
@@ -189,8 +176,8 @@
 
     @Test
     public void testRemoveEndListeners() throws InterruptedException {
-        mLayout.setController(mTestableController);
-        addOneMoreThanRenderLimitBubbles();
+        mLayout.setActiveController(mTestableController);
+        addOneMoreThanBubbleLimitBubbles();
         mTestableController.setChainedProperties(Sets.newHashSet());
 
         final CountDownLatch xLatch = new CountDownLatch(1);
@@ -229,8 +216,8 @@
     public void testSetController() throws InterruptedException {
         // Add the bubbles, then set the controller, to make sure that a controller added to an
         // already-initialized view works correctly.
-        addOneMoreThanRenderLimitBubbles();
-        mLayout.setController(mTestableController);
+        addOneMoreThanBubbleLimitBubbles();
+        mLayout.setActiveController(mTestableController);
         testChainedTranslationAnimations();
 
         TestableAnimationController secondController =
@@ -243,7 +230,7 @@
                 DynamicAnimation.SCALE_X, 10f);
         secondController.setRemoveImmediately(true);
 
-        mLayout.setController(secondController);
+        mLayout.setActiveController(secondController);
         mTestableController.animationForChildAtIndex(0)
                 .scaleX(1.5f)
                 .start();
@@ -266,7 +253,7 @@
         Mockito.verify(secondController, Mockito.atLeastOnce())
                 .getOffsetForChainedPropertyAnimation(eq(DynamicAnimation.SCALE_X));
 
-        mLayout.setController(mTestableController);
+        mLayout.setActiveController(mTestableController);
         mTestableController.animationForChildAtIndex(0)
                 .translationX(100f)
                 .start();
@@ -283,8 +270,8 @@
 
     @Test
     public void testArePropertiesAnimating() throws InterruptedException {
-        mLayout.setController(mTestableController);
-        addOneMoreThanRenderLimitBubbles();
+        mLayout.setActiveController(mTestableController);
+        addOneMoreThanBubbleLimitBubbles();
 
         assertFalse(mLayout.arePropertiesAnimating(
                 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y));
@@ -307,8 +294,8 @@
 
     @Test
     public void testCancelAllAnimations() throws InterruptedException {
-        mLayout.setController(mTestableController);
-        addOneMoreThanRenderLimitBubbles();
+        mLayout.setActiveController(mTestableController);
+        addOneMoreThanBubbleLimitBubbles();
 
         mTestableController.animationForChildAtIndex(0)
                 .position(1000, 1000)
@@ -321,29 +308,10 @@
         assertTrue(mViews.get(0).getTranslationY() < 1000);
     }
 
-    @Test
-    public void testSetChildVisibility() throws InterruptedException {
-        mLayout.setController(mTestableController);
-        addOneMoreThanRenderLimitBubbles();
-
-        // The last view should have been set to GONE by the controller, since we added one more
-        // than the limit and it got pushed off. None of the first children should have been set
-        // VISIBLE, since they would have been animated in by onChildAdded.
-        Mockito.verify(mTestableController).setChildVisibility(
-                mViews.get(mViews.size() - 1), 5, View.GONE);
-        Mockito.verify(mTestableController, never()).setChildVisibility(
-                any(View.class), anyInt(), eq(View.VISIBLE));
-
-        // Remove the first view, which should cause the last view to become visible again.
-        mLayout.removeView(mViews.get(0));
-        Mockito.verify(mTestableController).setChildVisibility(
-                mViews.get(mViews.size() - 1), 4, View.VISIBLE);
-    }
-
     /** Standard test of chained translation animations. */
     private void testChainedTranslationAnimations() throws InterruptedException {
-        mLayout.setController(mTestableController);
-        addOneMoreThanRenderLimitBubbles();
+        mLayout.setActiveController(mTestableController);
+        addOneMoreThanBubbleLimitBubbles();
 
         assertEquals(0, mLayout.getChildAt(0).getTranslationX(), .1f);
         assertEquals(0, mLayout.getChildAt(1).getTranslationX(), .1f);
@@ -354,11 +322,7 @@
 
         waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X);
 
-        // Since we enabled chaining, animating the first view to 100 should animate the second to
-        // 115 (since we set the offset to 15) and the third to 130, etc. Despite the sixth bubble
-        // not being visible, or animated, make sure that it has the appropriate chained
-        // translation.
-        for (int i = 0; i < mMaxRenderedBubbles + 1; i++) {
+        for (int i = 0; i < mLayout.getChildCount(); i++) {
             assertEquals(
                     100 + i * TEST_TRANSLATION_X_OFFSET,
                     mLayout.getChildAt(i).getTranslationX(), .1f);
@@ -383,8 +347,8 @@
 
     @Test
     public void testPhysicsAnimator() throws InterruptedException {
-        mLayout.setController(mTestableController);
-        addOneMoreThanRenderLimitBubbles();
+        mLayout.setActiveController(mTestableController);
+        addOneMoreThanBubbleLimitBubbles();
 
         Runnable afterAll = Mockito.mock(Runnable.class);
         Runnable after = Mockito.spy(new Runnable() {
@@ -430,9 +394,9 @@
         // Don't chain since we're going to invoke each animation independently.
         mTestableController.setChainedProperties(new HashSet<>());
 
-        mLayout.setController(mTestableController);
+        mLayout.setActiveController(mTestableController);
 
-        addOneMoreThanRenderLimitBubbles();
+        addOneMoreThanBubbleLimitBubbles();
 
         Runnable allEnd = Mockito.mock(Runnable.class);
 
@@ -452,7 +416,7 @@
 
     @Test
     public void testAnimationsForChildrenFromIndex_noChildren() {
-        mLayout.setController(mTestableController);
+        mLayout.setActiveController(mTestableController);
 
         final Runnable after = Mockito.mock(Runnable.class);
         mTestableController
@@ -523,8 +487,9 @@
         }
 
         @Override
-        protected void setChildVisibility(View child, int index, int visibility) {
-            super.setChildVisibility(child, index, visibility);
-        }
+        void onChildReordered(View child, int oldIndex, int newIndex) {}
+
+        @Override
+        void onActiveControllerForLayout(PhysicsAnimationLayout layout) {}
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTestCase.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTestCase.java
index c6acef5d..f633f39 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTestCase.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTestCase.java
@@ -56,7 +56,6 @@
 
     Handler mMainThreadHandler;
 
-    int mMaxRenderedBubbles;
     int mSystemWindowInsetSize = 50;
     int mCutoutInsetSize = 100;
 
@@ -69,6 +68,8 @@
     @Mock
     private DisplayCutout mCutout;
 
+    private int mMaxBubbles;
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
@@ -79,7 +80,7 @@
         mLayout.setTop(0);
         mLayout.setBottom(mHeight);
 
-        mMaxRenderedBubbles =
+        mMaxBubbles =
                 getContext().getResources().getInteger(R.integer.bubbles_max_rendered);
         mMainThreadHandler = new Handler(Looper.getMainLooper());
 
@@ -96,8 +97,8 @@
     }
 
     /** Add one extra bubble over the limit, so we can make sure it's gone/chains appropriately. */
-    void addOneMoreThanRenderLimitBubbles() throws InterruptedException {
-        for (int i = 0; i < mMaxRenderedBubbles + 1; i++) {
+    void addOneMoreThanBubbleLimitBubbles() throws InterruptedException {
+        for (int i = 0; i < mMaxBubbles + 1; i++) {
             final View newView = new FrameLayout(mContext);
             mLayout.addView(newView, 0);
             mViews.add(0, newView);
@@ -138,6 +139,13 @@
         }
 
         @Override
+        protected boolean isActiveController(PhysicsAnimationController controller) {
+            // Return true since otherwise all test controllers will be seen as inactive since they
+            // are wrapped by MainThreadAnimationControllerWrapper.
+            return true;
+        }
+
+        @Override
         public boolean post(Runnable action) {
             return mMainThreadHandler.post(action);
         }
@@ -148,9 +156,9 @@
         }
 
         @Override
-        public void setController(PhysicsAnimationController controller) {
+        public void setActiveController(PhysicsAnimationController controller) {
             runOnMainThreadAndBlock(
-                    () -> super.setController(
+                    () -> super.setActiveController(
                             new MainThreadAnimationControllerWrapper(controller)));
         }
 
@@ -267,8 +275,15 @@
             }
 
             @Override
-            protected void setChildVisibility(View child, int index, int visibility) {
-                mWrappedController.setChildVisibility(child, index, visibility);
+            void onChildReordered(View child, int oldIndex, int newIndex) {
+                runOnMainThreadAndBlock(
+                        () -> mWrappedController.onChildReordered(child, oldIndex, newIndex));
+            }
+
+            @Override
+            void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
+                runOnMainThreadAndBlock(
+                        () -> mWrappedController.onActiveControllerForLayout(layout));
             }
 
             @Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java
index 9218a8b..31a7d5a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java
@@ -54,8 +54,8 @@
     @Before
     public void setUp() throws Exception {
         super.setUp();
-        mLayout.setController(mStackController);
-        addOneMoreThanRenderLimitBubbles();
+        mLayout.setActiveController(mStackController);
+        addOneMoreThanBubbleLimitBubbles();
         mStackOffset = mLayout.getResources().getDimensionPixelSize(R.dimen.bubble_stack_offset);
     }
 
diff --git a/packages/overlays/NavigationBarModeGesturalOverlay/res/values/dimens.xml b/packages/overlays/NavigationBarModeGesturalOverlay/res/values/dimens.xml
index 987d203..1232201 100644
--- a/packages/overlays/NavigationBarModeGesturalOverlay/res/values/dimens.xml
+++ b/packages/overlays/NavigationBarModeGesturalOverlay/res/values/dimens.xml
@@ -25,4 +25,6 @@
     <dimen name="navigation_bar_width">16dp</dimen>
     <!-- Height of the bottom navigation / system bar. -->
     <dimen name="navigation_bar_frame_height">48dp</dimen>
+        <!-- The height of the bottom navigation gesture area. -->
+    <dimen name="navigation_bar_gesture_height">32dp</dimen>
 </resources>
\ No newline at end of file
diff --git a/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java b/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java
index 430abf5..10ba9a5 100644
--- a/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java
+++ b/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java
@@ -28,13 +28,17 @@
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ParceledListSlice;
 import android.content.pm.ServiceInfo;
+import android.os.IBinder;
 import android.os.RemoteException;
 import android.service.appprediction.AppPredictionService;
+import android.util.ArrayMap;
 import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.server.infra.AbstractPerUserSystemService;
 
+import java.util.ArrayList;
+
 /**
  * Per-user instance of {@link AppPredictionManagerService}.
  */
@@ -48,6 +52,17 @@
     @GuardedBy("mLock")
     private RemoteAppPredictionService mRemoteService;
 
+    /**
+     * When {@code true}, remote service died but service state is kept so it's restored after
+     * the system re-binds to it.
+     */
+    @GuardedBy("mLock")
+    private boolean mZombie;
+
+    @GuardedBy("mLock")
+    private final ArrayMap<AppPredictionSessionId, AppPredictionSessionInfo> mSessionInfos =
+            new ArrayMap<>();
+
     protected AppPredictionPerUserService(AppPredictionManagerService master,
             Object lock, int userId) {
         super(master, lock, userId);
@@ -92,6 +107,16 @@
         final RemoteAppPredictionService service = getRemoteServiceLocked();
         if (service != null) {
             service.onCreatePredictionSession(context, sessionId);
+
+            mSessionInfos.put(sessionId, new AppPredictionSessionInfo(sessionId, context, () -> {
+                synchronized (mLock) {
+                    AppPredictionSessionInfo sessionInfo = mSessionInfos.get(sessionId);
+                    if (sessionInfo != null) {
+                        sessionInfo.removeAllCallbacksLocked();
+                        mSessionInfos.remove(sessionId);
+                    }
+                }
+            }));
         }
     }
 
@@ -140,6 +165,11 @@
         final RemoteAppPredictionService service = getRemoteServiceLocked();
         if (service != null) {
             service.registerPredictionUpdates(sessionId, callback);
+
+            AppPredictionSessionInfo sessionInfo = mSessionInfos.get(sessionId);
+            if (sessionInfo != null) {
+                sessionInfo.addCallbackLocked(callback);
+            }
         }
     }
 
@@ -152,6 +182,11 @@
         final RemoteAppPredictionService service = getRemoteServiceLocked();
         if (service != null) {
             service.unregisterPredictionUpdates(sessionId, callback);
+
+            AppPredictionSessionInfo sessionInfo = mSessionInfos.get(sessionId);
+            if (sessionInfo != null) {
+                sessionInfo.removeCallbackLocked(callback);
+            }
         }
     }
 
@@ -174,6 +209,12 @@
         final RemoteAppPredictionService service = getRemoteServiceLocked();
         if (service != null) {
             service.onDestroyPredictionSession(sessionId);
+
+            AppPredictionSessionInfo sessionInfo = mSessionInfos.get(sessionId);
+            if (sessionInfo != null) {
+                sessionInfo.removeAllCallbacksLocked();
+                mSessionInfos.remove(sessionId);
+            }
         }
     }
 
@@ -182,17 +223,54 @@
         if (isDebug()) {
             Slog.d(TAG, "onFailureOrTimeout(): timed out=" + timedOut);
         }
-
         // Do nothing, we are just proxying to the prediction service
     }
 
     @Override
+    public void onConnectedStateChanged(boolean connected) {
+        if (isDebug()) {
+            Slog.d(TAG, "onConnectedStateChanged(): connected=" + connected);
+        }
+        if (connected) {
+            synchronized (mLock) {
+                if (mZombie) {
+                    // Sanity check - shouldn't happen
+                    if (mRemoteService == null) {
+                        Slog.w(TAG, "Cannot resurrect sessions because remote service is null");
+                        return;
+                    }
+                    mZombie = false;
+                    resurrectSessionsLocked();
+                }
+            }
+        }
+    }
+
+    @Override
     public void onServiceDied(RemoteAppPredictionService service) {
         if (isDebug()) {
-            Slog.d(TAG, "onServiceDied():");
+            Slog.w(TAG, "onServiceDied(): service=" + service);
+        }
+        synchronized (mLock) {
+            mZombie = true;
+        }
+        // Do nothing, eventually the system will bind to the remote service again...
+    }
+
+    /**
+     * Called after the remote service connected, it's used to restore state from a 'zombie'
+     * service (i.e., after it died).
+     */
+    private void resurrectSessionsLocked() {
+        final int numSessions = mSessionInfos.size();
+        if (isDebug()) {
+            Slog.d(TAG, "Resurrecting remote service (" + mRemoteService + ") on "
+                    + numSessions + " sessions.");
         }
 
-        // Do nothing, we are just proxying to the prediction service
+        for (AppPredictionSessionInfo sessionInfo : mSessionInfos.values()) {
+            sessionInfo.resurrectSessionLocked(this);
+        }
     }
 
     @GuardedBy("mLock")
@@ -215,4 +293,57 @@
 
         return mRemoteService;
     }
+
+    private static final class AppPredictionSessionInfo {
+        private final AppPredictionSessionId mSessionId;
+        private final AppPredictionContext mContext;
+        private final ArrayList<IPredictionCallback> mCallbacks = new ArrayList<>();
+        private final IBinder.DeathRecipient mBinderDeathHandler;
+
+        AppPredictionSessionInfo(AppPredictionSessionId id, AppPredictionContext context,
+                IBinder.DeathRecipient binderDeathHandler) {
+            mSessionId = id;
+            mContext = context;
+            mBinderDeathHandler = binderDeathHandler;
+        }
+
+        void addCallbackLocked(IPredictionCallback callback) {
+            if (mBinderDeathHandler != null) {
+                try {
+                    callback.asBinder().linkToDeath(mBinderDeathHandler, 0);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Failed to link to death: " + e);
+                }
+            }
+            mCallbacks.add(callback);
+        }
+
+        void removeCallbackLocked(IPredictionCallback callback) {
+            if (mBinderDeathHandler != null) {
+                callback.asBinder().unlinkToDeath(mBinderDeathHandler, 0);
+            }
+            mCallbacks.remove(callback);
+        }
+
+        void removeAllCallbacksLocked() {
+            if (mBinderDeathHandler != null) {
+                for (IPredictionCallback callback : mCallbacks) {
+                    callback.asBinder().unlinkToDeath(mBinderDeathHandler, 0);
+                }
+            }
+            mCallbacks.clear();
+        }
+
+        void resurrectSessionLocked(AppPredictionPerUserService service) {
+            if (service.isDebug()) {
+                Slog.d(TAG, "Resurrecting remote service (" + service.getRemoteServiceLocked()
+                        + ") for session Id=" + mSessionId + " and "
+                        + mCallbacks.size() + " callbacks.");
+            }
+            service.onCreatePredictionSessionLocked(mContext, mSessionId);
+            for (IPredictionCallback callback : mCallbacks) {
+                service.registerPredictionUpdatesLocked(mSessionId, callback);
+            }
+        }
+    }
 }
diff --git a/services/appprediction/java/com/android/server/appprediction/RemoteAppPredictionService.java b/services/appprediction/java/com/android/server/appprediction/RemoteAppPredictionService.java
index 19226be..c82e7a0 100644
--- a/services/appprediction/java/com/android/server/appprediction/RemoteAppPredictionService.java
+++ b/services/appprediction/java/com/android/server/appprediction/RemoteAppPredictionService.java
@@ -42,6 +42,8 @@
 
     private static final long TIMEOUT_REMOTE_REQUEST_MILLIS = 2 * DateUtils.SECOND_IN_MILLIS;
 
+    private final RemoteAppPredictionServiceCallbacks mCallback;
+
     public RemoteAppPredictionService(Context context, String serviceInterface,
             ComponentName componentName, int userId,
             RemoteAppPredictionServiceCallbacks callback, boolean bindInstantServiceAllowed,
@@ -50,6 +52,7 @@
                 context.getMainThreadHandler(),
                 bindInstantServiceAllowed ? Context.BIND_ALLOW_INSTANT : 0,
                 verbose, /* initialCapacity= */ 1);
+        mCallback = callback;
     }
 
     @Override
@@ -141,5 +144,17 @@
          * Notifies a the failure or timeout of a remote call.
          */
         void onFailureOrTimeout(boolean timedOut);
+
+        /**
+         * Notifies change in connected state of the remote service.
+         */
+        void onConnectedStateChanged(boolean connected);
+    }
+
+    @Override // from AbstractRemoteService
+    protected void handleOnConnectedStateChanged(boolean connected) {
+        if (mCallback != null) {
+            mCallback.onConnectedStateChanged(connected);
+        }
     }
 }
diff --git a/services/core/java/com/android/server/LocationManagerService.java b/services/core/java/com/android/server/LocationManagerService.java
index d52fe81..ae04f76 100644
--- a/services/core/java/com/android/server/LocationManagerService.java
+++ b/services/core/java/com/android/server/LocationManagerService.java
@@ -82,6 +82,7 @@
 import android.os.WorkSource;
 import android.os.WorkSource.WorkChain;
 import android.provider.Settings;
+import android.stats.location.LocationStatsEnums;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -269,10 +270,14 @@
     @PowerManager.LocationPowerSaveMode
     private int mBatterySaverMode;
 
+    @GuardedBy("mLock")
+    private final LocationUsageLogger mLocationUsageLogger;
+
     public LocationManagerService(Context context) {
         super();
         mContext = context;
         mHandler = FgThread.getHandler();
+        mLocationUsageLogger = new LocationUsageLogger();
 
         // Let the package manager query which are the default location
         // providers as they get certain permissions granted by default.
@@ -2346,7 +2351,18 @@
          * Method to be called when a record will no longer be used.
          */
         private void disposeLocked(boolean removeReceiver) {
-            mRequestStatistics.stopRequesting(mReceiver.mCallerIdentity.mPackageName, mProvider);
+            String packageName = mReceiver.mCallerIdentity.mPackageName;
+            mRequestStatistics.stopRequesting(packageName, mProvider);
+
+            mLocationUsageLogger.logLocationApiUsage(
+                    LocationStatsEnums.USAGE_ENDED,
+                    LocationStatsEnums.API_REQUEST_LOCATION_UPDATES,
+                    packageName,
+                    mRealRequest,
+                    mReceiver.isListener(),
+                    mReceiver.isPendingIntent(),
+                    /* radius= */ 0,
+                    mActivityManager.getPackageImportance(packageName));
 
             // remove from mRecordsByProvider
             ArrayList<UpdateRecord> globalRecords = mRecordsByProvider.get(this.mProvider);
@@ -2521,6 +2537,13 @@
                             "cannot register both listener and intent");
                 }
 
+                mLocationUsageLogger.logLocationApiUsage(
+                        LocationStatsEnums.USAGE_STARTED,
+                        LocationStatsEnums.API_REQUEST_LOCATION_UPDATES,
+                        packageName, request, listener != null, intent != null,
+                        /* radius= */ 0,
+                        mActivityManager.getPackageImportance(packageName));
+
                 Receiver receiver;
                 if (intent != null) {
                     receiver = getReceiverLocked(intent, pid, uid, packageName, workSource,
@@ -2813,6 +2836,18 @@
         }
         long identity = Binder.clearCallingIdentity();
         try {
+            synchronized (mLock) {
+                mLocationUsageLogger.logLocationApiUsage(
+                        LocationStatsEnums.USAGE_STARTED,
+                        LocationStatsEnums.API_REQUEST_GEOFENCE,
+                        packageName,
+                        request,
+                        /* hasListener= */ false,
+                        intent != null,
+                        geofence.getRadius(),
+                        mActivityManager.getPackageImportance(packageName));
+            }
+
             mGeofenceManager.addFence(sanitizedRequest, geofence, intent,
                     allowedResolutionLevel,
                     uid, packageName);
@@ -2833,6 +2868,17 @@
         // geo-fence manager uses the public location API, need to clear identity
         long identity = Binder.clearCallingIdentity();
         try {
+            synchronized (mLock) {
+                mLocationUsageLogger.logLocationApiUsage(
+                        LocationStatsEnums.USAGE_ENDED,
+                        LocationStatsEnums.API_REQUEST_GEOFENCE,
+                        packageName,
+                        /* LocationRequest= */ null,
+                        /* hasListener= */ false,
+                        intent != null,
+                        geofence.getRadius(),
+                        mActivityManager.getPackageImportance(packageName));
+            }
             mGeofenceManager.removeFence(geofence, intent);
         } finally {
             Binder.restoreCallingIdentity(identity);
@@ -2916,6 +2962,20 @@
             gnssDataListeners.put(binder, linkedListener);
             long identity = Binder.clearCallingIdentity();
             try {
+                if (gnssDataProvider == mGnssNavigationMessageProvider
+                        || gnssDataProvider == mGnssStatusProvider) {
+                    mLocationUsageLogger.logLocationApiUsage(
+                            LocationStatsEnums.USAGE_STARTED,
+                            gnssDataProvider == mGnssNavigationMessageProvider
+                                ? LocationStatsEnums.API_ADD_GNSS_MEASUREMENTS_LISTENER
+                                : LocationStatsEnums.API_REGISTER_GNSS_STATUS_CALLBACK,
+                            packageName,
+                            /* LocationRequest= */ null,
+                            /* hasListener= */ true,
+                            /* hasIntent= */ false,
+                            /* radius */ 0,
+                            mActivityManager.getPackageImportance(packageName));
+                }
                 if (isThrottlingExemptLocked(callerIdentity)
                         || isImportanceForeground(
                         mActivityManager.getPackageImportance(packageName))) {
@@ -2941,6 +3001,26 @@
             if (linkedListener == null) {
                 return;
             }
+            long identity = Binder.clearCallingIdentity();
+            try {
+                if (gnssDataProvider == mGnssNavigationMessageProvider
+                        || gnssDataProvider == mGnssStatusProvider) {
+                    mLocationUsageLogger.logLocationApiUsage(
+                            LocationStatsEnums.USAGE_ENDED,
+                            gnssDataProvider == mGnssNavigationMessageProvider
+                                ? LocationStatsEnums.API_ADD_GNSS_MEASUREMENTS_LISTENER
+                                : LocationStatsEnums.API_REGISTER_GNSS_STATUS_CALLBACK,
+                            linkedListener.mCallerIdentity.mPackageName,
+                            /* LocationRequest= */ null,
+                            /* hasListener= */ true,
+                            /* hasIntent= */ false,
+                            /* radius= */ 0,
+                            mActivityManager.getPackageImportance(
+                                    linkedListener.mCallerIdentity.mPackageName));
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
             unlinkFromListenerDeathNotificationLocked(binder, linkedListener);
             gnssDataProvider.removeListener(listener);
         }
@@ -3026,6 +3106,11 @@
             checkResolutionLevelIsSufficientForProviderUseLocked(getCallerAllowedResolutionLevel(),
                     providerName);
 
+            mLocationUsageLogger.logLocationApiUsage(
+                    LocationStatsEnums.USAGE_STARTED,
+                    LocationStatsEnums.API_SEND_EXTRA_COMMAND,
+                    providerName);
+
             // and check for ACCESS_LOCATION_EXTRA_COMMANDS
             if ((mContext.checkCallingOrSelfPermission(ACCESS_LOCATION_EXTRA_COMMANDS)
                     != PERMISSION_GRANTED)) {
@@ -3037,6 +3122,11 @@
                 provider.sendExtraCommandLocked(command, extras);
             }
 
+            mLocationUsageLogger.logLocationApiUsage(
+                    LocationStatsEnums.USAGE_ENDED,
+                    LocationStatsEnums.API_SEND_EXTRA_COMMAND,
+                    providerName);
+
             return true;
         }
     }
diff --git a/services/core/java/com/android/server/LocationUsageLogger.java b/services/core/java/com/android/server/LocationUsageLogger.java
new file mode 100644
index 0000000..c503035
--- /dev/null
+++ b/services/core/java/com/android/server/LocationUsageLogger.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import android.app.ActivityManager;
+import android.location.LocationManager;
+import android.location.LocationRequest;
+import android.os.SystemClock;
+import android.stats.location.LocationStatsEnums;
+import android.util.Log;
+import android.util.StatsLog;
+
+import java.time.Instant;
+
+/**
+ * Logger for Location API usage logging.
+ */
+class LocationUsageLogger {
+    private static final String TAG = "LocationUsageLogger";
+    private static final boolean D = Log.isLoggable(TAG, Log.DEBUG);
+
+    private static final int ONE_SEC_IN_MILLIS = 1000;
+    private static final int ONE_MINUTE_IN_MILLIS = 60000;
+    private static final int ONE_HOUR_IN_MILLIS = 3600000;
+
+    private long mLastApiUsageLogHour = 0;
+
+    private int mApiUsageLogHourlyCount = 0;
+
+    private static final int API_USAGE_LOG_HOURLY_CAP = 60;
+
+    private static int providerNameToStatsdEnum(String provider) {
+        if (LocationManager.NETWORK_PROVIDER.equals(provider)) {
+            return LocationStatsEnums.PROVIDER_NETWORK;
+        } else if (LocationManager.GPS_PROVIDER.equals(provider)) {
+            return LocationStatsEnums.PROVIDER_GPS;
+        } else if (LocationManager.PASSIVE_PROVIDER.equals(provider)) {
+            return LocationStatsEnums.PROVIDER_PASSIVE;
+        } else if (LocationManager.FUSED_PROVIDER.equals(provider)) {
+            return LocationStatsEnums.PROVIDER_FUSED;
+        } else {
+            return LocationStatsEnums.PROVIDER_UNKNOWN;
+        }
+    }
+
+    private static int bucketizeIntervalToStatsdEnum(long interval) {
+        // LocationManager already converts negative values to 0.
+        if (interval < ONE_SEC_IN_MILLIS) {
+            return LocationStatsEnums.INTERVAL_BETWEEN_0_SEC_AND_1_SEC;
+        } else if (interval < ONE_SEC_IN_MILLIS * 5) {
+            return LocationStatsEnums.INTERVAL_BETWEEN_1_SEC_AND_5_SEC;
+        } else if (interval < ONE_MINUTE_IN_MILLIS) {
+            return LocationStatsEnums.INTERVAL_BETWEEN_5_SEC_AND_1_MIN;
+        } else if (interval < ONE_MINUTE_IN_MILLIS * 10) {
+            return LocationStatsEnums.INTERVAL_BETWEEN_1_MIN_AND_10_MIN;
+        } else if (interval < ONE_HOUR_IN_MILLIS) {
+            return LocationStatsEnums.INTERVAL_BETWEEN_10_MIN_AND_1_HOUR;
+        } else {
+            return LocationStatsEnums.INTERVAL_LARGER_THAN_1_HOUR;
+        }
+    }
+
+    private static int bucketizeSmallestDisplacementToStatsdEnum(float smallestDisplacement) {
+        // LocationManager already converts negative values to 0.
+        if (smallestDisplacement == 0) {
+            return LocationStatsEnums.DISTANCE_ZERO;
+        } else if (smallestDisplacement > 0 && smallestDisplacement <= 100) {
+            return LocationStatsEnums.DISTANCE_BETWEEN_0_AND_100;
+        } else {
+            return LocationStatsEnums.DISTANCE_LARGER_THAN_100;
+        }
+    }
+
+    private static int bucketizeRadiusToStatsdEnum(float radius) {
+        if (radius < 0) {
+            return LocationStatsEnums.RADIUS_NEGATIVE;
+        } else if (radius < 100) {
+            return LocationStatsEnums.RADIUS_BETWEEN_0_AND_100;
+        } else if (radius < 200) {
+            return LocationStatsEnums.RADIUS_BETWEEN_100_AND_200;
+        } else if (radius < 300) {
+            return LocationStatsEnums.RADIUS_BETWEEN_200_AND_300;
+        } else if (radius < 1000) {
+            return LocationStatsEnums.RADIUS_BETWEEN_300_AND_1000;
+        } else if (radius < 10000) {
+            return LocationStatsEnums.RADIUS_BETWEEN_1000_AND_10000;
+        } else {
+            return LocationStatsEnums.RADIUS_LARGER_THAN_100000;
+        }
+    }
+
+    private static int getBucketizedExpireIn(long expireAt) {
+        if (expireAt == Long.MAX_VALUE) {
+            return LocationStatsEnums.EXPIRATION_NO_EXPIRY;
+        }
+
+        long elapsedRealtime = SystemClock.elapsedRealtime();
+        long expireIn = Math.max(0, expireAt - elapsedRealtime);
+
+        if (expireIn < 20 * ONE_SEC_IN_MILLIS) {
+            return LocationStatsEnums.EXPIRATION_BETWEEN_0_AND_20_SEC;
+        } else if (expireIn < ONE_MINUTE_IN_MILLIS) {
+            return LocationStatsEnums.EXPIRATION_BETWEEN_20_SEC_AND_1_MIN;
+        } else if (expireIn < ONE_MINUTE_IN_MILLIS * 10) {
+            return LocationStatsEnums.EXPIRATION_BETWEEN_1_MIN_AND_10_MIN;
+        } else if (expireIn < ONE_HOUR_IN_MILLIS) {
+            return LocationStatsEnums.EXPIRATION_BETWEEN_10_MIN_AND_1_HOUR;
+        } else {
+            return LocationStatsEnums.EXPIRATION_LARGER_THAN_1_HOUR;
+        }
+    }
+
+    private static int categorizeActivityImportance(int importance) {
+        if (importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
+            return LocationStatsEnums.IMPORTANCE_TOP;
+        } else if (importance == ActivityManager
+                                    .RunningAppProcessInfo
+                                    .IMPORTANCE_FOREGROUND_SERVICE) {
+            return LocationStatsEnums.IMPORTANCE_FORGROUND_SERVICE;
+        } else {
+            return LocationStatsEnums.IMPORTANCE_BACKGROUND;
+        }
+    }
+
+    private static int getCallbackType(
+            int apiType, boolean hasListener, boolean hasIntent) {
+        if (apiType == LocationStatsEnums.API_SEND_EXTRA_COMMAND) {
+            return LocationStatsEnums.CALLBACK_NOT_APPLICABLE;
+        }
+
+        // Listener and PendingIntent will not be set at
+        // the same time.
+        if (hasIntent) {
+            return LocationStatsEnums.CALLBACK_PENDING_INTENT;
+        } else if (hasListener) {
+            return LocationStatsEnums.CALLBACK_LISTENER;
+        } else {
+            return LocationStatsEnums.CALLBACK_UNKNOWN;
+        }
+    }
+
+    // Update the hourly count of APIUsage log event.
+    // Returns false if hit the hourly log cap.
+    private boolean checkApiUsageLogCap() {
+        if (D) {
+            Log.d(TAG, "checking APIUsage log cap.");
+        }
+
+        long currentHour = Instant.now().toEpochMilli() / ONE_HOUR_IN_MILLIS;
+        if (currentHour > mLastApiUsageLogHour) {
+            mLastApiUsageLogHour = currentHour;
+            mApiUsageLogHourlyCount = 0;
+            return true;
+        } else {
+            mApiUsageLogHourlyCount = Math.min(
+                mApiUsageLogHourlyCount + 1, API_USAGE_LOG_HOURLY_CAP);
+            return mApiUsageLogHourlyCount < API_USAGE_LOG_HOURLY_CAP;
+        }
+    }
+
+    /**
+     * Log a Location API usage event to Statsd.
+     * Logging event is capped at 60 per hour. Usage events exceeding
+     * the cap will be dropped by LocationUsageLogger.
+     */
+    public void logLocationApiUsage(int usageType, int apiInUse,
+            String packageName, LocationRequest locationRequest,
+            boolean hasListener, boolean hasIntent,
+            float radius, int activityImportance) {
+        try {
+            if (!checkApiUsageLogCap()) {
+                return;
+            }
+
+            boolean isLocationRequestNull = locationRequest == null;
+            if (D) {
+                Log.d(TAG, "log API Usage to statsd. usageType: " + usageType + ", apiInUse: "
+                        + apiInUse + ", packageName: " + (packageName == null ? "" : packageName)
+                        + ", locationRequest: "
+                        + (isLocationRequestNull ? "" : locationRequest.toString())
+                        + ", hasListener: " + hasListener
+                        + ", hasIntent: " + hasIntent
+                        + ", radius: " + radius
+                        + ", importance: " + activityImportance);
+            }
+
+            StatsLog.write(StatsLog.LOCATION_MANAGER_API_USAGE_REPORTED, usageType,
+                    apiInUse, packageName,
+                    isLocationRequestNull
+                        ? LocationStatsEnums.PROVIDER_UNKNOWN
+                        : providerNameToStatsdEnum(locationRequest.getProvider()),
+                    isLocationRequestNull
+                        ? LocationStatsEnums.QUALITY_UNKNOWN
+                        : locationRequest.getQuality(),
+                    isLocationRequestNull
+                        ? LocationStatsEnums.INTERVAL_UNKNOWN
+                        : bucketizeIntervalToStatsdEnum(locationRequest.getInterval()),
+                    isLocationRequestNull
+                        ? LocationStatsEnums.DISTANCE_UNKNOWN
+                        : bucketizeSmallestDisplacementToStatsdEnum(
+                                locationRequest.getSmallestDisplacement()),
+                    isLocationRequestNull ? 0 : locationRequest.getNumUpdates(),
+                    // only log expireIn for USAGE_STARTED
+                    isLocationRequestNull || usageType == LocationStatsEnums.USAGE_ENDED
+                        ? LocationStatsEnums.EXPIRATION_UNKNOWN
+                        : getBucketizedExpireIn(locationRequest.getExpireAt()),
+                    getCallbackType(apiInUse, hasListener, hasIntent),
+                    bucketizeRadiusToStatsdEnum(radius),
+                    categorizeActivityImportance(activityImportance));
+        } catch (Exception e) {
+            // Swallow exceptions to avoid crashing LMS.
+            Log.w(TAG, "Failed to log API usage to statsd.", e);
+        }
+    }
+
+    /**
+     * Log a Location API usage event to Statsd.
+     * Logging event is capped at 60 per hour. Usage events exceeding
+     * the cap will be dropped by LocationUsageLogger.
+     */
+    public void logLocationApiUsage(int usageType, int apiInUse, String providerName) {
+        try {
+            if (!checkApiUsageLogCap()) {
+                return;
+            }
+
+            if (D) {
+                Log.d(TAG, "log API Usage to statsd. usageType: " + usageType + ", apiInUse: "
+                        + apiInUse + ", providerName: " + providerName);
+            }
+
+            StatsLog.write(StatsLog.LOCATION_MANAGER_API_USAGE_REPORTED, usageType, apiInUse,
+                    /* package_name= */ null,
+                    providerNameToStatsdEnum(providerName),
+                    LocationStatsEnums.QUALITY_UNKNOWN,
+                    LocationStatsEnums.INTERVAL_UNKNOWN,
+                    LocationStatsEnums.DISTANCE_UNKNOWN,
+                    /* numUpdates= */ 0,
+                    LocationStatsEnums.EXPIRATION_UNKNOWN,
+                    getCallbackType(
+                            apiInUse,
+                            /* isListenerNull= */ true,
+                            /* isIntentNull= */ true),
+                    /* bucketizedRadius= */ 0,
+                    LocationStatsEnums.IMPORTANCE_UNKNOWN);
+        } catch (Exception e) {
+            // Swallow exceptions to avoid crashing LMS.
+            Log.w(TAG, "Failed to log API usage to statsd.", e);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 90266f1..fae853c 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -3250,6 +3250,7 @@
             int memFactor = mAm.mProcessStats.getMemFactorLocked();
             long now = SystemClock.uptimeMillis();
             r.tracker.setExecuting(false, memFactor, now);
+            r.tracker.setForeground(false, memFactor, now);
             r.tracker.setBound(false, memFactor, now);
             r.tracker.setStarted(false, memFactor, now);
         }
@@ -3293,8 +3294,10 @@
             }
             r.executeFg = false;
             if (r.tracker != null) {
-                r.tracker.setExecuting(false, mAm.mProcessStats.getMemFactorLocked(),
-                        SystemClock.uptimeMillis());
+                final int memFactor = mAm.mProcessStats.getMemFactorLocked();
+                final long now = SystemClock.uptimeMillis();
+                r.tracker.setExecuting(false, memFactor, now);
+                r.tracker.setForeground(false, memFactor, now);
                 if (finishing) {
                     r.tracker.clearCurrentOwner(r, false);
                     r.tracker = null;
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index a0522e3..fcd6a0a 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -241,15 +241,6 @@
         sendLMsgNoDelay(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT, SENDMSG_QUEUE, info);
     }
 
-    /*package*/ void postBluetoothA2dpDeviceConfigChangeExt(
-            @NonNull BluetoothDevice device,
-            @AudioService.BtProfileConnectionState int state, int profile,
-            boolean suppressNoisyIntent, int a2dpVolume) {
-        final BtDeviceConnectionInfo info = new BtDeviceConnectionInfo(device, state, profile,
-                suppressNoisyIntent, a2dpVolume);
-        sendLMsgNoDelay(MSG_L_A2DP_ACTIVE_DEVICE_CHANGE_EXT, SENDMSG_QUEUE, info);
-    }
-
     private static final class HearingAidDeviceConnectionInfo {
         final @NonNull BluetoothDevice mDevice;
         final @AudioService.BtProfileConnectionState int mState;
@@ -862,22 +853,6 @@
                                 info.mDevice, info.mState, info.mSupprNoisy, info.mMusicDevice);
                     }
                 } break;
-                case MSG_L_A2DP_ACTIVE_DEVICE_CHANGE_EXT: {
-                    final BtDeviceConnectionInfo info = (BtDeviceConnectionInfo) msg.obj;
-                    AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent(
-                            "handleBluetoothA2dpActiveDeviceChangeExt "
-                                    + " state=" + info.mState
-                                    // only querying address as this is the only readily available
-                                    // field on the device
-                                    + " addr=" + info.mDevice.getAddress()
-                                    + " prof=" + info.mProfile + " supprNoisy=" + info.mSupprNoisy
-                                    + " vol=" + info.mVolume)).printLog(TAG));
-                    synchronized (mDeviceStateLock) {
-                        mDeviceInventory.handleBluetoothA2dpActiveDeviceChangeExt(
-                                info.mDevice, info.mState, info.mProfile,
-                                info.mSupprNoisy, info.mVolume);
-                    }
-                } break;
                 default:
                     Log.wtf(TAG, "Invalid message " + msg.what);
             }
@@ -925,10 +900,8 @@
     private static final int MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT = 27;
     // process external command to (dis)connect a hearing aid device
     private static final int MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT = 28;
-    // process external command to (dis)connect or change active A2DP device
-    private static final int MSG_L_A2DP_ACTIVE_DEVICE_CHANGE_EXT = 29;
     // a ScoClient died in BtHelper
-    private static final int MSG_L_SCOCLIENT_DIED = 30;
+    private static final int MSG_L_SCOCLIENT_DIED = 29;
 
 
     private static boolean isMessageHandledUnderWakelock(int msgId) {
@@ -943,7 +916,6 @@
             case MSG_L_A2DP_ACTIVE_DEVICE_CHANGE:
             case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT:
             case MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT:
-            case MSG_L_A2DP_ACTIVE_DEVICE_CHANGE_EXT:
                 return true;
             default:
                 return false;
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 887c908..99b97cb 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -570,49 +570,6 @@
         }
     }
 
-    /*package*/ void handleBluetoothA2dpActiveDeviceChangeExt(
-            @NonNull BluetoothDevice device,
-            @AudioService.BtProfileConnectionState int state, int profile,
-            boolean suppressNoisyIntent, int a2dpVolume) {
-        if (state == BluetoothProfile.STATE_DISCONNECTED) {
-            mDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(
-                           device, state, profile, suppressNoisyIntent, a2dpVolume);
-            return;
-        }
-        // state == BluetoothProfile.STATE_CONNECTED
-        synchronized (mConnectedDevices) {
-            final String address = device.getAddress();
-            final int a2dpCodec = mDeviceBroker.getA2dpCodec(device);
-            final String deviceKey = DeviceInfo.makeDeviceListKey(
-                        AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address);
-            DeviceInfo deviceInfo = mConnectedDevices.get(deviceKey);
-            if (deviceInfo != null) {
-                // Device config change for matching A2DP device
-                mDeviceBroker.postBluetoothA2dpDeviceConfigChange(device);
-                return;
-            }
-            for (int i = 0; i < mConnectedDevices.size(); i++) {
-                deviceInfo = mConnectedDevices.valueAt(i);
-                if (deviceInfo.mDeviceType != AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP) {
-                    continue;
-                }
-                // A2DP device exists, handle active device change
-                final String existingDevicekey = mConnectedDevices.keyAt(i);
-                mConnectedDevices.remove(existingDevicekey);
-                mConnectedDevices.put(deviceKey, new DeviceInfo(
-                        AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, BtHelper.getName(device),
-                        address, a2dpCodec));
-                mDeviceBroker.postA2dpActiveDeviceChange(
-                        new BtHelper.BluetoothA2dpDeviceInfo(
-                            device, a2dpVolume, a2dpCodec));
-                return;
-            }
-        }
-        // New A2DP device connection
-        mDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(
-                           device, state, profile, suppressNoisyIntent, a2dpVolume);
-    }
-
     /*package*/ int setWiredDeviceConnectionState(int type, @AudioService.ConnectionState int state,
                                                   String address, String name, String caller) {
         synchronized (mConnectedDevices) {
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 15e8851..30035c8 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -4382,27 +4382,6 @@
         mDeviceBroker.postBluetoothA2dpDeviceConfigChange(device);
     }
 
-    /**
-     * @see AudioManager#handleBluetoothA2dpActiveDeviceChange(BluetoothDevice, int, int,
-     *                                                          boolean, int)
-     */
-    public void handleBluetoothA2dpActiveDeviceChange(
-            BluetoothDevice device, int state, int profile, boolean suppressNoisyIntent,
-            int a2dpVolume) {
-        if (device == null) {
-            throw new IllegalArgumentException("Illegal null device");
-        }
-        if (profile != BluetoothProfile.A2DP && profile != BluetoothProfile.A2DP_SINK) {
-            throw new IllegalArgumentException("invalid profile " + profile);
-        }
-        if (state != BluetoothProfile.STATE_CONNECTED
-                && state != BluetoothProfile.STATE_DISCONNECTED) {
-            throw new IllegalArgumentException("Invalid state " + state);
-        }
-        mDeviceBroker.postBluetoothA2dpDeviceConfigChangeExt(device, state, profile,
-                suppressNoisyIntent, a2dpVolume);
-    }
-
     private static final int DEVICE_MEDIA_UNMUTED_ON_PLUG =
             AudioSystem.DEVICE_OUT_WIRED_HEADSET | AudioSystem.DEVICE_OUT_WIRED_HEADPHONE |
             AudioSystem.DEVICE_OUT_LINE |
diff --git a/services/core/java/com/android/server/location/GnssVisibilityControl.java b/services/core/java/com/android/server/location/GnssVisibilityControl.java
index 8d4ad7f..65bd5c6 100644
--- a/services/core/java/com/android/server/location/GnssVisibilityControl.java
+++ b/services/core/java/com/android/server/location/GnssVisibilityControl.java
@@ -456,9 +456,8 @@
         final String proxyAppPkgName = nfwNotification.mProxyAppPackageName;
         final ProxyAppState proxyAppState = mProxyAppsState.get(proxyAppPkgName);
         final boolean isLocationRequestAccepted = nfwNotification.isRequestAccepted();
-        final boolean isPermissionMismatched =
-                (proxyAppState == null) ? isLocationRequestAccepted
-                        : (proxyAppState.mHasLocationPermission != isLocationRequestAccepted);
+        final boolean isPermissionMismatched = isPermissionMismatched(proxyAppState,
+                nfwNotification);
         logEvent(nfwNotification, isPermissionMismatched);
 
         if (!nfwNotification.isRequestAttributedToProxyApp()) {
@@ -506,14 +505,24 @@
 
         // Log proxy app permission mismatch between framework and GNSS HAL.
         if (isPermissionMismatched) {
-            Log.w(TAG, "Permission mismatch. Framework proxy app " + proxyAppPkgName
+            Log.w(TAG, "Permission mismatch. Proxy app " + proxyAppPkgName
                     + " location permission is set to " + proxyAppState.mHasLocationPermission
+                    + " and GNSS HAL enabled is set to " + mIsGpsEnabled
                     + " but GNSS non-framework location access response type is "
                     + nfwNotification.getResponseTypeAsString() + " for notification: "
                     + nfwNotification);
         }
     }
 
+    private boolean isPermissionMismatched(ProxyAppState proxyAppState,
+            NfwNotification nfwNotification) {
+        // Non-framework non-emergency location requests must be accepted only when IGnss.hal
+        // is enabled and the proxy app has location permission.
+        final boolean isLocationRequestAccepted = nfwNotification.isRequestAccepted();
+        return (proxyAppState == null || !mIsGpsEnabled) ? isLocationRequestAccepted
+                        : (proxyAppState.mHasLocationPermission != isLocationRequestAccepted);
+    }
+
     private void showLocationIcon(ProxyAppState proxyAppState, NfwNotification nfwNotification,
             int uid, String proxyAppPkgName) {
         // If we receive a new NfwNotification before the location icon is turned off for the
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index d35f952..2f86632 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -15032,24 +15032,26 @@
 
         void tryProcessInstallRequest(InstallArgs args, int currentStatus) {
             mCurrentState.put(args, currentStatus);
-            boolean success = true;
             if (mCurrentState.size() != mChildParams.size()) {
                 return;
             }
+            int completeStatus = PackageManager.INSTALL_SUCCEEDED;
             for (Integer status : mCurrentState.values()) {
                 if (status == PackageManager.INSTALL_UNKNOWN) {
                     return;
                 } else if (status != PackageManager.INSTALL_SUCCEEDED) {
-                    success = false;
+                    completeStatus = status;
                     break;
                 }
             }
             final List<InstallRequest> installRequests = new ArrayList<>(mCurrentState.size());
             for (Map.Entry<InstallArgs, Integer> entry : mCurrentState.entrySet()) {
                 installRequests.add(new InstallRequest(entry.getKey(),
-                        createPackageInstalledInfo(entry.getValue())));
+                        createPackageInstalledInfo(completeStatus)));
             }
-            processInstallRequestsAsync(success, installRequests);
+            processInstallRequestsAsync(
+                    completeStatus == PackageManager.INSTALL_SUCCEEDED,
+                    installRequests);
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/AppWindowToken.java b/services/core/java/com/android/server/wm/AppWindowToken.java
index 3d7e50d..cae7612 100644
--- a/services/core/java/com/android/server/wm/AppWindowToken.java
+++ b/services/core/java/com/android/server/wm/AppWindowToken.java
@@ -1733,17 +1733,13 @@
             return;
         }
 
-        if (mThumbnail == null && getTask() != null) {
-            final TaskSnapshotController snapshotCtrl = mWmService.mTaskSnapshotController;
-            final ArraySet<Task> tasks = new ArraySet<>();
-            tasks.add(getTask());
-            snapshotCtrl.snapshotTasks(tasks);
-            snapshotCtrl.addSkipClosingAppSnapshotTasks(tasks);
-            final ActivityManager.TaskSnapshot snapshot = snapshotCtrl.getSnapshot(
-                    getTask().mTaskId, getTask().mUserId, false /* restoreFromDisk */,
-                    false /* reducedResolution */);
+        Task task = getTask();
+        if (mThumbnail == null && task != null && !hasCommittedReparentToAnimationLeash()) {
+            SurfaceControl.ScreenshotGraphicBuffer snapshot =
+                    mWmService.mTaskSnapshotController.createTaskSnapshot(
+                            task, 1 /* scaleFraction */);
             if (snapshot != null) {
-                mThumbnail = new AppWindowThumbnail(t, this, snapshot.getSnapshot(),
+                mThumbnail = new AppWindowThumbnail(t, this, snapshot.getGraphicBuffer(),
                         true /* relative */);
             }
         }
@@ -2858,7 +2854,7 @@
             }
         }
         t.hide(mTransitChangeLeash);
-        t.reparent(mTransitChangeLeash, null);
+        t.remove(mTransitChangeLeash);
         mTransitChangeLeash = null;
         if (cancel) {
             onAnimationLeashLost(t);
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index e1dd352..21f01ff 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -2720,9 +2720,9 @@
                 res.getBoolean(R.bool.config_navBarAlwaysShowOnSideEdgeGesture);
 
         // This should calculate how much above the frame we accept gestures.
-        mBottomGestureAdditionalInset = Math.max(0,
+        mBottomGestureAdditionalInset =
                 res.getDimensionPixelSize(R.dimen.navigation_bar_gesture_height)
-                        - getNavigationBarFrameHeight(portraitRotation, uiMode));
+                        - getNavigationBarFrameHeight(portraitRotation, uiMode);
 
         updateConfigurationAndScreenSizeDependentBehaviors();
         mWindowOutsetBottom = ScreenShapeHelper.getWindowOutsetBottomPx(mContext.getResources());
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java
index 432ca33..1815218 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotController.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java
@@ -21,6 +21,7 @@
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.ActivityManager.TaskSnapshot;
@@ -241,6 +242,28 @@
         return null;
     }
 
+    @Nullable
+    SurfaceControl.ScreenshotGraphicBuffer createTaskSnapshot(@NonNull Task task,
+            float scaleFraction) {
+        if (task.getSurfaceControl() == null) {
+            if (DEBUG_SCREENSHOT) {
+                Slog.w(TAG_WM, "Failed to take screenshot. No surface control for " + task);
+            }
+            return null;
+        }
+        task.getBounds(mTmpRect);
+        mTmpRect.offsetTo(0, 0);
+        final SurfaceControl.ScreenshotGraphicBuffer screenshotBuffer =
+                SurfaceControl.captureLayers(
+                        task.getSurfaceControl().getHandle(), mTmpRect, scaleFraction);
+        final GraphicBuffer buffer = screenshotBuffer != null ? screenshotBuffer.getGraphicBuffer()
+                : null;
+        if (buffer == null || buffer.getWidth() <= 1 || buffer.getHeight() <= 1) {
+            return null;
+        }
+        return screenshotBuffer;
+    }
+
     @Nullable private TaskSnapshot snapshotTask(Task task) {
         if (!mService.mPolicy.isScreenOn()) {
             if (DEBUG_SCREENSHOT) {
@@ -248,12 +271,6 @@
             }
             return null;
         }
-        if (task.getSurfaceControl() == null) {
-            if (DEBUG_SCREENSHOT) {
-                Slog.w(TAG_WM, "Failed to take screenshot. No surface control for " + task);
-            }
-            return null;
-        }
 
         final AppWindowToken appWindowToken = findAppTokenForSnapshot(task);
         if (appWindowToken == null) {
@@ -271,8 +288,6 @@
 
         final boolean isLowRamDevice = ActivityManager.isLowRamDeviceStatic();
         final float scaleFraction = isLowRamDevice ? mPersister.getReducedScale() : 1f;
-        task.getBounds(mTmpRect);
-        mTmpRect.offsetTo(0, 0);
 
         final WindowState mainWindow = appWindowToken.findMainWindow();
         if (mainWindow == null) {
@@ -280,18 +295,17 @@
             return null;
         }
         final SurfaceControl.ScreenshotGraphicBuffer screenshotBuffer =
-                SurfaceControl.captureLayers(
-                        task.getSurfaceControl().getHandle(), mTmpRect, scaleFraction);
-        final GraphicBuffer buffer = screenshotBuffer != null ? screenshotBuffer.getGraphicBuffer()
-                : null;
-        if (buffer == null || buffer.getWidth() <= 1 || buffer.getHeight() <= 1) {
+                createTaskSnapshot(task, scaleFraction);
+
+        if (screenshotBuffer == null) {
             if (DEBUG_SCREENSHOT) {
                 Slog.w(TAG_WM, "Failed to take screenshot for " + task);
             }
             return null;
         }
         final boolean isWindowTranslucent = mainWindow.getAttrs().format != PixelFormat.OPAQUE;
-        return new TaskSnapshot(appWindowToken.mActivityComponent, buffer,
+        return new TaskSnapshot(
+                appWindowToken.mActivityComponent, screenshotBuffer.getGraphicBuffer(),
                 screenshotBuffer.getColorSpace(), appWindowToken.getConfiguration().orientation,
                 getInsets(mainWindow), isLowRamDevice /* reduced */, scaleFraction /* scale */,
                 true /* isRealSnapshot */, task.getWindowingMode(), getSystemUiVisibility(task),
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 5175c1d..a125e31 100755
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -1973,26 +1973,6 @@
     public static final String KEY_CARRIER_WIFI_STRING_ARRAY = "carrier_wifi_string_array";
 
     /**
-     * Base64 Encoding method the carrier will use for encoding encrypted IMSI and SSID.
-     * The value set as below:
-     * 2045 - RFC2045 (default value)
-     * 4648 - RFC4648
-     *
-     * @hide
-     */
-    public static final String KEY_IMSI_ENCODING_METHOD_INT = "imsi_encoding_method_int";
-
-    /**
-     * Defines the sequence of sending an encrypted IMSI identity for EAP-SIM/AKA authentication.
-     * The value set as below:
-     * 1 - encrypted IMSI as EAP-RESPONSE/IDENTITY (default one).
-     * 2 - anonymous as EAP-RESPONSE/IDENTITY -> encrypted IMSI as EAP-RESPONSE/AKA|SIM-IDENTITY.
-     *
-     * @hide
-     */
-    public static final String KEY_EAP_IDENTITY_SEQUENCE_INT = "imsi_eap_identity_sequence_int";
-
-    /**
      * Time delay (in ms) after which we show the notification to switch the preferred
      * network.
      * @hide
@@ -3265,8 +3245,6 @@
         sDefaults.putBoolean(KEY_STK_DISABLE_LAUNCH_BROWSER_BOOL, false);
         sDefaults.putBoolean(KEY_ALLOW_METERED_NETWORK_FOR_CERT_DOWNLOAD_BOOL, false);
         sDefaults.putStringArray(KEY_CARRIER_WIFI_STRING_ARRAY, null);
-        sDefaults.putInt(KEY_IMSI_ENCODING_METHOD_INT, 2045);
-        sDefaults.putInt(KEY_EAP_IDENTITY_SEQUENCE_INT, 1);
         sDefaults.putInt(KEY_PREF_NETWORK_NOTIFICATION_DELAY_INT, -1);
         sDefaults.putInt(KEY_EMERGENCY_NOTIFICATION_DELAY_INT, -1);
         sDefaults.putBoolean(KEY_ALLOW_USSD_REQUESTS_VIA_TELEPHONY_MANAGER_BOOL, true);
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index addd9e0..e010d28 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -775,6 +775,14 @@
     public static final int PROFILE_CLASS_DEFAULT = PROFILE_CLASS_UNSET;
 
     /**
+     * TelephonyProvider column name IMSI (International Mobile Subscriber Identity).
+     * <P>Type: TEXT </P>
+     * @hide
+     */
+    //TODO: add @SystemApi
+    public static final String IMSI = "imsi";
+
+    /**
      * Broadcast Action: The user has changed one of the default subs related to
      * data, phone calls, or sms</p>
      *
diff --git a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
index 9771637..232b5cb 100644
--- a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
+++ b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
@@ -23,11 +23,20 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.Manifest;
 import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
 import android.content.pm.VersionedPackage;
 import android.net.NetworkStackClient;
+import android.net.NetworkStackClient.NetworkStackHealthListener;
 import android.os.Handler;
 import android.os.test.TestLooper;
 import android.provider.DeviceConfig;
@@ -42,6 +51,8 @@
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -73,8 +84,13 @@
     private static final long SHORT_DURATION = TimeUnit.SECONDS.toMillis(1);
     private static final long LONG_DURATION = TimeUnit.SECONDS.toMillis(5);
     private TestLooper mTestLooper;
+    private Context mSpyContext;
     @Mock
-    private NetworkStackClient mNetworkStackClient;
+    private NetworkStackClient mMockNetworkStackClient;
+    @Mock
+    private PackageManager mMockPackageManager;
+    @Captor
+    private ArgumentCaptor<NetworkStackHealthListener> mNetworkStackCallbackCaptor;
 
     @Before
     public void setUp() throws Exception {
@@ -83,6 +99,14 @@
                 "package-watchdog.xml").delete();
         adoptShellPermissions(Manifest.permission.READ_DEVICE_CONFIG);
         mTestLooper = new TestLooper();
+        mSpyContext = spy(InstrumentationRegistry.getContext());
+        when(mSpyContext.getPackageManager()).thenReturn(mMockPackageManager);
+        when(mMockPackageManager.getPackageInfo(anyString(), anyInt())).then(inv -> {
+            final PackageInfo res = new PackageInfo();
+            res.packageName = inv.getArgument(0);
+            res.setLongVersionCode(VERSION_CODE);
+            return res;
+        });
     }
 
     @After
@@ -702,6 +726,26 @@
         assertEquals(MonitoredPackage.STATE_PASSED, m4.handleElapsedTimeLocked(LONG_DURATION));
     }
 
+    @Test
+    public void testNetworkStackFailure() {
+        final PackageWatchdog wd = createWatchdog();
+
+        // Start observing with failure handling
+        TestObserver observer = new TestObserver(OBSERVER_NAME_1,
+                PackageHealthObserverImpact.USER_IMPACT_HIGH);
+        wd.startObservingHealth(observer, Collections.singletonList(APP_A), SHORT_DURATION);
+
+        // Notify of NetworkStack failure
+        mNetworkStackCallbackCaptor.getValue().onNetworkStackFailure(APP_A);
+
+        // Run handler so package failures are dispatched to observers
+        mTestLooper.dispatchAll();
+
+        // Verify the NetworkStack observer is notified
+        assertEquals(1, observer.mFailedPackages.size());
+        assertEquals(APP_A, observer.mFailedPackages.get(0));
+    }
+
     private void adoptShellPermissions(String... permissions) {
         InstrumentationRegistry
                 .getInstrumentation()
@@ -733,19 +777,23 @@
     }
 
     private PackageWatchdog createWatchdog(TestController controller, boolean withPackagesReady) {
-        Context context = InstrumentationRegistry.getContext();
         AtomicFile policyFile =
-                new AtomicFile(new File(context.getFilesDir(), "package-watchdog.xml"));
+                new AtomicFile(new File(mSpyContext.getFilesDir(), "package-watchdog.xml"));
         Handler handler = new Handler(mTestLooper.getLooper());
         PackageWatchdog watchdog =
-                new PackageWatchdog(context, policyFile, handler, handler, controller,
-                        mNetworkStackClient);
+                new PackageWatchdog(mSpyContext, policyFile, handler, handler, controller,
+                        mMockNetworkStackClient);
         // Verify controller is not automatically started
         assertFalse(controller.mIsEnabled);
         if (withPackagesReady) {
+            // Only capture the NetworkStack callback for the latest registered watchdog
+            reset(mMockNetworkStackClient);
             watchdog.onPackagesReady();
             // Verify controller by default is started when packages are ready
             assertTrue(controller.mIsEnabled);
+
+            verify(mMockNetworkStackClient).registerHealthListener(
+                    mNetworkStackCallbackCaptor.capture());
         }
         return watchdog;
     }