Merge "Make media buttons shrink when tapped" into rvc-dev
diff --git a/apex/Android.bp b/apex/Android.bp
index e8afa1d..f511af5 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -92,6 +92,9 @@
         sdk_version: "module_current",
     },
 
+    // Configure framework module specific metalava options.
+    droiddoc_options: [mainline_stubs_args],
+
     // The stub libraries must be visible to frameworks/base so they can be combined
     // into API specific libraries.
     stubs_library_visibility: [
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index a1a11ed..16e5156 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -226,9 +226,10 @@
      * <p>
      * This intent should be launched using
      * {@link Activity#startActivityForResult(Intent, int)} so that the user
-     * knows which app is requesting to clear cache. The returned result will
-     * be {@link Activity#RESULT_OK} if the activity was launched and the user accepted to clear
-     * cache, or {@link Activity#RESULT_CANCELED} otherwise.
+     * knows which app is requesting to clear cache. The returned result will be:
+     * {@link Activity#RESULT_OK} if the activity was launched and all cache was cleared,
+     * {@link OsConstants#EIO} if an error occurred while clearing the cache or
+     * {@link Activity#RESULT_CANCELED} otherwise.
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_EXTERNAL_STORAGE)
     @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
diff --git a/core/java/com/android/internal/BrightnessSynchronizer.java b/core/java/com/android/internal/BrightnessSynchronizer.java
index aa23251..42724be 100644
--- a/core/java/com/android/internal/BrightnessSynchronizer.java
+++ b/core/java/com/android/internal/BrightnessSynchronizer.java
@@ -84,17 +84,17 @@
      * Converts between the int brightness system and the float brightness system.
      */
     public static float brightnessIntToFloat(Context context, int brightnessInt) {
-        PowerManager pm = context.getSystemService(PowerManager.class);
-        float pmMinBrightness = pm.getBrightnessConstraint(
+        final PowerManager pm = context.getSystemService(PowerManager.class);
+        final float pmMinBrightness = pm.getBrightnessConstraint(
                 PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MINIMUM);
-        float pmMaxBrightness = pm.getBrightnessConstraint(
+        final float pmMaxBrightness = pm.getBrightnessConstraint(
                 PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MAXIMUM);
-        int minBrightnessInt = brightnessFloatToInt(pmMinBrightness, PowerManager.BRIGHTNESS_MIN,
-                PowerManager.BRIGHTNESS_MAX, PowerManager.BRIGHTNESS_OFF + 1,
-                PowerManager.BRIGHTNESS_ON);
-        int maxBrightnessInt = brightnessFloatToInt(pmMaxBrightness, PowerManager.BRIGHTNESS_MIN,
-                PowerManager.BRIGHTNESS_MAX, PowerManager.BRIGHTNESS_OFF + 1,
-                PowerManager.BRIGHTNESS_ON);
+        final int minBrightnessInt = Math.round(brightnessFloatToIntRange(pmMinBrightness,
+                PowerManager.BRIGHTNESS_MIN, PowerManager.BRIGHTNESS_MAX,
+                PowerManager.BRIGHTNESS_OFF + 1, PowerManager.BRIGHTNESS_ON));
+        final int maxBrightnessInt = Math.round(brightnessFloatToIntRange(pmMaxBrightness,
+                PowerManager.BRIGHTNESS_MIN, PowerManager.BRIGHTNESS_MAX,
+                PowerManager.BRIGHTNESS_OFF + 1, PowerManager.BRIGHTNESS_ON));
 
         return brightnessIntToFloat(brightnessInt, minBrightnessInt, maxBrightnessInt,
                 pmMinBrightness, pmMaxBrightness);
@@ -119,34 +119,43 @@
      * Converts between the float brightness system and the int brightness system.
      */
     public static int brightnessFloatToInt(Context context, float brightnessFloat) {
-        PowerManager pm = context.getSystemService(PowerManager.class);
-        float pmMinBrightness = pm.getBrightnessConstraint(
-                PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MINIMUM);
-        float pmMaxBrightness = pm.getBrightnessConstraint(
-                PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MAXIMUM);
-        int minBrightnessInt = brightnessFloatToInt(pmMinBrightness, PowerManager.BRIGHTNESS_MIN,
-                PowerManager.BRIGHTNESS_MAX, PowerManager.BRIGHTNESS_OFF + 1,
-                PowerManager.BRIGHTNESS_ON);
-        int maxBrightnessInt = brightnessFloatToInt(pmMaxBrightness, PowerManager.BRIGHTNESS_MIN,
-                PowerManager.BRIGHTNESS_MAX, PowerManager.BRIGHTNESS_OFF + 1,
-                PowerManager.BRIGHTNESS_ON);
-
-        return brightnessFloatToInt(brightnessFloat, pmMinBrightness, pmMaxBrightness,
-                minBrightnessInt, maxBrightnessInt);
+        return Math.round(brightnessFloatToIntRange(context, brightnessFloat));
     }
 
     /**
-     * Converts between the float brightness system and the int brightness system.
+     * Converts between the float brightness system and the int brightness system, but returns
+     * the converted value as a float within the int-system's range. This method helps with
+     * conversions from one system to the other without losing the floating-point precision.
      */
-    public static int brightnessFloatToInt(float brightnessFloat, float minFloat, float maxFloat,
-            int minInt, int maxInt) {
+    public static float brightnessFloatToIntRange(Context context, float brightnessFloat) {
+        final PowerManager pm = context.getSystemService(PowerManager.class);
+        final float minFloat = pm.getBrightnessConstraint(
+                PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MINIMUM);
+        final float maxFloat = pm.getBrightnessConstraint(
+                PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MAXIMUM);
+        final float minInt = brightnessFloatToIntRange(minFloat,
+                PowerManager.BRIGHTNESS_MIN, PowerManager.BRIGHTNESS_MAX,
+                PowerManager.BRIGHTNESS_OFF + 1, PowerManager.BRIGHTNESS_ON);
+        final float maxInt = brightnessFloatToIntRange(maxFloat,
+                PowerManager.BRIGHTNESS_MIN, PowerManager.BRIGHTNESS_MAX,
+                PowerManager.BRIGHTNESS_OFF + 1, PowerManager.BRIGHTNESS_ON);
+        return brightnessFloatToIntRange(brightnessFloat, minFloat, maxFloat, minInt, maxInt);
+    }
+
+    /**
+     * Translates specified value from the float brightness system to the int brightness system,
+     * given the min/max of each range.  Accounts for special values such as OFF and invalid values.
+     * Value returned as a float privimite (to preserve precision), but is a value within the
+     * int-system range.
+     */
+    private static float brightnessFloatToIntRange(float brightnessFloat, float minFloat,
+            float maxFloat, float minInt, float maxInt) {
         if (floatEquals(brightnessFloat, PowerManager.BRIGHTNESS_OFF_FLOAT)) {
             return PowerManager.BRIGHTNESS_OFF;
         } else if (Float.isNaN(brightnessFloat)) {
             return PowerManager.BRIGHTNESS_INVALID;
         } else {
-            return Math.round(MathUtils.constrainedMap((float) minInt, (float) maxInt, minFloat,
-                    maxFloat, brightnessFloat));
+            return MathUtils.constrainedMap(minInt, maxInt, minFloat, maxFloat, brightnessFloat);
         }
     }
 
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index cff669e..9950e55 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -3506,13 +3506,11 @@
         }
 
         /**
-         * Only expand direct share area if there is a minimum number of shortcuts,
-         * which will help reduce the amount of visible shuffling due to older-style
-         * direct share targets.
+         * Only expand direct share area if there is a minimum number of targets.
          */
         private boolean canExpandDirectShare() {
             int orientation = getResources().getConfiguration().orientation;
-            return mChooserListAdapter.getNumShortcutResults() > getMaxTargetsPerRow()
+            return mChooserListAdapter.getNumServiceTargetsForExpand() > getMaxTargetsPerRow()
                     && orientation == Configuration.ORIENTATION_PORTRAIT
                     && !isInMultiWindowMode();
         }
@@ -3721,8 +3719,12 @@
 
                 // only expand if we have more than maxTargetsPerRow, and delay that decision
                 // until they start to scroll
-                if (mChooserMultiProfilePagerAdapter.getActiveListAdapter()
-                        .getSelectableServiceTargetCount() <= maxTargetsPerRow) {
+                ChooserListAdapter adapter =
+                        mChooserMultiProfilePagerAdapter.getActiveListAdapter();
+                int validTargets =
+                        mAppendDirectShareEnabled ? adapter.getNumServiceTargetsForExpand()
+                                : adapter.getSelectableServiceTargetCount();
+                if (validTargets <= maxTargetsPerRow) {
                     mHideDirectShareExpansion = true;
                     return;
                 }
diff --git a/core/java/com/android/internal/app/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java
index 492f98c..2568d09 100644
--- a/core/java/com/android/internal/app/ChooserListAdapter.java
+++ b/core/java/com/android/internal/app/ChooserListAdapter.java
@@ -79,6 +79,7 @@
 
     private static final int MAX_SUGGESTED_APP_TARGETS = 4;
     private static final int MAX_CHOOSER_TARGETS_PER_APP = 2;
+    private static final int MAX_SERVICE_TARGET_APP = 8;
 
     static final int MAX_SERVICE_TARGETS = 8;
 
@@ -98,6 +99,7 @@
     private ChooserTargetInfo
             mPlaceHolderTargetInfo = new ChooserActivity.PlaceHolderTargetInfo();
     private int mValidServiceTargetsNum = 0;
+    private int mAvailableServiceTargetsNum = 0;
     private final Map<ComponentName, Pair<List<ChooserTargetInfo>, Integer>>
             mParkingDirectShareTargets = new HashMap<>();
     private final Map<ComponentName, Map<String, Integer>> mChooserTargetScores = new HashMap<>();
@@ -603,7 +605,13 @@
             Pair<List<ChooserTargetInfo>, Integer> parkingTargetInfoPair =
                     mParkingDirectShareTargets.getOrDefault(origComponentName,
                             new Pair<>(new ArrayList<>(), 0));
-            parkingTargetInfoPair.first.addAll(parkingTargetInfos);
+            for (ChooserTargetInfo target : parkingTargetInfos) {
+                if (!checkDuplicateTarget(target, parkingTargetInfoPair.first)
+                        && !checkDuplicateTarget(target, mServiceTargets)) {
+                    parkingTargetInfoPair.first.add(target);
+                    mAvailableServiceTargetsNum++;
+                }
+            }
             mParkingDirectShareTargets.put(origComponentName, parkingTargetInfoPair);
             rankTargetsWithinComponent(origComponentName);
             if (isShortcutResult) {
@@ -648,7 +656,7 @@
             List<ChooserTargetInfo> parkingTargets = parkingTargetsItem.first;
             int insertedNum = parkingTargetsItem.second;
             while (insertedNum < quota && !parkingTargets.isEmpty()) {
-                if (!checkDuplicateTarget(parkingTargets.get(0))) {
+                if (!checkDuplicateTarget(parkingTargets.get(0), mServiceTargets)) {
                     mServiceTargets.add(mValidServiceTargetsNum, parkingTargets.get(0));
                     mValidServiceTargetsNum++;
                     insertedNum++;
@@ -663,9 +671,6 @@
                         + " totalScore=" + totalScore
                         + " quota=" + quota);
             }
-            if (mShortcutComponents.contains(component)) {
-                mNumShortcutResults += insertedNum - parkingTargetsItem.second;
-            }
             mParkingDirectShareTargets.put(component, new Pair<>(parkingTargets, insertedNum));
         }
         if (!shouldWaitPendingService) {
@@ -681,19 +686,15 @@
             return;
         }
         Log.i(TAG, " fillAllServiceTargets");
-        int maxRankedTargets = mChooserListCommunicator.getMaxRankedTargets();
-        List<ComponentName> topComponentNames = getTopComponentNames(maxRankedTargets);
+        List<ComponentName> topComponentNames = getTopComponentNames(MAX_SERVICE_TARGET_APP);
         // Append all remaining targets of top recommended components into direct share row.
         for (ComponentName component : topComponentNames) {
             if (!mParkingDirectShareTargets.containsKey(component)) {
                 continue;
             }
             mParkingDirectShareTargets.get(component).first.stream()
-                    .filter(target -> !checkDuplicateTarget(target))
+                    .filter(target -> !checkDuplicateTarget(target, mServiceTargets))
                     .forEach(target -> {
-                        if (mShortcutComponents.contains(component)) {
-                            mNumShortcutResults++;
-                        }
                         mServiceTargets.add(mValidServiceTargetsNum, target);
                         mValidServiceTargetsNum++;
                     });
@@ -706,28 +707,34 @@
                 .map(pair -> pair.first)
                 .forEach(targets -> {
                     for (ChooserTargetInfo target : targets) {
-                        if (!checkDuplicateTarget(target)) {
+                        if (!checkDuplicateTarget(target, mServiceTargets)) {
                             mServiceTargets.add(mValidServiceTargetsNum, target);
                             mValidServiceTargetsNum++;
-                            mNumShortcutResults++;
                         }
                     }
                 });
         mParkingDirectShareTargets.clear();
     }
 
-    private boolean checkDuplicateTarget(ChooserTargetInfo chooserTargetInfo) {
+    private boolean checkDuplicateTarget(ChooserTargetInfo target,
+            List<ChooserTargetInfo> destination) {
         // Check for duplicates and abort if found
-        for (ChooserTargetInfo otherTargetInfo : mServiceTargets) {
-            if (chooserTargetInfo.isSimilar(otherTargetInfo)) {
+        for (ChooserTargetInfo otherTargetInfo : destination) {
+            if (target.isSimilar(otherTargetInfo)) {
                 return true;
             }
         }
         return false;
     }
 
-    int getNumShortcutResults() {
-        return mNumShortcutResults;
+    /**
+     * The return number have to exceed a minimum limit to make direct share area expandable. When
+     * append direct share targets is enabled, return count of all available targets parking in the
+     * memory; otherwise, it is shortcuts count which will help reduce the amount of visible
+     * shuffling due to older-style direct share targets.
+     */
+    int getNumServiceTargetsForExpand() {
+        return mAppendDirectShareEnabled ? mAvailableServiceTargetsNum : mNumShortcutResults;
     }
 
     /**
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 003c0da..b62bb33 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4165,9 +4165,21 @@
      and a second time clipped to the fill level to indicate charge -->
     <bool name="config_batterymeterDualTone">false</bool>
 
-    <!-- The default peak refresh rate for a given device. Change this value if you want to allow
-         for higher refresh rates to be automatically used out of the box -->
-    <integer name="config_defaultPeakRefreshRate">60</integer>
+    <!-- The default refresh rate for a given device. Change this value to set a higher default
+         refresh rate. If the hardware composer on the device supports display modes with a higher
+         refresh rate than the default value specified here, the framework may use those higher
+         refresh rate modes if an app chooses one by setting preferredDisplayModeId or calling
+         setFrameRate().
+         If a non-zero value is set for config_defaultPeakRefreshRate, then
+         config_defaultRefreshRate may be set to 0, in which case the value set for
+         config_defaultPeakRefreshRate will act as the default frame rate. -->
+    <integer name="config_defaultRefreshRate">60</integer>
+
+    <!-- The default peak refresh rate for a given device. Change this value if you want to prevent
+         the framework from using higher refresh rates, even if display modes with higher refresh
+         rates are available from hardware composer. Only has an effect if the value is
+         non-zero. -->
+    <integer name="config_defaultPeakRefreshRate">0</integer>
 
     <!-- The display uses different gamma curves for different refresh rates. It's hard for panel
          vendor to tune the curves to have exact same brightness for different refresh rate. So
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 54d14f8..233f72ea 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -451,10 +451,14 @@
     <string name="personal_apps_suspension_text">
         Your personal apps are blocked until you turn on your work profile</string>
     <!-- Notification text. This notification lets a user know that their apps will be blocked
-        tomorrow due to a work policy from their IT admin, and that they need to turn on their work
-        profile to prevent the apps from being blocked. [CHAR LIMIT=NONE] -->
-    <string name="personal_apps_suspension_tomorrow_text">
-        Your personal apps will be blocked tomorrow</string>
+        at a particular time due to a work policy from their IT admin, and that they need to turn on
+        their work profile to prevent the apps from being blocked. It also explains for how many
+        days the profile is allowed to be off and this number is at least 3. [CHAR LIMIT=NONE] -->
+    <string name="personal_apps_suspension_soon_text">
+        Personal apps will be blocked on <xliff:g id="date" example="May 29">%1$s</xliff:g> at
+        <xliff:g id="time" example="5:20 PM">%2$s</xliff:g>. Your work profile can\u2019t stay off
+        for more than <xliff:g id="number" example="3">%3$d</xliff:g> days.
+    </string>
     <!-- Title for the button that turns work profile on. To be used in a notification
         [CHAR LIMIT=NONE] -->
     <string name="personal_apps_suspended_turn_profile_on">Turn on work profile</string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 8347a24..b483ee0 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1200,7 +1200,7 @@
   <java-symbol type="string" name="location_changed_notification_title" />
   <java-symbol type="string" name="location_changed_notification_text" />
   <java-symbol type="string" name="personal_apps_suspension_title" />
-  <java-symbol type="string" name="personal_apps_suspension_tomorrow_text" />
+  <java-symbol type="string" name="personal_apps_suspension_soon_text" />
   <java-symbol type="string" name="personal_apps_suspension_text" />
   <java-symbol type="string" name="personal_apps_suspended_turn_profile_on" />
   <java-symbol type="string" name="notification_work_profile_content_description" />
@@ -3772,6 +3772,7 @@
   <java-symbol type="string" name="bluetooth_airplane_mode_toast" />
 
   <!-- For high refresh rate displays -->
+  <java-symbol type="integer" name="config_defaultRefreshRate" />
   <java-symbol type="integer" name="config_defaultPeakRefreshRate" />
   <java-symbol type="integer" name="config_defaultRefreshRateInZone" />
   <java-symbol type="array" name="config_brightnessThresholdsOfPeakRefreshRate" />
diff --git a/data/etc/com.android.systemui.xml b/data/etc/com.android.systemui.xml
index 72827a9..a5a2221 100644
--- a/data/etc/com.android.systemui.xml
+++ b/data/etc/com.android.systemui.xml
@@ -16,6 +16,7 @@
   -->
 <permissions>
     <privapp-permissions package="com.android.systemui">
+        <permission name="android.permission.CAPTURE_AUDIO_OUTPUT"/>
         <permission name="android.permission.BATTERY_STATS"/>
         <permission name="android.permission.BIND_APPWIDGET"/>
         <permission name="android.permission.BLUETOOTH_PRIVILEGED"/>
diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/FusedPrintersProvider.java b/packages/PrintSpooler/src/com/android/printspooler/ui/FusedPrintersProvider.java
index 1709506..467a60e 100644
--- a/packages/PrintSpooler/src/com/android/printspooler/ui/FusedPrintersProvider.java
+++ b/packages/PrintSpooler/src/com/android/printspooler/ui/FusedPrintersProvider.java
@@ -231,11 +231,6 @@
         // Add the new printers, i.e. what is left.
         printers.addAll(discoveredPrinters.values());
 
-        // Do nothing if the printer list is not changed.
-        if (Objects.equals(mPrinters, printers)) {
-            return;
-        }
-
         // Update the list of printers.
         mPrinters.clear();
         mPrinters.addAll(printers);
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
index a860ff1..9e8c70f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
@@ -381,6 +381,15 @@
         return mInfoMediaManager.getActiveMediaSession();
     }
 
+    /**
+     * Gets the current package name.
+     *
+     * @return current package name
+     */
+    public String getPackageName() {
+        return mPackageName;
+    }
+
     private MediaDevice updateCurrentConnectedDevice() {
         synchronized (mMediaDevicesLock) {
             for (MediaDevice device : mMediaDevices) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java
index cefe690..8968340 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java
@@ -83,7 +83,16 @@
  * <p>An AccessPoint, which would be more fittingly named "WifiNetwork", is an aggregation of
  * {@link ScanResult ScanResults} along with pertinent metadata (e.g. current connection info,
  * network scores) required to successfully render the network to the user.
+ *
+ * @deprecated WifiTracker/AccessPoint is no longer supported, and will be removed in a future
+ * release. Clients that need a dynamic list of available wifi networks should migrate to one of the
+ * newer tracker classes,
+ * {@link com.android.wifitrackerlib.WifiPickerTracker},
+ * {@link com.android.wifitrackerlib.SavedNetworkTracker},
+ * {@link com.android.wifitrackerlib.NetworkDetailsTracker},
+ * in conjunction with {@link com.android.wifitrackerlib.WifiEntry} to represent each wifi network.
  */
+@Deprecated
 public class AccessPoint implements Comparable<AccessPoint> {
     static final String TAG = "SettingsLib.AccessPoint";
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java
index 586c154..d1cd043 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java
@@ -77,7 +77,16 @@
 
 /**
  * Tracks saved or available wifi networks and their state.
+ *
+ * @deprecated WifiTracker/AccessPoint is no longer supported, and will be removed in a future
+ * release. Clients that need a dynamic list of available wifi networks should migrate to one of the
+ * newer tracker classes,
+ * {@link com.android.wifitrackerlib.WifiPickerTracker},
+ * {@link com.android.wifitrackerlib.SavedNetworkTracker},
+ * {@link com.android.wifitrackerlib.NetworkDetailsTracker},
+ * in conjunction with {@link com.android.wifitrackerlib.WifiEntry} to represent each wifi network.
  */
+@Deprecated
 public class WifiTracker implements LifecycleObserver, OnStart, OnStop, OnDestroy {
     /**
      * Default maximum age in millis of cached scored networks in
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index c4ea2e9..055b2be 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -155,6 +155,7 @@
     <!-- Screen Recording -->
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
     <uses-permission android:name="android.permission.RECORD_AUDIO" />
+    <uses-permission android:name="android.permission.CAPTURE_AUDIO_OUTPUT"/>
 
     <!-- Assist -->
     <uses-permission android:name="android.permission.ACCESS_VOICE_INTERACTION_SERVICE" />
@@ -685,6 +686,7 @@
         </activity>
 
         <activity android:name=".controls.management.ControlsEditingActivity"
+                  android:label="@string/controls_menu_edit"
                   android:theme="@style/Theme.ControlsManagement"
                   android:excludeFromRecents="true"
                   android:noHistory="true"
@@ -762,7 +764,8 @@
         <provider
             android:name="com.android.keyguard.clock.ClockOptionsProvider"
             android:authorities="com.android.keyguard.clock"
-            android:exported="true"
+            android:enabled="false"
+            android:exported="false"
             android:grantUriPermissions="true">
         </provider>
 
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index 76ca385..09918e7 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -169,5 +169,8 @@
     <item type="id" name="screen_recording_options" />
     <item type="id" name="screen_recording_dialog_source_text" />
     <item type="id" name="screen_recording_dialog_source_description" />
+
+    <item type="id" name="accessibility_action_controls_move_before" />
+    <item type="id" name="accessibility_action_controls_move_after" />
 </resources>
 
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 8c10f61..8bbcfa0 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2711,6 +2711,8 @@
     <string name="accessibility_control_change_favorite">favorite</string>
     <!-- a11y action to unfavorite a control. It will read as "Double-tap to unfavorite" in screen readers [CHAR LIMIT=NONE] -->
     <string name="accessibility_control_change_unfavorite">unfavorite</string>
+    <!-- a11y action to move a control to the position specified by the parameter [CHAR LIMIT=NONE] -->
+    <string name="accessibility_control_move">Move to position <xliff:g id="number" example="1">%d</xliff:g></string>
 
     <!-- Controls management controls screen default title [CHAR LIMIT=30] -->
     <string name="controls_favorite_default_title">Controls</string>
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
index f044389..f1b401e 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
@@ -16,7 +16,6 @@
 package com.android.systemui.bubbles;
 
 
-import static android.app.Notification.FLAG_BUBBLE;
 import static android.os.AsyncTask.Status.FINISHED;
 import static android.view.Display.INVALID_DISPLAY;
 
@@ -28,7 +27,6 @@
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherApps;
 import android.content.pm.PackageManager;
 import android.content.pm.ShortcutInfo;
@@ -57,11 +55,6 @@
 class Bubble implements BubbleViewProvider {
     private static final String TAG = "Bubble";
 
-    /**
-     * NotificationEntry associated with the bubble. A null value implies this bubble is loaded
-     * from disk.
-     */
-    @Nullable
     private NotificationEntry mEntry;
     private final String mKey;
     private final String mGroupId;
@@ -102,56 +95,18 @@
     private Bitmap mBadgedImage;
     private int mDotColor;
     private Path mDotPath;
-    private int mFlags;
 
-    /**
-     * Extract GroupId from {@link NotificationEntry}. See also {@link #groupId(ShortcutInfo)}.
-     */
+
     public static String groupId(NotificationEntry entry) {
         UserHandle user = entry.getSbn().getUser();
         return user.getIdentifier() + "|" + entry.getSbn().getPackageName();
     }
 
-    /**
-     * Extract GroupId from {@link ShortcutInfo}. This should match the one generated from
-     * {@link NotificationEntry}. See also {@link #groupId(NotificationEntry)}.
-     */
-    @NonNull
-    public static String groupId(@NonNull final ShortcutInfo shortcutInfo) {
-        return shortcutInfo.getUserId() + "|" + shortcutInfo.getPackage();
-    }
-
-    /**
-     * Generate a unique identifier for this bubble based on given {@link NotificationEntry}. If
-     * {@link ShortcutInfo} was found in the notification entry, the identifier would be generated
-     * from {@link ShortcutInfo} instead. See also {@link #key(ShortcutInfo)}.
-     */
-    @NonNull
-    public static String key(@NonNull final NotificationEntry entry) {
-        final ShortcutInfo shortcutInfo = entry.getRanking().getShortcutInfo();
-        if (shortcutInfo != null) return key(shortcutInfo);
-        return entry.getKey();
-    }
-
-    /**
-     * Generate a unique identifier for this bubble based on given {@link ShortcutInfo}.
-     * See also {@link #key(NotificationEntry)}.
-     */
-    @NonNull
-    public static String key(@NonNull final ShortcutInfo shortcutInfo) {
-        return shortcutInfo.getUserId() + "|" + shortcutInfo.getPackage() + "|"
-                + shortcutInfo.getId();
-    }
-
-    /**
-     * Create a bubble with limited information based on given {@link ShortcutInfo}.
-     * Note: Currently this is only being used when the bubble is persisted to disk.
-     */
+    // TODO: Decouple Bubble from NotificationEntry and transform ShortcutInfo into Bubble
     Bubble(ShortcutInfo shortcutInfo) {
         mShortcutInfo = shortcutInfo;
-        mKey = key(shortcutInfo);
-        mGroupId = groupId(shortcutInfo);
-        mFlags = 0;
+        mKey = shortcutInfo.getId();
+        mGroupId = shortcutInfo.getId();
     }
 
     /** Used in tests when no UI is required. */
@@ -159,11 +114,10 @@
     Bubble(NotificationEntry e,
             BubbleController.NotificationSuppressionChangedListener listener) {
         mEntry = e;
-        mKey = key(e);
+        mKey = e.getKey();
         mLastUpdated = e.getSbn().getPostTime();
         mGroupId = groupId(e);
         mSuppressionListener = listener;
-        mFlags = e.getSbn().getNotification().flags;
     }
 
     @Override
@@ -171,26 +125,16 @@
         return mKey;
     }
 
-    @Nullable
     public NotificationEntry getEntry() {
         return mEntry;
     }
 
-    @Nullable
-    public UserHandle getUser() {
-        if (mEntry != null) return mEntry.getSbn().getUser();
-        if (mShortcutInfo != null) return mShortcutInfo.getUserHandle();
-        return null;
-    }
-
     public String getGroupId() {
         return mGroupId;
     }
 
     public String getPackageName() {
-        return mEntry == null
-                ? mShortcutInfo == null ? null : mShortcutInfo.getPackage()
-                : mEntry.getSbn().getPackageName();
+        return mEntry.getSbn().getPackageName();
     }
 
     @Override
@@ -274,8 +218,7 @@
     void inflate(BubbleViewInfoTask.Callback callback,
             Context context,
             BubbleStackView stackView,
-            BubbleIconFactory iconFactory,
-            boolean skipInflation) {
+            BubbleIconFactory iconFactory) {
         if (isBubbleLoading()) {
             mInflationTask.cancel(true /* mayInterruptIfRunning */);
         }
@@ -283,7 +226,6 @@
                 context,
                 stackView,
                 iconFactory,
-                skipInflation,
                 callback);
         if (mInflateSynchronously) {
             mInflationTask.onPostExecute(mInflationTask.doInBackground());
@@ -403,7 +345,6 @@
      * Whether this notification should be shown in the shade.
      */
     boolean showInShade() {
-        if (mEntry == null) return false;
         return !shouldSuppressNotification() || !mEntry.isClearable();
     }
 
@@ -411,8 +352,8 @@
      * Sets whether this notification should be suppressed in the shade.
      */
     void setSuppressNotification(boolean suppressNotification) {
-        if (mEntry == null) return;
         boolean prevShowInShade = showInShade();
+
         Notification.BubbleMetadata data = mEntry.getBubbleMetadata();
         int flags = data.getFlags();
         if (suppressNotification) {
@@ -443,7 +384,6 @@
      */
     @Override
     public boolean showDot() {
-        if (mEntry == null) return false;
         return mShowBubbleUpdateDot
                 && !mEntry.shouldSuppressNotificationDot()
                 && !shouldSuppressNotification();
@@ -453,7 +393,6 @@
      * Whether the flyout for the bubble should be shown.
      */
     boolean showFlyout() {
-        if (mEntry == null) return false;
         return !mSuppressFlyout && !mEntry.shouldSuppressPeek()
                 && !shouldSuppressNotification()
                 && !mEntry.shouldSuppressNotificationList();
@@ -477,13 +416,11 @@
      * is an ongoing bubble.
      */
     boolean isOngoing() {
-        if (mEntry == null) return false;
         int flags = mEntry.getSbn().getNotification().flags;
         return (flags & Notification.FLAG_FOREGROUND_SERVICE) != 0;
     }
 
     float getDesiredHeight(Context context) {
-        if (mEntry == null) return 0;
         Notification.BubbleMetadata data = mEntry.getBubbleMetadata();
         boolean useRes = data.getDesiredHeightResId() != 0;
         if (useRes) {
@@ -497,7 +434,6 @@
     }
 
     String getDesiredHeightString() {
-        if (mEntry == null) return String.valueOf(0);
         Notification.BubbleMetadata data = mEntry.getBubbleMetadata();
         boolean useRes = data.getDesiredHeightResId() != 0;
         if (useRes) {
@@ -514,13 +450,11 @@
      * To populate the icon use {@link LauncherApps#getShortcutIconDrawable(ShortcutInfo, int)}.
      */
     boolean usingShortcutInfo() {
-        return mEntry != null && mEntry.getBubbleMetadata().getShortcutId() != null
-                || mShortcutInfo != null;
+        return mEntry.getBubbleMetadata().getShortcutId() != null;
     }
 
     @Nullable
     PendingIntent getBubbleIntent() {
-        if (mEntry == null) return null;
         Notification.BubbleMetadata data = mEntry.getBubbleMetadata();
         if (data != null) {
             return data.getIntent();
@@ -528,32 +462,16 @@
         return null;
     }
 
-    Intent getSettingsIntent(final Context context) {
+    Intent getSettingsIntent() {
         final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS);
         intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
-        final int uid = getUid(context);
-        if (uid != -1) {
-            intent.putExtra(Settings.EXTRA_APP_UID, uid);
-        }
+        intent.putExtra(Settings.EXTRA_APP_UID, mEntry.getSbn().getUid());
         intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
         return intent;
     }
 
-    private int getUid(final Context context) {
-        if (mEntry != null) return mEntry.getSbn().getUid();
-        final PackageManager pm = context.getPackageManager();
-        if (pm == null) return -1;
-        try {
-            final ApplicationInfo info = pm.getApplicationInfo(mShortcutInfo.getPackage(), 0);
-            return info.uid;
-        } catch (PackageManager.NameNotFoundException e) {
-            Log.e(TAG, "cannot find uid", e);
-        }
-        return -1;
-    }
-
     private int getDimenForPackageUser(Context context, int resId, String pkg, int userId) {
         PackageManager pm = context.getPackageManager();
         Resources r;
@@ -575,30 +493,15 @@
     }
 
     private boolean shouldSuppressNotification() {
-        if (mEntry == null) return false;
         return mEntry.getBubbleMetadata() != null
                 && mEntry.getBubbleMetadata().isNotificationSuppressed();
     }
 
     boolean shouldAutoExpand() {
-        if (mEntry == null) return false;
         Notification.BubbleMetadata metadata = mEntry.getBubbleMetadata();
         return metadata != null && metadata.getAutoExpandBubble();
     }
 
-    public boolean isBubble() {
-        if (mEntry == null) return (mFlags & FLAG_BUBBLE) != 0;
-        return (mEntry.getSbn().getNotification().flags & FLAG_BUBBLE) != 0;
-    }
-
-    public void enable(int option) {
-        mFlags |= option;
-    }
-
-    public void disable(int option) {
-        mFlags &= ~option;
-    }
-
     @Override
     public String toString() {
         return "Bubble{" + mKey + '}';
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index 8707d38..a578f33 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -42,7 +42,6 @@
 import static java.lang.annotation.ElementType.PARAMETER;
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
-import android.annotation.NonNull;
 import android.annotation.UserIdInt;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.INotificationManager;
@@ -243,7 +242,7 @@
          * This can happen when an app cancels a bubbled notification or when the user dismisses a
          * bubble.
          */
-        void removeNotification(@NonNull NotificationEntry entry, int reason);
+        void removeNotification(NotificationEntry entry, int reason);
 
         /**
          * Called when a bubbled notification has changed whether it should be
@@ -259,7 +258,7 @@
          * removes all remnants of the group's summary from the notification pipeline.
          * TODO: (b/145659174) Only old pipeline needs this - delete post-migration.
          */
-        void maybeCancelSummary(@NonNull NotificationEntry entry);
+        void maybeCancelSummary(NotificationEntry entry);
     }
 
     /**
@@ -482,7 +481,7 @@
 
         addNotifCallback(new NotifCallback() {
             @Override
-            public void removeNotification(@NonNull final NotificationEntry entry, int reason) {
+            public void removeNotification(NotificationEntry entry, int reason) {
                 mNotificationEntryManager.performRemoveNotification(entry.getSbn(),
                         reason);
             }
@@ -493,7 +492,7 @@
             }
 
             @Override
-            public void maybeCancelSummary(@NonNull final NotificationEntry entry) {
+            public void maybeCancelSummary(NotificationEntry entry) {
                 // Check if removed bubble has an associated suppressed group summary that needs
                 // to be removed now.
                 final String groupKey = entry.getSbn().getGroupKey();
@@ -702,12 +701,10 @@
         mBubbleIconFactory = new BubbleIconFactory(mContext);
         // Reload each bubble
         for (Bubble b: mBubbleData.getBubbles()) {
-            b.inflate(null /* callback */, mContext, mStackView, mBubbleIconFactory,
-                    false /* skipInflation */);
+            b.inflate(null /* callback */, mContext, mStackView, mBubbleIconFactory);
         }
         for (Bubble b: mBubbleData.getOverflowBubbles()) {
-            b.inflate(null /* callback */, mContext, mStackView, mBubbleIconFactory,
-                    false /* skipInflation */);
+            b.inflate(null /* callback */, mContext, mStackView, mBubbleIconFactory);
         }
     }
 
@@ -806,7 +803,7 @@
             if (bubble != null) {
                 mBubbleData.promoteBubbleFromOverflow(bubble, mStackView, mBubbleIconFactory);
             }
-        } else if (bubble.isBubble()) {
+        } else if (bubble.getEntry().isBubble()){
             mBubbleData.setSelectedBubble(bubble);
         }
         mBubbleData.setExpanded(true);
@@ -835,33 +832,10 @@
         updateBubble(notif, suppressFlyout, true /* showInShade */);
     }
 
-    /**
-     * Fills the overflow bubbles by loading them from disk.
-     */
-    void loadOverflowBubblesFromDisk() {
-        if (!mBubbleData.getOverflowBubbles().isEmpty()) {
-            // we don't need to load overflow bubbles from disk if it is already in memory
-            return;
-        }
-        mDataRepository.loadBubbles((bubbles) -> {
-            bubbles.forEach(bubble -> {
-                if (mBubbleData.getBubbles().contains(bubble)) {
-                    // if the bubble is already active, there's no need to push it to overflow
-                    return;
-                }
-                bubble.inflate((b) -> mBubbleData.overflowBubble(DISMISS_AGED, bubble),
-                        mContext, mStackView, mBubbleIconFactory, true /* skipInflation */);
-            });
-            return null;
-        });
-    }
-
     void updateBubble(NotificationEntry notif, boolean suppressFlyout, boolean showInShade) {
         if (mStackView == null) {
             // Lazy init stack view when a bubble is created
             ensureStackViewCreated();
-            // Lazy load overflow bubbles from disk
-            loadOverflowBubblesFromDisk();
         }
         // If this is an interruptive notif, mark that it's interrupted
         if (notif.getImportance() >= NotificationManager.IMPORTANCE_HIGH) {
@@ -881,11 +855,11 @@
                             return;
                         }
                         mHandler.post(
-                                () -> removeBubble(bubble.getKey(),
+                                () -> removeBubble(bubble.getEntry(),
                                         BubbleController.DISMISS_INVALID_INTENT));
                     });
                 },
-                mContext, mStackView, mBubbleIconFactory, false /* skipInflation */);
+                mContext, mStackView, mBubbleIconFactory);
     }
 
     /**
@@ -897,10 +871,7 @@
      * @param entry the notification to change bubble state for.
      * @param shouldBubble whether the notification should show as a bubble or not.
      */
-    public void onUserChangedBubble(@Nullable final NotificationEntry entry, boolean shouldBubble) {
-        if (entry == null) {
-            return;
-        }
+    public void onUserChangedBubble(NotificationEntry entry, boolean shouldBubble) {
         NotificationChannel channel = entry.getChannel();
         final String appPkg = entry.getSbn().getPackageName();
         final int appUid = entry.getSbn().getUid();
@@ -939,14 +910,14 @@
     }
 
     /**
-     * Removes the bubble with the given key.
+     * Removes the bubble with the given NotificationEntry.
      * <p>
      * Must be called from the main thread.
      */
     @MainThread
-    void removeBubble(String key, int reason) {
-        if (mBubbleData.hasAnyBubbleWithKey(key)) {
-            mBubbleData.notificationEntryRemoved(key, reason);
+    void removeBubble(NotificationEntry entry, int reason) {
+        if (mBubbleData.hasAnyBubbleWithKey(entry.getKey())) {
+            mBubbleData.notificationEntryRemoved(entry, reason);
         }
     }
 
@@ -962,7 +933,7 @@
                 && canLaunchInActivityView(mContext, entry);
         if (!shouldBubble && mBubbleData.hasAnyBubbleWithKey(entry.getKey())) {
             // It was previously a bubble but no longer a bubble -- lets remove it
-            removeBubble(entry.getKey(), DISMISS_NO_LONGER_BUBBLE);
+            removeBubble(entry, DISMISS_NO_LONGER_BUBBLE);
         } else if (shouldBubble) {
             updateBubble(entry);
         }
@@ -976,10 +947,10 @@
             // Remove any associated bubble children with the summary
             final List<Bubble> bubbleChildren = mBubbleData.getBubblesInGroup(groupKey);
             for (int i = 0; i < bubbleChildren.size(); i++) {
-                removeBubble(bubbleChildren.get(i).getKey(), DISMISS_GROUP_CANCELLED);
+                removeBubble(bubbleChildren.get(i).getEntry(), DISMISS_GROUP_CANCELLED);
             }
         } else {
-            removeBubble(entry.getKey(), DISMISS_NOTIF_CANCEL);
+            removeBubble(entry, DISMISS_NOTIF_CANCEL);
         }
     }
 
@@ -1001,8 +972,7 @@
             rankingMap.getRanking(key, mTmpRanking);
             boolean isActiveBubble = mBubbleData.hasAnyBubbleWithKey(key);
             if (isActiveBubble && !mTmpRanking.canBubble()) {
-                mBubbleData.notificationEntryRemoved(entry.getKey(),
-                        BubbleController.DISMISS_BLOCKED);
+                mBubbleData.notificationEntryRemoved(entry, BubbleController.DISMISS_BLOCKED);
             } else if (entry != null && mTmpRanking.isBubble() && !isActiveBubble) {
                 entry.setFlagBubble(true);
                 onEntryUpdated(entry);
@@ -1012,15 +982,9 @@
 
     private void setIsBubble(Bubble b, boolean isBubble) {
         if (isBubble) {
-            if (b.getEntry() != null) {
-                b.getEntry().getSbn().getNotification().flags |= FLAG_BUBBLE;
-            }
-            b.enable(FLAG_BUBBLE);
+            b.getEntry().getSbn().getNotification().flags |= FLAG_BUBBLE;
         } else {
-            if (b.getEntry() != null) {
-                b.getEntry().getSbn().getNotification().flags &= ~FLAG_BUBBLE;
-            }
-            b.disable(FLAG_BUBBLE);
+            b.getEntry().getSbn().getNotification().flags &= ~FLAG_BUBBLE;
         }
         try {
             mBarService.onNotificationBubbleChanged(b.getKey(), isBubble, 0);
@@ -1068,27 +1032,23 @@
                         // The bubble is now gone & the notification is hidden from the shade, so
                         // time to actually remove it
                         for (NotifCallback cb : mCallbacks) {
-                            if (bubble.getEntry() != null) {
-                                cb.removeNotification(bubble.getEntry(), REASON_CANCEL);
-                            }
+                            cb.removeNotification(bubble.getEntry(), REASON_CANCEL);
                         }
                     } else {
-                        if (bubble.isBubble() && bubble.showInShade()) {
+                        if (bubble.getEntry().isBubble() && bubble.showInShade()) {
                             setIsBubble(bubble, /* isBubble */ false);
                         }
-                        if (bubble.getEntry() != null && bubble.getEntry().getRow() != null) {
+                        if (bubble.getEntry().getRow() != null) {
                             bubble.getEntry().getRow().updateBubbleButton();
                         }
                     }
 
                 }
-                if (bubble.getEntry() != null) {
-                    final String groupKey = bubble.getEntry().getSbn().getGroupKey();
-                    if (mBubbleData.getBubblesInGroup(groupKey).isEmpty()) {
-                        // Time to potentially remove the summary
-                        for (NotifCallback cb : mCallbacks) {
-                            cb.maybeCancelSummary(bubble.getEntry());
-                        }
+                final String groupKey = bubble.getEntry().getSbn().getGroupKey();
+                if (mBubbleData.getBubblesInGroup(groupKey).isEmpty()) {
+                    // Time to potentially remove the summary
+                    for (NotifCallback cb : mCallbacks) {
+                        cb.maybeCancelSummary(bubble.getEntry());
                     }
                 }
             }
@@ -1113,7 +1073,7 @@
 
             if (update.selectionChanged) {
                 mStackView.setSelectedBubble(update.selectedBubble);
-                if (update.selectedBubble != null && update.selectedBubble.getEntry() != null) {
+                if (update.selectedBubble != null) {
                     mNotificationGroupManager.updateSuppression(
                             update.selectedBubble.getEntry());
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
index 7b323ce..65d5beb 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
@@ -23,7 +23,6 @@
 
 import static java.util.stream.Collectors.toList;
 
-import android.annotation.NonNull;
 import android.app.Notification;
 import android.app.PendingIntent;
 import android.content.Context;
@@ -224,7 +223,7 @@
                             false, /* showInShade */ true);
                     setSelectedBubble(bubble);
                 },
-                mContext, stack, factory, false /* skipInflation */);
+                mContext, stack, factory);
         dispatchPendingChanges();
     }
 
@@ -279,8 +278,7 @@
         }
         mPendingBubbles.remove(bubble); // No longer pending once we're here
         Bubble prevBubble = getBubbleInStackWithKey(bubble.getKey());
-        suppressFlyout |= bubble.getEntry() == null
-                || !bubble.getEntry().getRanking().visuallyInterruptive();
+        suppressFlyout |= !bubble.getEntry().getRanking().visuallyInterruptive();
 
         if (prevBubble == null) {
             // Create a new bubble
@@ -309,14 +307,11 @@
         dispatchPendingChanges();
     }
 
-    /**
-     * Called when a notification associated with a bubble is removed.
-     */
-    public void notificationEntryRemoved(String key, @DismissReason int reason) {
+    public void notificationEntryRemoved(NotificationEntry entry, @DismissReason int reason) {
         if (DEBUG_BUBBLE_DATA) {
-            Log.d(TAG, "notificationEntryRemoved: key=" + key + " reason=" + reason);
+            Log.d(TAG, "notificationEntryRemoved: entry=" + entry + " reason=" + reason);
         }
-        doRemove(key, reason);
+        doRemove(entry.getKey(), reason);
         dispatchPendingChanges();
     }
 
@@ -364,7 +359,7 @@
             return bubbleChildren;
         }
         for (Bubble b : mBubbles) {
-            if (b.getEntry() != null && groupKey.equals(b.getEntry().getSbn().getGroupKey())) {
+            if (groupKey.equals(b.getEntry().getSbn().getGroupKey())) {
                 bubbleChildren.add(b);
             }
         }
@@ -475,9 +470,7 @@
             Bubble newSelected = mBubbles.get(newIndex);
             setSelectedBubbleInternal(newSelected);
         }
-        if (bubbleToRemove.getEntry() != null) {
-            maybeSendDeleteIntent(reason, bubbleToRemove.getEntry());
-        }
+        maybeSendDeleteIntent(reason, bubbleToRemove.getEntry());
     }
 
     void overflowBubble(@DismissReason int reason, Bubble bubble) {
@@ -751,8 +744,7 @@
         return true;
     }
 
-    private void maybeSendDeleteIntent(@DismissReason int reason,
-            @NonNull final NotificationEntry entry) {
+    private void maybeSendDeleteIntent(@DismissReason int reason, NotificationEntry entry) {
         if (reason == BubbleController.DISMISS_USER_GESTURE) {
             Notification.BubbleMetadata bubbleMetadata = entry.getBubbleMetadata();
             PendingIntent deleteIntent = bubbleMetadata != null
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt
index 6df3132..ba93f41 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt
@@ -74,10 +74,7 @@
 
     private fun transform(userId: Int, bubbles: List<Bubble>): List<BubbleEntity> {
         return bubbles.mapNotNull { b ->
-            var shortcutId = b.shortcutInfo?.id
-            if (shortcutId == null) shortcutId = b.entry?.bubbleMetadata?.shortcutId
-            if (shortcutId == null) shortcutId = b.entry?.ranking?.shortcutInfo?.id
-            if (shortcutId == null) return@mapNotNull null
+            val shortcutId = b.shortcutInfo?.id ?: return@mapNotNull null
             BubbleEntity(userId, b.packageName, shortcutId)
         }
     }
@@ -111,6 +108,7 @@
     /**
      * Load bubbles from disk.
      */
+    // TODO: call this method from BubbleController and update UI
     @SuppressLint("WrongConstant")
     fun loadBubbles(cb: (List<Bubble>) -> Unit) = ioScope.launch {
         /**
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
index 0a1a0f7..baf92dc 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
@@ -65,6 +65,7 @@
 import com.android.systemui.R;
 import com.android.systemui.recents.TriangleShape;
 import com.android.systemui.statusbar.AlphaOptimizedButton;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 
 /**
  * Container for the expanded bubble view, handles rendering the caret and settings icon.
@@ -160,7 +161,7 @@
                             // the bubble again so we'll just remove it.
                             Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey()
                                     + ", " + e.getMessage() + "; removing bubble");
-                            mBubbleController.removeBubble(getBubbleKey(),
+                            mBubbleController.removeBubble(getBubbleEntry(),
                                     BubbleController.DISMISS_INVALID_INTENT);
                         }
                     });
@@ -204,7 +205,7 @@
             }
             if (mBubble != null) {
                 // Must post because this is called from a binder thread.
-                post(() -> mBubbleController.removeBubble(mBubble.getKey(),
+                post(() -> mBubbleController.removeBubble(mBubble.getEntry(),
                         BubbleController.DISMISS_TASK_FINISHED));
             }
         }
@@ -291,6 +292,10 @@
         return mBubble != null ? mBubble.getKey() : "null";
     }
 
+    private NotificationEntry getBubbleEntry() {
+        return mBubble != null ? mBubble.getEntry() : null;
+    }
+
     void setManageClickListener(OnClickListener manageClickListener) {
         findViewById(R.id.settings_button).setOnClickListener(manageClickListener);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index e35b203..88f5eb0 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -49,6 +49,7 @@
 import android.graphics.Region;
 import android.os.Bundle;
 import android.os.Vibrator;
+import android.service.notification.StatusBarNotification;
 import android.util.Log;
 import android.view.Choreographer;
 import android.view.DisplayCutout;
@@ -918,10 +919,10 @@
                     showManageMenu(false /* show */);
                     final Bubble bubble = mBubbleData.getSelectedBubble();
                     if (bubble != null && mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) {
-                        final Intent intent = bubble.getSettingsIntent(mContext);
+                        final Intent intent = bubble.getSettingsIntent();
                         collapseStack(() -> {
-
-                            mContext.startActivityAsUser(intent, bubble.getUser());
+                            mContext.startActivityAsUser(
+                                    intent, bubble.getEntry().getSbn().getUser());
                             logBubbleClickEvent(
                                     bubble,
                                     SysUiStatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_SETTINGS);
@@ -1178,19 +1179,13 @@
         for (int i = 0; i < mBubbleData.getBubbles().size(); i++) {
             final Bubble bubble = mBubbleData.getBubbles().get(i);
             final String appName = bubble.getAppName();
+            final Notification notification = bubble.getEntry().getSbn().getNotification();
+            final CharSequence titleCharSeq =
+                    notification.extras.getCharSequence(Notification.EXTRA_TITLE);
 
-            final CharSequence titleCharSeq;
-            if (bubble.getEntry() == null) {
-                titleCharSeq = null;
-            } else {
-                titleCharSeq = bubble.getEntry().getSbn().getNotification().extras.getCharSequence(
-                        Notification.EXTRA_TITLE);
-            }
-            final String titleStr;
+            String titleStr = getResources().getString(R.string.notification_bubble_title);
             if (titleCharSeq != null) {
                 titleStr = titleCharSeq.toString();
-            } else {
-                titleStr = getResources().getString(R.string.notification_bubble_title);
             }
 
             if (bubble.getIconView() != null) {
@@ -1803,7 +1798,7 @@
     private void dismissBubbleIfExists(@Nullable Bubble bubble) {
         if (bubble != null && mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) {
             mBubbleData.notificationEntryRemoved(
-                    bubble.getKey(), BubbleController.DISMISS_USER_GESTURE);
+                    bubble.getEntry(), BubbleController.DISMISS_USER_GESTURE);
         }
     }
 
@@ -2301,12 +2296,18 @@
      * @param action the user interaction enum.
      */
     private void logBubbleClickEvent(Bubble bubble, int action) {
-        bubble.logUIEvent(
+        StatusBarNotification notification = bubble.getEntry().getSbn();
+        SysUiStatsLog.write(SysUiStatsLog.BUBBLE_UI_CHANGED,
+                notification.getPackageName(),
+                notification.getNotification().getChannelId(),
+                notification.getId(),
+                getBubbleIndex(getExpandedBubble()),
                 getBubbleCount(),
                 action,
                 getNormalizedXPosition(),
                 getNormalizedYPosition(),
-                getBubbleIndex(getExpandedBubble())
-        );
+                bubble.showInShade(),
+                bubble.isOngoing(),
+                false /* isAppForeground (unused) */);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java
index 525d5b5..8a57a73 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java
@@ -37,7 +37,6 @@
 import android.graphics.drawable.Icon;
 import android.os.AsyncTask;
 import android.os.Parcelable;
-import android.os.UserHandle;
 import android.service.notification.StatusBarNotification;
 import android.text.TextUtils;
 import android.util.Log;
@@ -75,7 +74,6 @@
     private WeakReference<Context> mContext;
     private WeakReference<BubbleStackView> mStackView;
     private BubbleIconFactory mIconFactory;
-    private boolean mSkipInflation;
     private Callback mCallback;
 
     /**
@@ -86,20 +84,17 @@
             Context context,
             BubbleStackView stackView,
             BubbleIconFactory factory,
-            boolean skipInflation,
             Callback c) {
         mBubble = b;
         mContext = new WeakReference<>(context);
         mStackView = new WeakReference<>(stackView);
         mIconFactory = factory;
-        mSkipInflation = skipInflation;
         mCallback = c;
     }
 
     @Override
     protected BubbleViewInfo doInBackground(Void... voids) {
-        return BubbleViewInfo.populate(mContext.get(), mStackView.get(), mIconFactory, mBubble,
-                mSkipInflation);
+        return BubbleViewInfo.populate(mContext.get(), mStackView.get(), mIconFactory, mBubble);
     }
 
     @Override
@@ -128,36 +123,11 @@
 
         @Nullable
         static BubbleViewInfo populate(Context c, BubbleStackView stackView,
-                BubbleIconFactory iconFactory, Bubble b, boolean skipInflation) {
-            final NotificationEntry entry = b.getEntry();
-            if (entry == null) {
-                // populate from ShortcutInfo when NotificationEntry is not available
-                final ShortcutInfo s = b.getShortcutInfo();
-                return populate(c, stackView, iconFactory, skipInflation || b.isInflated(),
-                        s.getPackage(), s.getUserHandle(), s, null);
-            }
-            final StatusBarNotification sbn = entry.getSbn();
-            final String bubbleShortcutId =  entry.getBubbleMetadata().getShortcutId();
-            final ShortcutInfo si = bubbleShortcutId == null
-                    ? null : entry.getRanking().getShortcutInfo();
-            return populate(
-                    c, stackView, iconFactory, skipInflation || b.isInflated(),
-                    sbn.getPackageName(), sbn.getUser(), si, entry);
-        }
-
-        private static BubbleViewInfo populate(
-                @NonNull final Context c,
-                @NonNull final BubbleStackView stackView,
-                @NonNull final BubbleIconFactory iconFactory,
-                final boolean isInflated,
-                @NonNull final String packageName,
-                @NonNull final UserHandle user,
-                @Nullable final ShortcutInfo shortcutInfo,
-                @Nullable final NotificationEntry entry) {
+                BubbleIconFactory iconFactory, Bubble b) {
             BubbleViewInfo info = new BubbleViewInfo();
 
             // View inflation: only should do this once per bubble
-            if (!isInflated) {
+            if (!b.isInflated()) {
                 LayoutInflater inflater = LayoutInflater.from(c);
                 info.imageView = (BadgedImageView) inflater.inflate(
                         R.layout.bubble_view, stackView, false /* attachToRoot */);
@@ -167,8 +137,12 @@
                 info.expandedView.setStackView(stackView);
             }
 
-            if (shortcutInfo != null) {
-                info.shortcutInfo = shortcutInfo;
+            StatusBarNotification sbn = b.getEntry().getSbn();
+            String packageName = sbn.getPackageName();
+
+            String bubbleShortcutId =  b.getEntry().getBubbleMetadata().getShortcutId();
+            if (bubbleShortcutId != null) {
+                info.shortcutInfo = b.getEntry().getRanking().getShortcutInfo();
             }
 
             // App name & app icon
@@ -187,7 +161,7 @@
                     info.appName = String.valueOf(pm.getApplicationLabel(appInfo));
                 }
                 appIcon = pm.getApplicationIcon(packageName);
-                badgedIcon = pm.getUserBadgedIcon(appIcon, user);
+                badgedIcon = pm.getUserBadgedIcon(appIcon, sbn.getUser());
             } catch (PackageManager.NameNotFoundException exception) {
                 // If we can't find package... don't think we should show the bubble.
                 Log.w(TAG, "Unable to find package: " + packageName);
@@ -196,7 +170,7 @@
 
             // Badged bubble image
             Drawable bubbleDrawable = iconFactory.getBubbleDrawable(c, info.shortcutInfo,
-                    entry == null ? null : entry.getBubbleMetadata());
+                    b.getEntry().getBubbleMetadata());
             if (bubbleDrawable == null) {
                 // Default to app icon
                 bubbleDrawable = appIcon;
@@ -222,9 +196,7 @@
                     Color.WHITE, WHITE_SCRIM_ALPHA);
 
             // Flyout
-            if (entry != null) {
-                info.flyoutMessage = extractFlyoutMessage(c, entry);
-            }
+            info.flyoutMessage = extractFlyoutMessage(c, b.getEntry());
             return info;
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/AllModel.kt b/packages/SystemUI/src/com/android/systemui/controls/management/AllModel.kt
index 175ed06..00a406e 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/AllModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/AllModel.kt
@@ -48,6 +48,8 @@
 
     private var modified = false
 
+    override val moveHelper = null
+
     override val favorites: List<ControlInfo>
         get() = favoriteIds.mapNotNull { id ->
             val control = controls.firstOrNull { it.control.controlId == id }?.control
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt
index 4b283d6..2f91710 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt
@@ -18,6 +18,7 @@
 
 import android.content.ComponentName
 import android.graphics.Rect
+import android.os.Bundle
 import android.service.controls.Control
 import android.service.controls.DeviceTypes
 import android.view.LayoutInflater
@@ -78,7 +79,7 @@
                         background = parent.context.getDrawable(
                                 R.drawable.control_background_ripple)
                     },
-                    model is FavoritesModel // Indicates that position information is needed
+                    model?.moveHelper // Indicates that position information is needed
                 ) { id, favorite ->
                     model?.changeFavoriteStatus(id, favorite)
                 }
@@ -176,12 +177,14 @@
 
 /**
  * Holder for using with [ControlStatusWrapper] to display names of zones.
+ * @param moveHelper a helper interface to facilitate a11y rearranging. Null indicates no
+ *                   rearranging
  * @param favoriteCallback this callback will be called whenever the favorite state of the
  *                         [Control] this view represents changes.
  */
 internal class ControlHolder(
     view: View,
-    val withPosition: Boolean,
+    val moveHelper: ControlsModel.MoveHelper?,
     val favoriteCallback: ModelFavoriteChanger
 ) : Holder(view) {
     private val favoriteStateDescription =
@@ -197,7 +200,11 @@
         visibility = View.VISIBLE
     }
 
-    private val accessibilityDelegate = ControlHolderAccessibilityDelegate(this::stateDescription)
+    private val accessibilityDelegate = ControlHolderAccessibilityDelegate(
+        this::stateDescription,
+        this::getLayoutPosition,
+        moveHelper
+    )
 
     init {
         ViewCompat.setAccessibilityDelegate(itemView, accessibilityDelegate)
@@ -207,7 +214,7 @@
     private fun stateDescription(favorite: Boolean): CharSequence? {
         if (!favorite) {
             return notFavoriteStateDescription
-        } else if (!withPosition) {
+        } else if (moveHelper == null) {
             return favoriteStateDescription
         } else {
             val position = layoutPosition + 1
@@ -256,15 +263,67 @@
     }
 }
 
+/**
+ * Accessibility delegate for [ControlHolder].
+ *
+ * Provides the following functionality:
+ * * Sets the state description indicating whether the controls is Favorited or Unfavorited
+ * * Adds the position to the state description if necessary.
+ * * Adds context action for moving (rearranging) a control.
+ *
+ * @param stateRetriever function to determine the state description based on the favorite state
+ * @param positionRetriever function to obtain the position of this control. It only has to be
+ *                          correct in controls that are currently favorites (and therefore can
+ *                          be moved).
+ * @param moveHelper helper interface to determine if a control can be moved and actually move it.
+ */
 private class ControlHolderAccessibilityDelegate(
-    val stateRetriever: (Boolean) -> CharSequence?
+    val stateRetriever: (Boolean) -> CharSequence?,
+    val positionRetriever: () -> Int,
+    val moveHelper: ControlsModel.MoveHelper?
 ) : AccessibilityDelegateCompat() {
 
     var isFavorite = false
 
+    companion object {
+        private val MOVE_BEFORE_ID = R.id.accessibility_action_controls_move_before
+        private val MOVE_AFTER_ID = R.id.accessibility_action_controls_move_after
+    }
+
     override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
         super.onInitializeAccessibilityNodeInfo(host, info)
 
+        info.isContextClickable = false
+        addClickAction(host, info)
+        maybeAddMoveBeforeAction(host, info)
+        maybeAddMoveAfterAction(host, info)
+
+        // Determine the stateDescription based on the holder information
+        info.stateDescription = stateRetriever(isFavorite)
+        // Remove the information at the end indicating row and column.
+        info.setCollectionItemInfo(null)
+
+        info.className = Switch::class.java.name
+    }
+
+    override fun performAccessibilityAction(host: View?, action: Int, args: Bundle?): Boolean {
+        if (super.performAccessibilityAction(host, action, args)) {
+            return true
+        }
+        return when (action) {
+            MOVE_BEFORE_ID -> {
+                moveHelper?.moveBefore(positionRetriever())
+                true
+            }
+            MOVE_AFTER_ID -> {
+                moveHelper?.moveAfter(positionRetriever())
+                true
+            }
+            else -> false
+        }
+    }
+
+    private fun addClickAction(host: View, info: AccessibilityNodeInfoCompat) {
         // Change the text for the double-tap action
         val clickActionString = if (isFavorite) {
             host.context.getString(R.string.accessibility_control_change_unfavorite)
@@ -276,13 +335,30 @@
             // “favorite/unfavorite”
             clickActionString)
         info.addAction(click)
+    }
 
-        // Determine the stateDescription based on the holder information
-        info.stateDescription = stateRetriever(isFavorite)
-        // Remove the information at the end indicating row and column.
-        info.setCollectionItemInfo(null)
+    private fun maybeAddMoveBeforeAction(host: View, info: AccessibilityNodeInfoCompat) {
+        if (moveHelper?.canMoveBefore(positionRetriever()) ?: false) {
+            val newPosition = positionRetriever() + 1 - 1
+            val moveBefore = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+                MOVE_BEFORE_ID,
+                host.context.getString(R.string.accessibility_control_move, newPosition)
+            )
+            info.addAction(moveBefore)
+            info.isContextClickable = true
+        }
+    }
 
-        info.className = Switch::class.java.name
+    private fun maybeAddMoveAfterAction(host: View, info: AccessibilityNodeInfoCompat) {
+        if (moveHelper?.canMoveAfter(positionRetriever()) ?: false) {
+            val newPosition = positionRetriever() + 1 + 1
+            val moveAfter = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+                MOVE_AFTER_ID,
+                host.context.getString(R.string.accessibility_control_move, newPosition)
+            )
+            info.addAction(moveAfter)
+            info.isContextClickable = true
+        }
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsModel.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsModel.kt
index 37b6d15..2543953 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsModel.kt
@@ -42,6 +42,8 @@
      */
     val elements: List<ElementWrapper>
 
+    val moveHelper: MoveHelper?
+
     /**
      * Change the favorite status of a particular control.
      */
@@ -69,6 +71,34 @@
          */
         fun onFirstChange()
     }
+
+    /**
+     * Interface to facilitate moving controls from an [AccessibilityDelegate].
+     *
+     * All positions should be 0 based.
+     */
+    interface MoveHelper {
+
+        /**
+         * Whether the control in `position` can be moved to the position before it.
+         */
+        fun canMoveBefore(position: Int): Boolean
+
+        /**
+         * Whether the control in `position` can be moved to the position after it.
+         */
+        fun canMoveAfter(position: Int): Boolean
+
+        /**
+         * Move the control in `position` to the position before it.
+         */
+        fun moveBefore(position: Int)
+
+        /**
+         * Move the control in `position` to the position after it.
+         */
+        fun moveAfter(position: Int)
+    }
 }
 
 /**
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/FavoritesModel.kt b/packages/SystemUI/src/com/android/systemui/controls/management/FavoritesModel.kt
index 411170cb..5242501 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/FavoritesModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/FavoritesModel.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.controls.management
 
 import android.content.ComponentName
+import android.util.Log
 import androidx.recyclerview.widget.ItemTouchHelper
 import androidx.recyclerview.widget.RecyclerView
 import com.android.systemui.controls.ControlInterface
@@ -39,9 +40,39 @@
     private val favoritesModelCallback: FavoritesModelCallback
 ) : ControlsModel {
 
+    companion object {
+        private const val TAG = "FavoritesModel"
+    }
+
     private var adapter: RecyclerView.Adapter<*>? = null
     private var modified = false
 
+    override val moveHelper = object : ControlsModel.MoveHelper {
+        override fun canMoveBefore(position: Int): Boolean {
+            return position > 0 && position < dividerPosition
+        }
+
+        override fun canMoveAfter(position: Int): Boolean {
+            return position >= 0 && position < dividerPosition - 1
+        }
+
+        override fun moveBefore(position: Int) {
+            if (!canMoveBefore(position)) {
+                Log.w(TAG, "Cannot move position $position before")
+            } else {
+                onMoveItem(position, position - 1)
+            }
+        }
+
+        override fun moveAfter(position: Int) {
+            if (!canMoveAfter(position)) {
+                Log.w(TAG, "Cannot move position $position after")
+            } else {
+                onMoveItem(position, position + 1)
+            }
+        }
+    }
+
     override fun attachAdapter(adapter: RecyclerView.Adapter<*>) {
         this.adapter = adapter
     }
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt
index 17e4234..f979bbb 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt
@@ -18,10 +18,15 @@
 
 import android.animation.Animator
 import android.animation.AnimatorListenerAdapter
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
 import android.animation.ValueAnimator
+import android.annotation.ColorRes
 import android.app.Dialog
 import android.content.Context
+import android.content.res.ColorStateList
 import android.graphics.drawable.ClipDrawable
+import android.graphics.drawable.Drawable
 import android.graphics.drawable.GradientDrawable
 import android.graphics.drawable.LayerDrawable
 import android.service.controls.Control
@@ -34,6 +39,7 @@
 import android.service.controls.templates.ToggleRangeTemplate
 import android.service.controls.templates.ToggleTemplate
 import android.util.MathUtils
+import android.util.TypedValue
 import android.view.View
 import android.view.ViewGroup
 import android.widget.ImageView
@@ -63,6 +69,8 @@
         private const val UPDATE_DELAY_IN_MILLIS = 3000L
         private const val ALPHA_ENABLED = 255
         private const val ALPHA_DISABLED = 0
+        private const val STATUS_ALPHA_ENABLED = 1f
+        private const val STATUS_ALPHA_DIMMED = 0.45f
         private val FORCE_PANEL_DEVICES = setOf(
             DeviceTypes.TYPE_THERMOSTAT,
             DeviceTypes.TYPE_CAMERA
@@ -94,9 +102,11 @@
     private val toggleBackgroundIntensity: Float = layout.context.resources
             .getFraction(R.fraction.controls_toggle_bg_intensity, 1, 1)
     private var stateAnimator: ValueAnimator? = null
+    private var statusAnimator: Animator? = null
     private val baseLayer: GradientDrawable
     val icon: ImageView = layout.requireViewById(R.id.icon)
-    val status: TextView = layout.requireViewById(R.id.status)
+    private val status: TextView = layout.requireViewById(R.id.status)
+    private var nextStatusText: CharSequence = ""
     val title: TextView = layout.requireViewById(R.id.title)
     val subtitle: TextView = layout.requireViewById(R.id.subtitle)
     val context: Context = layout.getContext()
@@ -105,6 +115,7 @@
     var cancelUpdate: Runnable? = null
     var behavior: Behavior? = null
     var lastAction: ControlAction? = null
+    var isLoading = false
     private var lastChallengeDialog: Dialog? = null
     private val onDialogCancel: () -> Unit = { lastChallengeDialog = null }
 
@@ -144,6 +155,7 @@
             })
         }
 
+        isLoading = false
         behavior = bindBehavior(behavior, findBehaviorClass(controlStatus, template, deviceType))
         updateContentDescription()
     }
@@ -189,11 +201,11 @@
         val previousText = status.getText()
 
         cancelUpdate = uiExecutor.executeDelayed({
-                status.setText(previousText)
-                updateContentDescription()
-            }, UPDATE_DELAY_IN_MILLIS)
+            setStatusText(previousText)
+            updateContentDescription()
+        }, UPDATE_DELAY_IN_MILLIS)
 
-        status.setText(tempStatus)
+        setStatusText(tempStatus)
         updateContentDescription()
     }
 
@@ -231,18 +243,50 @@
     }
 
     internal fun applyRenderInfo(enabled: Boolean, offset: Int, animated: Boolean = true) {
-        setEnabled(enabled)
-
         val ri = RenderInfo.lookup(context, cws.componentName, deviceType, enabled, offset)
-
         val fg = context.resources.getColorStateList(ri.foreground, context.theme)
+        val newText = nextStatusText
+        nextStatusText = ""
+        val control = cws.control
+
+        var shouldAnimate = animated
+        if (newText == status.text) {
+            shouldAnimate = false
+        }
+        animateStatusChange(shouldAnimate) {
+            updateStatusRow(enabled, newText, ri.icon, fg, control)
+        }
+
+        animateBackgroundChange(shouldAnimate, enabled, ri.enabledBackground)
+    }
+
+    fun getStatusText() = status.text
+
+    fun setStatusTextSize(textSize: Float) =
+        status.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
+
+    fun setStatusText(text: CharSequence, immediately: Boolean = false) {
+        if (immediately) {
+            status.alpha = STATUS_ALPHA_ENABLED
+            status.text = text
+            nextStatusText = ""
+        } else {
+            nextStatusText = text
+        }
+    }
+
+    private fun animateBackgroundChange(
+        animated: Boolean,
+        enabled: Boolean,
+        @ColorRes bgColor: Int
+    ) {
         val bg = context.resources.getColor(R.color.control_default_background, context.theme)
         var (newClipColor, newAlpha) = if (enabled) {
             // allow color overrides for the enabled state only
             val color = cws.control?.getCustomColor()?.let {
                 val state = intArrayOf(android.R.attr.state_enabled)
                 it.getColorForState(state, it.getDefaultColor())
-            } ?: context.resources.getColor(ri.enabledBackground, context.theme)
+            } ?: context.resources.getColor(bgColor, context.theme)
             listOf(color, ALPHA_ENABLED)
         } else {
             listOf(
@@ -251,21 +295,6 @@
             )
         }
 
-        status.setTextColor(fg)
-
-        cws.control?.getCustomIcon()?.let {
-            // do not tint custom icons, assume the intended icon color is correct
-            icon.imageTintList = null
-            icon.setImageIcon(it)
-        } ?: run {
-            icon.setImageDrawable(ri.icon)
-
-            // do not color app icons
-            if (deviceType != DeviceTypes.TYPE_ROUTINE) {
-                icon.imageTintList = fg
-            }
-        }
-
         (clipLayer.getDrawable() as GradientDrawable).apply {
             val newBaseColor = if (behavior is ToggleRangeBehavior) {
                 ColorUtils.blendARGB(bg, newClipColor, toggleBackgroundIntensity)
@@ -303,6 +332,77 @@
         }
     }
 
+    private fun animateStatusChange(animated: Boolean, statusRowUpdater: () -> Unit) {
+        statusAnimator?.cancel()
+
+        if (!animated) {
+            statusRowUpdater.invoke()
+            return
+        }
+
+        if (isLoading) {
+            statusRowUpdater.invoke()
+            statusAnimator = ObjectAnimator.ofFloat(status, "alpha", STATUS_ALPHA_DIMMED).apply {
+                repeatMode = ValueAnimator.REVERSE
+                repeatCount = ValueAnimator.INFINITE
+                duration = 500L
+                interpolator = Interpolators.LINEAR
+                startDelay = 900L
+                start()
+            }
+        } else {
+            val fadeOut = ObjectAnimator.ofFloat(status, "alpha", 0f).apply {
+                duration = 200L
+                interpolator = Interpolators.LINEAR
+                addListener(object : AnimatorListenerAdapter() {
+                    override fun onAnimationEnd(animation: Animator?) {
+                        statusRowUpdater.invoke()
+                    }
+                })
+            }
+            val fadeIn = ObjectAnimator.ofFloat(status, "alpha", STATUS_ALPHA_ENABLED).apply {
+                duration = 200L
+                interpolator = Interpolators.LINEAR
+            }
+            statusAnimator = AnimatorSet().apply {
+                playSequentially(fadeOut, fadeIn)
+                addListener(object : AnimatorListenerAdapter() {
+                    override fun onAnimationEnd(animation: Animator?) {
+                        status.alpha = STATUS_ALPHA_ENABLED
+                        statusAnimator = null
+                    }
+                })
+                start()
+            }
+        }
+    }
+
+    private fun updateStatusRow(
+        enabled: Boolean,
+        text: CharSequence,
+        drawable: Drawable,
+        color: ColorStateList,
+        control: Control?
+    ) {
+        setEnabled(enabled)
+
+        status.text = text
+        status.setTextColor(color)
+
+        control?.getCustomIcon()?.let {
+            // do not tint custom icons, assume the intended icon color is correct
+            icon.imageTintList = null
+            icon.setImageIcon(it)
+        } ?: run {
+            icon.setImageDrawable(drawable)
+
+            // do not color app icons
+            if (deviceType != DeviceTypes.TYPE_ROUTINE) {
+                icon.imageTintList = color
+            }
+        }
+    }
+
     private fun setEnabled(enabled: Boolean) {
         status.setEnabled(enabled)
         icon.setEnabled(enabled)
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/DefaultBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/DefaultBehavior.kt
index 722ade9..0c8e3ff 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/DefaultBehavior.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/DefaultBehavior.kt
@@ -24,7 +24,7 @@
     }
 
     override fun bind(cws: ControlWithState, colorOffset: Int) {
-        cvh.status.setText(cws.control?.getStatusText() ?: "")
+        cvh.setStatusText(cws.control?.getStatusText() ?: "")
         cvh.applyRenderInfo(false, colorOffset)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/RenderInfo.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/RenderInfo.kt
index 124df32..ba331f4 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/RenderInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/RenderInfo.kt
@@ -97,6 +97,8 @@
 private const val THERMOSTAT_RANGE = DeviceTypes.TYPE_THERMOSTAT * BUCKET_SIZE
 
 private val deviceColorMap = mapOf<Int, Pair<Int, Int>>(
+    (THERMOSTAT_RANGE + TemperatureControlTemplate.MODE_OFF) to
+        Pair(R.color.control_default_foreground, R.color.control_default_background),
     (THERMOSTAT_RANGE + TemperatureControlTemplate.MODE_HEAT) to
         Pair(R.color.thermo_heat_foreground, R.color.control_enabled_thermo_heat_background),
     (THERMOSTAT_RANGE + TemperatureControlTemplate.MODE_COOL) to
@@ -108,13 +110,9 @@
 }
 
 private val deviceIconMap = mapOf<Int, IconState>(
-    THERMOSTAT_RANGE to IconState(
-        R.drawable.ic_device_thermostat_off,
-        R.drawable.ic_device_thermostat_on
-    ),
     (THERMOSTAT_RANGE + TemperatureControlTemplate.MODE_OFF) to IconState(
         R.drawable.ic_device_thermostat_off,
-        R.drawable.ic_device_thermostat_on
+        R.drawable.ic_device_thermostat_off
     ),
     (THERMOSTAT_RANGE + TemperatureControlTemplate.MODE_HEAT) to IconState(
         R.drawable.ic_device_thermostat_off,
@@ -130,7 +128,7 @@
     ),
     (THERMOSTAT_RANGE + TemperatureControlTemplate.MODE_ECO) to IconState(
         R.drawable.ic_device_thermostat_off,
-        R.drawable.ic_device_thermostat_on
+        R.drawable.ic_device_thermostat_off
     ),
     DeviceTypes.TYPE_THERMOSTAT to IconState(
         R.drawable.ic_device_thermostat_off,
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt
index d8dceba..bf3835d 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt
@@ -32,9 +32,12 @@
         val msg = when (status) {
             Control.STATUS_ERROR -> R.string.controls_error_generic
             Control.STATUS_NOT_FOUND -> R.string.controls_error_removed
-            else -> com.android.internal.R.string.loading
+            else -> {
+                cvh.isLoading = true
+                com.android.internal.R.string.loading
+            }
         }
-        cvh.status.setText(cvh.context.getString(msg))
+        cvh.setStatusText(cvh.context.getString(msg))
         cvh.applyRenderInfo(false, colorOffset)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/TemperatureControlBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/TemperatureControlBehavior.kt
index 2795c7a..a7dc09b 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/TemperatureControlBehavior.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/TemperatureControlBehavior.kt
@@ -39,7 +39,7 @@
     override fun bind(cws: ControlWithState, colorOffset: Int) {
         this.control = cws.control!!
 
-        cvh.status.setText(control.getStatusText())
+        cvh.setStatusText(control.getStatusText())
 
         val ld = cvh.layout.getBackground() as LayerDrawable
         clipLayer = ld.findDrawableByLayerId(R.id.clip_layer)
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleBehavior.kt
index c432c09..dc7247c 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleBehavior.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleBehavior.kt
@@ -34,7 +34,6 @@
 
     override fun initialize(cvh: ControlViewHolder) {
         this.cvh = cvh
-        cvh.applyRenderInfo(false /* enabled */, 0 /* offset */, false /* animated */)
 
         cvh.layout.setOnClickListener(View.OnClickListener() {
             cvh.controlActionCoordinator.toggle(cvh, template.getTemplateId(), template.isChecked())
@@ -44,7 +43,7 @@
     override fun bind(cws: ControlWithState, colorOffset: Int) {
         this.control = cws.control!!
 
-        cvh.status.setText(control.getStatusText())
+        cvh.setStatusText(control.getStatusText())
         val controlTemplate = control.getControlTemplate()
         template = when (controlTemplate) {
             is ToggleTemplate -> controlTemplate
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt
index a09ed09..1f0ca9b 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt
@@ -31,7 +31,6 @@
 import android.service.controls.templates.ToggleRangeTemplate
 import android.util.Log
 import android.util.MathUtils
-import android.util.TypedValue
 import android.view.GestureDetector
 import android.view.GestureDetector.SimpleOnGestureListener
 import android.view.MotionEvent
@@ -39,7 +38,6 @@
 import android.view.ViewGroup
 import android.view.accessibility.AccessibilityEvent
 import android.view.accessibility.AccessibilityNodeInfo
-import android.widget.TextView
 import com.android.systemui.Interpolators
 import com.android.systemui.R
 import com.android.systemui.controls.ui.ControlViewHolder.Companion.MAX_LEVEL
@@ -57,7 +55,6 @@
     lateinit var control: Control
     lateinit var cvh: ControlViewHolder
     lateinit var rangeTemplate: RangeTemplate
-    lateinit var status: TextView
     lateinit var context: Context
     var currentStatusText: CharSequence = ""
     var currentRangeValue: String = ""
@@ -71,10 +68,7 @@
 
     override fun initialize(cvh: ControlViewHolder) {
         this.cvh = cvh
-        status = cvh.status
-        context = status.getContext()
-
-        cvh.applyRenderInfo(false /* enabled */, colorOffset, false /* animated */)
+        context = cvh.context
 
         val gestureListener = ToggleRangeGestureListener(cvh.layout)
         val gestureDetector = GestureDetector(context, gestureListener)
@@ -131,7 +125,6 @@
         this.colorOffset = colorOffset
 
         currentStatusText = control.getStatusText()
-        status.setText(currentStatusText)
 
         // ControlViewHolder sets a long click listener, but we want to handle touch in
         // here instead, otherwise we'll have state conflicts.
@@ -222,7 +215,7 @@
     }
 
     fun beginUpdateRange() {
-        status.setTextSize(TypedValue.COMPLEX_UNIT_PX, context.getResources()
+        cvh.setStatusTextSize(context.getResources()
                 .getDimensionPixelSize(R.dimen.control_status_expanded).toFloat())
     }
 
@@ -261,14 +254,13 @@
             val newValue = levelToRangeValue(newLevel)
             currentRangeValue = format(rangeTemplate.getFormatString().toString(),
                     DEFAULT_FORMAT, newValue)
-            val text = if (isDragging) {
-                currentRangeValue
+            if (isDragging) {
+                cvh.setStatusText(currentRangeValue, /* immediately */ true)
             } else {
-                "$currentStatusText $currentRangeValue"
+                cvh.setStatusText("$currentStatusText $currentRangeValue")
             }
-            status.setText(text)
         } else {
-            status.setText(currentStatusText)
+            cvh.setStatusText(currentStatusText)
         }
     }
 
@@ -296,9 +288,9 @@
     }
 
     fun endUpdateRange() {
-        status.setTextSize(TypedValue.COMPLEX_UNIT_PX, context.getResources()
+        cvh.setStatusTextSize(context.getResources()
                 .getDimensionPixelSize(R.dimen.control_status_normal).toFloat())
-        status.setText("$currentStatusText $currentRangeValue")
+        cvh.setStatusText("$currentStatusText $currentRangeValue", /* immediately */ true)
         cvh.action(FloatAction(rangeTemplate.getTemplateId(),
             findNearestStep(levelToRangeValue(clipLayer.getLevel()))))
     }
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/TouchBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/TouchBehavior.kt
index 8ce2e61..48f9458 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/TouchBehavior.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/TouchBehavior.kt
@@ -23,6 +23,7 @@
 import android.service.controls.templates.ControlTemplate
 
 import com.android.systemui.R
+import com.android.systemui.controls.ui.ControlViewHolder.Companion.MAX_LEVEL
 import com.android.systemui.controls.ui.ControlViewHolder.Companion.MIN_LEVEL
 
 /**
@@ -37,7 +38,6 @@
 
     override fun initialize(cvh: ControlViewHolder) {
         this.cvh = cvh
-        cvh.applyRenderInfo(false /* enabled */, 0 /* offset */, false /* animated */)
 
         cvh.layout.setOnClickListener(View.OnClickListener() {
             cvh.controlActionCoordinator.touch(cvh, template.getTemplateId(), control)
@@ -46,13 +46,14 @@
 
     override fun bind(cws: ControlWithState, colorOffset: Int) {
         this.control = cws.control!!
-        cvh.status.setText(control.getStatusText())
+        cvh.setStatusText(control.getStatusText())
         template = control.getControlTemplate()
 
         val ld = cvh.layout.getBackground() as LayerDrawable
         clipLayer = ld.findDrawableByLayerId(R.id.clip_layer)
-        clipLayer.setLevel(MIN_LEVEL)
 
-        cvh.applyRenderInfo(false, colorOffset)
+        val enabled = if (colorOffset > 0) true else false
+        clipLayer.setLevel(if (enabled) MAX_LEVEL else MIN_LEVEL)
+        cvh.applyRenderInfo(enabled, colorOffset)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
index 8e1854a..6274467 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
@@ -2221,7 +2221,7 @@
             mBackgroundDrawable.setAlpha(0);
             float xOffset = mGlobalActionsLayout.getAnimationOffsetX();
             ObjectAnimator alphaAnimator =
-                    ObjectAnimator.ofFloat(mContainer, "transitionAlpha", 0f, 1f);
+                    ObjectAnimator.ofFloat(mContainer, "alpha", 0f, 1f);
             alphaAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
             alphaAnimator.setDuration(183);
             alphaAnimator.addUpdateListener((animation) -> {
@@ -2234,8 +2234,8 @@
 
             ObjectAnimator xAnimator =
                     ObjectAnimator.ofFloat(mContainer, "translationX", xOffset, 0f);
-            alphaAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
-            alphaAnimator.setDuration(350);
+            xAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+            xAnimator.setDuration(350);
 
             AnimatorSet animatorSet = new AnimatorSet();
             animatorSet.playTogether(alphaAnimator, xAnimator);
@@ -2247,7 +2247,7 @@
             dismissWithAnimation(() -> {
                 mContainer.setTranslationX(0);
                 ObjectAnimator alphaAnimator =
-                        ObjectAnimator.ofFloat(mContainer, "transitionAlpha", 1f, 0f);
+                        ObjectAnimator.ofFloat(mContainer, "alpha", 1f, 0f);
                 alphaAnimator.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
                 alphaAnimator.setDuration(233);
                 alphaAnimator.addUpdateListener((animation) -> {
@@ -2261,8 +2261,8 @@
                 float xOffset = mGlobalActionsLayout.getAnimationOffsetX();
                 ObjectAnimator xAnimator =
                         ObjectAnimator.ofFloat(mContainer, "translationX", 0f, xOffset);
-                alphaAnimator.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
-                alphaAnimator.setDuration(350);
+                xAnimator.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
+                xAnimator.setDuration(350);
 
                 AnimatorSet animatorSet = new AnimatorSet();
                 animatorSet.playTogether(alphaAnimator, xAnimator);
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
index cf098d5..960c501 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
@@ -156,7 +156,7 @@
                 sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
 
                 // Remove notification
-                notificationManager.cancel(NOTIFICATION_RECORDING_ID);
+                notificationManager.cancel(NOTIFICATION_VIEW_ID);
 
                 startActivity(Intent.createChooser(shareIntent, shareLabel)
                         .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
@@ -175,7 +175,7 @@
                         Toast.LENGTH_LONG).show();
 
                 // Remove notification
-                notificationManager.cancel(NOTIFICATION_RECORDING_ID);
+                notificationManager.cancel(NOTIFICATION_VIEW_ID);
                 Log.d(TAG, "Deleted recording " + uri);
                 break;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java
index 9cfb1b2..839ea69 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java
@@ -56,6 +56,7 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.provider.DeviceConfig;
+import android.provider.Settings;
 import android.util.DisplayMetrics;
 import android.util.Log;
 import android.util.MathUtils;
@@ -160,6 +161,9 @@
     static final String EXTRA_CANCEL_NOTIFICATION = "android:screenshot_cancel_notification";
     static final String EXTRA_DISALLOW_ENTER_PIP = "android:screenshot_disallow_enter_pip";
 
+    // From WizardManagerHelper.java
+    private static final String SETTINGS_SECURE_USER_SETUP_COMPLETE = "user_setup_complete";
+
     private static final String TAG = "GlobalScreenshot";
 
     private static final long SCREENSHOT_FLASH_IN_DURATION_MS = 133;
@@ -460,6 +464,13 @@
             return;
         }
 
+        if (!isUserSetupComplete()) {
+            // User setup isn't complete, so we don't want to show any UI beyond a toast, as editing
+            // and sharing shouldn't be exposed to the user.
+            saveScreenshotAndToast(finisher);
+            return;
+        }
+
         // Optimizations
         mScreenBitmap.setHasAlpha(false);
         mScreenBitmap.prepareToDraw();
@@ -546,6 +557,41 @@
     }
 
     /**
+     * Save the bitmap but don't show the normal screenshot UI.. just a toast (or notification on
+     * failure).
+     */
+    private void saveScreenshotAndToast(Consumer<Uri> finisher) {
+        // Play the shutter sound to notify that we've taken a screenshot
+        mScreenshotHandler.post(() -> {
+            mCameraSound.play(MediaActionSound.SHUTTER_CLICK);
+        });
+
+        saveScreenshotInWorkerThread(finisher, new ActionsReadyListener() {
+            @Override
+            void onActionsReady(SavedImageData imageData) {
+                finisher.accept(imageData.uri);
+                if (imageData.uri == null) {
+                    mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED);
+                    mNotificationsController.notifyScreenshotError(
+                            R.string.screenshot_failed_to_capture_text);
+                } else {
+                    mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED);
+
+                    mScreenshotHandler.post(() -> {
+                        Toast.makeText(mContext, R.string.screenshot_saved_title,
+                                Toast.LENGTH_SHORT).show();
+                    });
+                }
+            }
+        });
+    }
+
+    private boolean isUserSetupComplete() {
+        return Settings.Secure.getInt(mContext.getContentResolver(),
+                SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
+    }
+
+    /**
      * Clears current screenshot
      */
     private void dismissScreenshot(String reason, boolean immediate) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt
new file mode 100644
index 0000000..7f7ff9cf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar
+
+import android.app.PendingIntent
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogLevel
+import com.android.systemui.log.dagger.NotifInteractionLog
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import javax.inject.Inject
+
+/**
+ * Logger class for events related to the user clicking on notification actions
+ */
+class ActionClickLogger @Inject constructor(
+    @NotifInteractionLog private val buffer: LogBuffer
+) {
+    fun logInitialClick(
+        entry: NotificationEntry?,
+        pendingIntent: PendingIntent
+    ) {
+        buffer.log(TAG, LogLevel.DEBUG, {
+            str1 = entry?.key
+            str2 = entry?.ranking?.channel?.id
+            str3 = pendingIntent.intent.toString()
+        }, {
+            "ACTION CLICK $str1 (channel=$str2) for pending intent $str3"
+        })
+    }
+
+    fun logRemoteInputWasHandled(
+        entry: NotificationEntry?
+    ) {
+        buffer.log(TAG, LogLevel.DEBUG, {
+            str1 = entry?.key
+        }, {
+            "  [Action click] Triggered remote input (for $str1))"
+        })
+    }
+
+    fun logStartingIntentWithDefaultHandler(
+        entry: NotificationEntry?,
+        pendingIntent: PendingIntent
+    ) {
+        buffer.log(TAG, LogLevel.DEBUG, {
+            str1 = entry?.key
+            str2 = pendingIntent.intent.toString()
+        }, {
+            "  [Action click] Launching intent $str2 via default handler (for $str1)"
+        })
+    }
+
+    fun logWaitingToCloseKeyguard(
+        pendingIntent: PendingIntent
+    ) {
+        buffer.log(TAG, LogLevel.DEBUG, {
+            str1 = pendingIntent.intent.toString()
+        }, {
+            "  [Action click] Intent $str1 launches an activity, dismissing keyguard first..."
+        })
+    }
+
+    fun logKeyguardGone(
+        pendingIntent: PendingIntent
+    ) {
+        buffer.log(TAG, LogLevel.DEBUG, {
+            str1 = pendingIntent.intent.toString()
+        }, {
+            "  [Action click] Keyguard dismissed, calling default handler for intent $str1"
+        })
+    }
+}
+
+private const val TAG = "ActionClickLogger"
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
index bf28040..9181c69 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
@@ -54,7 +54,7 @@
 import com.android.systemui.R;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.statusbar.dagger.StatusBarModule;
+import com.android.systemui.statusbar.dagger.StatusBarDependenciesModule;
 import com.android.systemui.statusbar.notification.NotificationEntryListener;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -114,6 +114,7 @@
     private final SmartReplyController mSmartReplyController;
     private final NotificationEntryManager mEntryManager;
     private final Handler mMainHandler;
+    private final ActionClickLogger mLogger;
 
     private final Lazy<StatusBar> mStatusBarLazy;
 
@@ -138,14 +139,18 @@
             mStatusBarLazy.get().wakeUpIfDozing(SystemClock.uptimeMillis(), view,
                     "NOTIFICATION_CLICK");
 
+            final NotificationEntry entry = getNotificationForParent(view.getParent());
+            mLogger.logInitialClick(entry, pendingIntent);
+
             if (handleRemoteInput(view, pendingIntent)) {
+                mLogger.logRemoteInputWasHandled(entry);
                 return true;
             }
 
             if (DEBUG) {
                 Log.v(TAG, "Notification click handler invoked for intent: " + pendingIntent);
             }
-            logActionClick(view, pendingIntent);
+            logActionClick(view, entry, pendingIntent);
             // The intent we are sending is for the application, which
             // won't have permission to immediately start an activity after
             // the user switches to home.  We know it is safe to do at this
@@ -158,11 +163,15 @@
                 Pair<Intent, ActivityOptions> options = response.getLaunchOptions(view);
                 options.second.setLaunchWindowingMode(
                         WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY);
+                mLogger.logStartingIntentWithDefaultHandler(entry, pendingIntent);
                 return RemoteViews.startPendingIntent(view, pendingIntent, options);
             });
         }
 
-        private void logActionClick(View view, PendingIntent actionIntent) {
+        private void logActionClick(
+                View view,
+                NotificationEntry entry,
+                PendingIntent actionIntent) {
             Integer actionIndex = (Integer)
                     view.getTag(com.android.internal.R.id.notification_action_index_tag);
             if (actionIndex == null) {
@@ -170,7 +179,7 @@
                 return;
             }
             ViewParent parent = view.getParent();
-            StatusBarNotification statusBarNotification = getNotificationForParent(parent);
+            StatusBarNotification statusBarNotification = entry.getSbn();
             if (statusBarNotification == null) {
                 Log.w(TAG, "Couldn't determine notification for click.");
                 return;
@@ -212,10 +221,10 @@
             }
         }
 
-        private StatusBarNotification getNotificationForParent(ViewParent parent) {
+        private NotificationEntry getNotificationForParent(ViewParent parent) {
             while (parent != null) {
                 if (parent instanceof ExpandableNotificationRow) {
-                    return ((ExpandableNotificationRow) parent).getEntry().getSbn();
+                    return ((ExpandableNotificationRow) parent).getEntry();
                 }
                 parent = parent.getParent();
             }
@@ -255,7 +264,7 @@
     };
 
     /**
-     * Injected constructor. See {@link StatusBarModule}.
+     * Injected constructor. See {@link StatusBarDependenciesModule}.
      */
     public NotificationRemoteInputManager(
             Context context,
@@ -265,13 +274,15 @@
             Lazy<StatusBar> statusBarLazy,
             StatusBarStateController statusBarStateController,
             @Main Handler mainHandler,
-            RemoteInputUriController remoteInputUriController) {
+            RemoteInputUriController remoteInputUriController,
+            ActionClickLogger logger) {
         mContext = context;
         mLockscreenUserManager = lockscreenUserManager;
         mSmartReplyController = smartReplyController;
         mEntryManager = notificationEntryManager;
         mStatusBarLazy = statusBarLazy;
         mMainHandler = mainHandler;
+        mLogger = logger;
         mBarService = IStatusBarService.Stub.asInterface(
                 ServiceManager.getService(Context.STATUS_BAR_SERVICE));
         mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
index 765f85a..3dda15b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
@@ -186,8 +186,7 @@
 
             boolean groupChangesAllowed =
                     mVisualStabilityManager.areGroupChangesAllowed() // user isn't looking at notifs
-                    || !ent.hasFinishedInitialization() // notif recently added
-                    || !mListContainer.containsView(ent.getRow()); // notif recently unfiltered
+                    || !ent.hasFinishedInitialization(); // notif recently added
 
             NotificationEntry parent = mGroupManager.getGroupSummary(ent.getSbn());
             if (!groupChangesAllowed) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
index f0fed131..b08eb9f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
@@ -25,6 +25,7 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.media.MediaDataManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.statusbar.ActionClickLogger;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.MediaArtworkProcessor;
 import com.android.systemui.statusbar.NotificationListener;
@@ -73,7 +74,8 @@
             Lazy<StatusBar> statusBarLazy,
             StatusBarStateController statusBarStateController,
             Handler mainHandler,
-            RemoteInputUriController remoteInputUriController) {
+            RemoteInputUriController remoteInputUriController,
+            ActionClickLogger actionClickLogger) {
         return new NotificationRemoteInputManager(
                 context,
                 lockscreenUserManager,
@@ -82,7 +84,8 @@
                 statusBarLazy,
                 statusBarStateController,
                 mainHandler,
-                remoteInputUriController);
+                remoteInputUriController,
+                actionClickLogger);
     }
 
     /** */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
index d647124..2be1531 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
@@ -151,6 +151,16 @@
     @Override
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         pw.println("NotificationEntryManager state:");
+        pw.println("  mAllNotifications=");
+        if (mAllNotifications.size() == 0) {
+            pw.println("null");
+        } else {
+            int i = 0;
+            for (NotificationEntry entry : mAllNotifications) {
+                dumpEntry(pw, "  ", i, entry);
+                i++;
+            }
+        }
         pw.print("  mPendingNotifications=");
         if (mPendingNotifications.size() == 0) {
             pw.println("null");
@@ -350,8 +360,8 @@
     private final NotificationHandler mNotifListener = new NotificationHandler() {
         @Override
         public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
-            final boolean isUpdate = mActiveNotifications.containsKey(sbn.getKey());
-            if (isUpdate) {
+            final boolean isUpdateToInflatedNotif = mActiveNotifications.containsKey(sbn.getKey());
+            if (isUpdateToInflatedNotif) {
                 updateNotification(sbn, rankingMap);
             } else {
                 addNotification(sbn, rankingMap);
@@ -442,16 +452,12 @@
                 }
                 if (!lifetimeExtended) {
                     // At this point, we are guaranteed the notification will be removed
+                    abortExistingInflation(key, "removeNotification");
                     mAllNotifications.remove(pendingEntry);
+                    mLeakDetector.trackGarbage(pendingEntry);
                 }
             }
-        }
-
-        if (!lifetimeExtended) {
-            abortExistingInflation(key, "removeNotification");
-        }
-
-        if (entry != null) {
+        } else {
             // If a manager needs to keep the notification around for whatever reason, we
             // keep the notification
             boolean entryDismissed = entry.isRowDismissed();
@@ -469,6 +475,8 @@
 
             if (!lifetimeExtended) {
                 // At this point, we are guaranteed the notification will be removed
+                abortExistingInflation(key, "removeNotification");
+                mAllNotifications.remove(entry);
 
                 // Ensure any managers keeping the lifetime extended stop managing the entry
                 cancelLifetimeExtension(entry);
@@ -477,13 +485,10 @@
                     entry.removeRow();
                 }
 
-                mAllNotifications.remove(entry);
-
                 // Let's remove the children if this was a summary
                 handleGroupSummaryRemoved(key);
                 removeVisibleNotification(key);
                 updateNotifications("removeNotificationInternal");
-                mLeakDetector.trackGarbage(entry);
                 removedByUser |= entryDismissed;
 
                 mLogger.logNotifRemoved(entry.getKey(), removedByUser);
@@ -497,6 +502,7 @@
                 for (NotifCollectionListener listener : mNotifCollectionListeners) {
                     listener.onEntryCleanUp(entry);
                 }
+                mLeakDetector.trackGarbage(entry);
             }
         }
     }
@@ -556,17 +562,24 @@
         Ranking ranking = new Ranking();
         rankingMap.getRanking(key, ranking);
 
-        NotificationEntry entry = new NotificationEntry(
-                notification,
-                ranking,
-                mFgsFeatureController.isForegroundServiceDismissalEnabled(),
-                SystemClock.uptimeMillis());
+        NotificationEntry entry = mPendingNotifications.get(key);
+        if (entry != null) {
+            entry.setSbn(notification);
+        } else {
+            entry = new NotificationEntry(
+                    notification,
+                    ranking,
+                    mFgsFeatureController.isForegroundServiceDismissalEnabled(),
+                    SystemClock.uptimeMillis());
+            mAllNotifications.add(entry);
+            mLeakDetector.trackInstance(entry);
+        }
+
+        abortExistingInflation(key, "addNotification");
+
         for (NotifCollectionListener listener : mNotifCollectionListeners) {
             listener.onEntryBind(entry, notification);
         }
-        mAllNotifications.add(entry);
-
-        mLeakDetector.trackInstance(entry);
 
         for (NotifCollectionListener listener : mNotifCollectionListeners) {
             listener.onEntryInit(entry);
@@ -581,7 +594,6 @@
                             mInflationCallback);
         }
 
-        abortExistingInflation(key, "addNotification");
         mPendingNotifications.put(key, entry);
         mLogger.logNotifAdded(entry.getKey());
         for (NotificationEntryListener listener : mNotificationEntryListeners) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index 22ac1a2..1eadd9e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -151,6 +151,8 @@
     public CharSequence headsUpStatusBarText;
     public CharSequence headsUpStatusBarTextPublic;
 
+    // indicates when this entry's view was first attached to a window
+    // this value will reset when the view is completely removed from the shade (ie: filtered out)
     private long initializationTime = -1;
 
     /**
@@ -473,8 +475,8 @@
     }
 
     public boolean hasFinishedInitialization() {
-        return initializationTime == -1
-                || SystemClock.elapsedRealtime() > initializationTime + INITIALIZATION_DELAY;
+        return initializationTime != -1
+                && SystemClock.elapsedRealtime() > initializationTime + INITIALIZATION_DELAY;
     }
 
     public int getContrastedColor(Context context, boolean isLowPriority,
@@ -565,6 +567,10 @@
         return false;
     }
 
+    public void resetInitializationTime() {
+        initializationTime = -1;
+    }
+
     public void setInitializationTime(long time) {
         if (initializationTime == -1) {
             initializationTime = time;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManager.kt
index cbf680c..dc0b802 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManager.kt
@@ -134,13 +134,22 @@
     ): List<NotificationEntry> {
         logger.logFilterAndSort(reason)
         val filtered = entries.asSequence()
-                .filterNot(notifFilter::shouldFilterOut)
+                .filterNot(this::filter)
                 .sortedWith(rankingComparator)
                 .toList()
         entries.forEach { it.bucket = getBucketForEntry(it) }
         return filtered
     }
 
+    private fun filter(entry: NotificationEntry): Boolean {
+        val filtered = notifFilter.shouldFilterOut(entry)
+        if (filtered) {
+            // notification is removed from the list, so we reset its initialization time
+            entry.resetInitializationTime()
+        }
+        return filtered
+    }
+
     @PriorityBucket
     private fun getBucketForEntry(entry: NotificationEntry): Int {
         val isHeadsUp = entry.isRowHeadsUp
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
index 0a3b02c..4cec383 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
@@ -713,6 +713,10 @@
     private boolean applyFilters(NotificationEntry entry, long now, List<NotifFilter> filters) {
         final NotifFilter filter = findRejectingFilter(entry, now, filters);
         entry.getAttachState().setExcludingFilter(filter);
+        if (filter != null) {
+            // notification is removed from the list, so we reset its initialization time
+            entry.resetInitializationTime();
+        }
         return filter != null;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BubbleCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BubbleCoordinator.java
index 57be1a5..92426e5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BubbleCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BubbleCoordinator.java
@@ -19,8 +19,6 @@
 import static android.service.notification.NotificationStats.DISMISSAL_OTHER;
 import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL;
 
-import android.annotation.NonNull;
-
 import com.android.internal.statusbar.NotificationVisibility;
 import com.android.systemui.bubbles.BubbleController;
 import com.android.systemui.statusbar.notification.collection.NotifCollection;
@@ -123,7 +121,7 @@
     private final BubbleController.NotifCallback mNotifCallback =
             new BubbleController.NotifCallback() {
         @Override
-        public void removeNotification(@NonNull final NotificationEntry entry, int reason) {
+        public void removeNotification(NotificationEntry entry, int reason) {
             if (isInterceptingDismissal(entry)) {
                 mInterceptedDismissalEntries.remove(entry.getKey());
                 mOnEndDismissInterception.onEndDismissInterception(mDismissInterceptor, entry,
@@ -143,7 +141,7 @@
         }
 
         @Override
-        public void maybeCancelSummary(@NonNull final NotificationEntry entry) {
+        public void maybeCancelSummary(NotificationEntry entry) {
             // no-op
         }
     };
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java
index d0a872e..80785db 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java
@@ -16,7 +16,6 @@
 
 package com.android.systemui.statusbar.phone;
 
-import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.service.notification.StatusBarNotification;
 import android.util.ArraySet;
@@ -455,7 +454,7 @@
      * If there is a {@link NotificationGroup} associated with the provided entry, this method
      * will update the suppression of that group.
      */
-    public void updateSuppression(@NonNull final NotificationEntry entry) {
+    public void updateSuppression(NotificationEntry entry) {
         NotificationGroup group = mGroupMap.get(getGroupKey(entry.getSbn()));
         if (group != null) {
             updateSuppression(group);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowController.java
index 926c1bd..2e4a929d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowController.java
@@ -321,7 +321,8 @@
                 || state.mPanelVisible || state.mKeyguardFadingAway || state.mBouncerShowing
                 || state.mHeadsUpShowing
                 || state.mScrimsVisibility != ScrimController.TRANSPARENT)
-                || state.mBackgroundBlurRadius > 0;
+                || state.mBackgroundBlurRadius > 0
+                || state.mLaunchingActivity;
     }
 
     private void applyFitsSystemWindows(State state) {
@@ -485,6 +486,11 @@
         apply(mCurrentState);
     }
 
+    void setLaunchingActivity(boolean launching) {
+        mCurrentState.mLaunchingActivity = launching;
+        apply(mCurrentState);
+    }
+
     public void setScrimsVisibility(int scrimsVisibility) {
         mCurrentState.mScrimsVisibility = scrimsVisibility;
         apply(mCurrentState);
@@ -645,6 +651,7 @@
         boolean mForceCollapsed;
         boolean mForceDozeBrightness;
         boolean mForceUserActivity;
+        boolean mLaunchingActivity;
         boolean mBackdropShowing;
         boolean mWallpaperSupportsAmbientMode;
         boolean mNotTouchable;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
index 596a607..42222d7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
@@ -93,6 +93,7 @@
     private PhoneStatusBarView mStatusBarView;
     private PhoneStatusBarTransitions mBarTransitions;
     private StatusBar mService;
+    private NotificationShadeWindowController mNotificationShadeWindowController;
     private DragDownHelper mDragDownHelper;
     private boolean mDoubleTapEnabled;
     private boolean mSingleTapEnabled;
@@ -429,11 +430,19 @@
     }
 
     public void setExpandAnimationPending(boolean pending) {
-        mExpandAnimationPending = pending;
+        if (mExpandAnimationPending != pending) {
+            mExpandAnimationPending = pending;
+            mNotificationShadeWindowController
+                    .setLaunchingActivity(mExpandAnimationPending | mExpandAnimationRunning);
+        }
     }
 
     public void setExpandAnimationRunning(boolean running) {
-        mExpandAnimationRunning = running;
+        if (mExpandAnimationRunning != running) {
+            mExpandAnimationRunning = running;
+            mNotificationShadeWindowController
+                    .setLaunchingActivity(mExpandAnimationPending | mExpandAnimationRunning);
+        }
     }
 
     public void cancelExpandHelper() {
@@ -456,8 +465,9 @@
         }
     }
 
-    public void setService(StatusBar statusBar) {
+    public void setService(StatusBar statusBar, NotificationShadeWindowController controller) {
         mService = statusBar;
+        mNotificationShadeWindowController = controller;
     }
 
     @VisibleForTesting
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index 3fa530a..19de191 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -1001,7 +1001,7 @@
         updateTheme();
 
         inflateStatusBarWindow();
-        mNotificationShadeWindowViewController.setService(this);
+        mNotificationShadeWindowViewController.setService(this, mNotificationShadeWindowController);
         mNotificationShadeWindowView.setOnTouchListener(getStatusBarWindowTouchListener());
 
         // TODO: Deal with the ugliness that comes from having some of the statusbar broken out
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
index 44ece35..7924348 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -429,7 +429,9 @@
             }
             int launchResult = intent.sendAndReturnResult(mContext, 0, fillInIntent, null,
                     null, null, getActivityOptions(adapter));
-            mActivityLaunchAnimator.setLaunchResult(launchResult, isActivityIntent);
+            mMainThreadHandler.post(() -> {
+                mActivityLaunchAnimator.setLaunchResult(launchResult, isActivityIntent);
+            });
         } catch (RemoteException | PendingIntent.CanceledException e) {
             // the stack trace isn't very helpful here.
             // Just log the exception message.
@@ -449,10 +451,11 @@
                                 mActivityLaunchAnimator.getLaunchAnimation(
                                         row, mStatusBar.isOccluded())),
                                 new UserHandle(UserHandle.getUserId(appUid)));
-                mActivityLaunchAnimator.setLaunchResult(launchResult, true /* isActivityIntent */);
 
                 // Putting it back on the main thread, since we're touching views
                 mMainThreadHandler.post(() -> {
+                    mActivityLaunchAnimator.setLaunchResult(launchResult,
+                            true /* isActivityIntent */);
                     removeHUN(row);
                 });
                 if (shouldCollapse()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
index 428de9e..669e6a4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
@@ -37,6 +37,7 @@
 import com.android.systemui.ActivityIntentHelper;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.statusbar.ActionClickLogger;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.CommandQueue.Callbacks;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
@@ -73,6 +74,7 @@
     private View mPendingRemoteInputView;
     private KeyguardManager mKeyguardManager;
     private final CommandQueue mCommandQueue;
+    private final ActionClickLogger mActionClickLogger;
     private int mDisabled2;
     protected BroadcastReceiver mChallengeReceiver = new ChallengeReceiver();
     private Handler mMainHandler = new Handler();
@@ -87,7 +89,8 @@
             StatusBarStateController statusBarStateController,
             StatusBarKeyguardViewManager statusBarKeyguardViewManager,
             ActivityStarter activityStarter, ShadeController shadeController,
-            CommandQueue commandQueue) {
+            CommandQueue commandQueue,
+            ActionClickLogger clickLogger) {
         mContext = context;
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
         mShadeController = shadeController;
@@ -101,6 +104,7 @@
         mKeyguardManager = context.getSystemService(KeyguardManager.class);
         mCommandQueue = commandQueue;
         mCommandQueue.addCallback(this);
+        mActionClickLogger = clickLogger;
         mActivityIntentHelper = new ActivityIntentHelper(mContext);
         mGroupManager = groupManager;
         // Listen to onKeyguardShowingChanged in case a managed profile needs to be unlocked
@@ -304,9 +308,12 @@
             NotificationRemoteInputManager.ClickHandler defaultHandler) {
         final boolean isActivity = pendingIntent.isActivity();
         if (isActivity) {
+            mActionClickLogger.logWaitingToCloseKeyguard(pendingIntent);
             final boolean afterKeyguardGone = mActivityIntentHelper.wouldLaunchResolverActivity(
                     pendingIntent.getIntent(), mLockscreenUserManager.getCurrentUserId());
             mActivityStarter.dismissKeyguardThenExecute(() -> {
+                mActionClickLogger.logKeyguardGone(pendingIntent);
+
                 try {
                     ActivityManager.getService().resumeAppSwitches();
                 } catch (RemoteException e) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
index 52923ab..96e868d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
@@ -317,7 +317,7 @@
         verify(mNotificationEntryManager).updateNotifications(any());
 
         mBubbleController.removeBubble(
-                mRow.getEntry().getKey(), BubbleController.DISMISS_USER_GESTURE);
+                mRow.getEntry(), BubbleController.DISMISS_USER_GESTURE);
         assertNull(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()));
         verify(mNotificationEntryManager, times(2)).updateNotifications(anyString());
 
@@ -329,7 +329,7 @@
         mBubbleController.updateBubble(mRow2.getEntry());
         mBubbleController.updateBubble(mRow.getEntry());
         mBubbleController.removeBubble(
-                mRow.getEntry().getKey(), BubbleController.DISMISS_USER_GESTURE);
+                mRow.getEntry(), BubbleController.DISMISS_USER_GESTURE);
 
         Bubble b = mBubbleData.getOverflowBubbleWithKey(mRow.getEntry().getKey());
         assertThat(mBubbleData.getOverflowBubbles()).isEqualTo(ImmutableList.of(b));
@@ -350,10 +350,9 @@
         mBubbleController.updateBubble(mRow.getEntry(), /* suppressFlyout */
                 false, /* showInShade */ true);
         mBubbleController.removeBubble(
-                mRow.getEntry().getKey(), BubbleController.DISMISS_USER_GESTURE);
+                mRow.getEntry(), BubbleController.DISMISS_USER_GESTURE);
 
-        mBubbleController.removeBubble(
-                mRow.getEntry().getKey(), BubbleController.DISMISS_NOTIF_CANCEL);
+        mBubbleController.removeBubble(mRow.getEntry(), BubbleController.DISMISS_NOTIF_CANCEL);
         verify(mNotificationEntryManager, times(1)).performRemoveNotification(
                 eq(mRow.getEntry().getSbn()), anyInt());
         assertThat(mBubbleData.getOverflowBubbles()).isEmpty();
@@ -366,7 +365,7 @@
         assertTrue(mBubbleController.hasBubbles());
 
         mBubbleController.removeBubble(
-                mRow.getEntry().getKey(), BubbleController.DISMISS_USER_CHANGED);
+                mRow.getEntry(), BubbleController.DISMISS_USER_CHANGED);
         verify(mNotificationEntryManager, never()).performRemoveNotification(
                 eq(mRow.getEntry().getSbn()), anyInt());
         assertFalse(mBubbleController.hasBubbles());
@@ -564,8 +563,7 @@
 
         // Dismiss currently expanded
         mBubbleController.removeBubble(
-                mBubbleData.getBubbleInStackWithKey(stackView.getExpandedBubble().getKey())
-                        .getEntry().getKey(),
+                mBubbleData.getBubbleInStackWithKey(stackView.getExpandedBubble().getKey()).getEntry(),
                 BubbleController.DISMISS_USER_GESTURE);
         verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow2.getEntry().getKey());
 
@@ -576,8 +574,7 @@
 
         // Dismiss that one
         mBubbleController.removeBubble(
-                mBubbleData.getBubbleInStackWithKey(stackView.getExpandedBubble().getKey())
-                        .getEntry().getKey(),
+                mBubbleData.getBubbleInStackWithKey(stackView.getExpandedBubble().getKey()).getEntry(),
                 BubbleController.DISMISS_USER_GESTURE);
 
         // Make sure state changes and collapse happens
@@ -703,7 +700,7 @@
     @Test
     public void testDeleteIntent_removeBubble_aged() throws PendingIntent.CanceledException {
         mBubbleController.updateBubble(mRow.getEntry());
-        mBubbleController.removeBubble(mRow.getEntry().getKey(), BubbleController.DISMISS_AGED);
+        mBubbleController.removeBubble(mRow.getEntry(), BubbleController.DISMISS_AGED);
         verify(mDeleteIntent, never()).send();
     }
 
@@ -711,7 +708,7 @@
     public void testDeleteIntent_removeBubble_user() throws PendingIntent.CanceledException {
         mBubbleController.updateBubble(mRow.getEntry());
         mBubbleController.removeBubble(
-                mRow.getEntry().getKey(), BubbleController.DISMISS_USER_GESTURE);
+                mRow.getEntry(), BubbleController.DISMISS_USER_GESTURE);
         verify(mDeleteIntent, times(1)).send();
     }
 
@@ -811,7 +808,7 @@
 
         // Dismiss the bubble into overflow.
         mBubbleController.removeBubble(
-                mRow.getEntry().getKey(), BubbleController.DISMISS_USER_GESTURE);
+                mRow.getEntry(), BubbleController.DISMISS_USER_GESTURE);
         assertFalse(mBubbleController.hasBubbles());
 
         boolean intercepted = mRemoveInterceptor.onNotificationRemoveRequested(
@@ -832,7 +829,7 @@
                 mRow.getEntry()));
 
         mBubbleController.removeBubble(
-                mRow.getEntry().getKey(), BubbleController.DISMISS_NO_LONGER_BUBBLE);
+                mRow.getEntry(), BubbleController.DISMISS_NO_LONGER_BUBBLE);
         assertFalse(mBubbleController.hasBubbles());
 
         boolean intercepted = mRemoveInterceptor.onNotificationRemoveRequested(
@@ -854,12 +851,12 @@
 
         mBubbleData.setMaxOverflowBubbles(1);
         mBubbleController.removeBubble(
-                mRow.getEntry().getKey(), BubbleController.DISMISS_USER_GESTURE);
+                mRow.getEntry(), BubbleController.DISMISS_USER_GESTURE);
         assertEquals(mBubbleData.getBubbles().size(), 2);
         assertEquals(mBubbleData.getOverflowBubbles().size(), 1);
 
         mBubbleController.removeBubble(
-                mRow2.getEntry().getKey(), BubbleController.DISMISS_USER_GESTURE);
+                mRow2.getEntry(), BubbleController.DISMISS_USER_GESTURE);
         // Overflow max of 1 is reached; mRow is oldest, so it gets removed
         verify(mNotificationEntryManager, times(1)).performRemoveNotification(
                 mRow.getEntry().getSbn(), REASON_CANCEL);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
index 882504b..66f119a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
@@ -20,8 +20,11 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
@@ -177,8 +180,7 @@
         mBubbleData.setListener(mListener);
 
         // Test
-        mBubbleData.notificationEntryRemoved(
-                mEntryA1.getKey(), BubbleController.DISMISS_USER_GESTURE);
+        mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_USER_GESTURE);
 
         // Verify
         verifyUpdateReceived();
@@ -295,14 +297,12 @@
         mBubbleData.setListener(mListener);
 
         mBubbleData.setMaxOverflowBubbles(1);
-        mBubbleData.notificationEntryRemoved(
-                mEntryA1.getKey(), BubbleController.DISMISS_USER_GESTURE);
+        mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_USER_GESTURE);
         verifyUpdateReceived();
         assertOverflowChangedTo(ImmutableList.of(mBubbleA1));
 
         // Overflow max of 1 is reached; A1 is oldest, so it gets removed
-        mBubbleData.notificationEntryRemoved(
-                mEntryA2.getKey(), BubbleController.DISMISS_USER_GESTURE);
+        mBubbleData.notificationEntryRemoved(mEntryA2, BubbleController.DISMISS_USER_GESTURE);
         verifyUpdateReceived();
         assertOverflowChangedTo(ImmutableList.of(mBubbleA2));
     }
@@ -449,8 +449,7 @@
         mBubbleData.setListener(mListener);
 
         // Test
-        mBubbleData.notificationEntryRemoved(
-                mEntryA2.getKey(), BubbleController.DISMISS_USER_GESTURE);
+        mBubbleData.notificationEntryRemoved(mEntryA2, BubbleController.DISMISS_USER_GESTURE);
         verifyUpdateReceived();
         assertOrderChangedTo(mBubbleB2, mBubbleB1, mBubbleA1);
     }
@@ -470,8 +469,7 @@
         mBubbleData.setListener(mListener);
 
         // Test
-        mBubbleData.notificationEntryRemoved(
-                mEntryB1.getKey(), BubbleController.DISMISS_USER_GESTURE);
+        mBubbleData.notificationEntryRemoved(mEntryB1, BubbleController.DISMISS_USER_GESTURE);
         verifyUpdateReceived();
         assertOrderNotChanged();
     }
@@ -491,8 +489,7 @@
         mBubbleData.setListener(mListener);
 
         // Test
-        mBubbleData.notificationEntryRemoved(
-                mEntryA1.getKey(), BubbleController.DISMISS_NOTIF_CANCEL);
+        mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_NOTIF_CANCEL);
         verifyUpdateReceived();
         assertOrderChangedTo(mBubbleB2, mBubbleB1, mBubbleA2);
     }
@@ -513,14 +510,12 @@
         mBubbleData.setListener(mListener);
 
         // Test
-        mBubbleData.notificationEntryRemoved(
-                mEntryA1.getKey(), BubbleController.DISMISS_NOTIF_CANCEL);
+        mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_NOTIF_CANCEL);
         verifyUpdateReceived();
         assertOverflowChangedTo(ImmutableList.of(mBubbleA2));
 
         // Test
-        mBubbleData.notificationEntryRemoved(
-                mEntryA2.getKey(), BubbleController.DISMISS_GROUP_CANCELLED);
+        mBubbleData.notificationEntryRemoved(mEntryA2, BubbleController.DISMISS_GROUP_CANCELLED);
         verifyUpdateReceived();
         assertOverflowChangedTo(ImmutableList.of());
     }
@@ -539,8 +534,7 @@
         mBubbleData.setListener(mListener);
 
         // Test
-        mBubbleData.notificationEntryRemoved(
-                mEntryA2.getKey(), BubbleController.DISMISS_NOTIF_CANCEL);
+        mBubbleData.notificationEntryRemoved(mEntryA2, BubbleController.DISMISS_NOTIF_CANCEL);
         verifyUpdateReceived();
         assertSelectionChangedTo(mBubbleB2);
     }
@@ -631,8 +625,7 @@
         mBubbleData.setListener(mListener);
 
         // Test
-        mBubbleData.notificationEntryRemoved(
-                mEntryA1.getKey(), BubbleController.DISMISS_USER_GESTURE);
+        mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_USER_GESTURE);
 
         // Verify the selection was cleared.
         verifyUpdateReceived();
@@ -786,8 +779,7 @@
         mBubbleData.setListener(mListener);
 
         // Test
-        mBubbleData.notificationEntryRemoved(
-                mEntryB2.getKey(), BubbleController.DISMISS_USER_GESTURE);
+        mBubbleData.notificationEntryRemoved(mEntryB2, BubbleController.DISMISS_USER_GESTURE);
         verifyUpdateReceived();
         assertOrderChangedTo(mBubbleB1, mBubbleA2, mBubbleA1);
     }
@@ -812,13 +804,11 @@
         mBubbleData.setListener(mListener);
 
         // Test
-        mBubbleData.notificationEntryRemoved(
-                mEntryA2.getKey(), BubbleController.DISMISS_USER_GESTURE);
+        mBubbleData.notificationEntryRemoved(mEntryA2, BubbleController.DISMISS_USER_GESTURE);
         verifyUpdateReceived();
         assertSelectionChangedTo(mBubbleA1);
 
-        mBubbleData.notificationEntryRemoved(
-                mEntryA1.getKey(), BubbleController.DISMISS_USER_GESTURE);
+        mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_USER_GESTURE);
         verifyUpdateReceived();
         assertSelectionChangedTo(mBubbleB1);
     }
@@ -933,8 +923,7 @@
         mBubbleData.setListener(mListener);
 
         // Test
-        mBubbleData.notificationEntryRemoved(
-                mEntryA1.getKey(), BubbleController.DISMISS_USER_GESTURE);
+        mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_USER_GESTURE);
         verifyUpdateReceived();
         assertExpandedChangedTo(false);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java
index c16801c..73b8760 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java
@@ -285,8 +285,7 @@
         assertTrue(mBubbleController.hasBubbles());
         verify(mNotifCallback, times(1)).invalidateNotifications(anyString());
 
-        mBubbleController.removeBubble(
-                mRow.getEntry().getKey(), BubbleController.DISMISS_USER_GESTURE);
+        mBubbleController.removeBubble(mRow.getEntry(), BubbleController.DISMISS_USER_GESTURE);
         assertNull(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()));
         verify(mNotifCallback, times(2)).invalidateNotifications(anyString());
     }
@@ -303,8 +302,7 @@
         mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()).setSuppressNotification(true);
 
         // Now remove the bubble
-        mBubbleController.removeBubble(
-                mRow.getEntry().getKey(), BubbleController.DISMISS_USER_GESTURE);
+        mBubbleController.removeBubble(mRow.getEntry(), BubbleController.DISMISS_USER_GESTURE);
         assertTrue(mBubbleData.hasOverflowBubbleWithKey(mRow.getEntry().getKey()));
 
         // We don't remove the notification since the bubble is still in overflow.
@@ -324,8 +322,7 @@
         mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()).setSuppressNotification(true);
 
         // Now remove the bubble
-        mBubbleController.removeBubble(
-                mRow.getEntry().getKey(), BubbleController.DISMISS_NOTIF_CANCEL);
+        mBubbleController.removeBubble(mRow.getEntry(), BubbleController.DISMISS_NOTIF_CANCEL);
         assertFalse(mBubbleData.hasOverflowBubbleWithKey(mRow.getEntry().getKey()));
 
         // Since the notif is dismissed and not in overflow, once the bubble is removed,
@@ -505,8 +502,7 @@
 
         // Dismiss currently expanded
         mBubbleController.removeBubble(
-                mBubbleData.getBubbleInStackWithKey(
-                        stackView.getExpandedBubble().getKey()).getEntry().getKey(),
+                mBubbleData.getBubbleInStackWithKey(stackView.getExpandedBubble().getKey()).getEntry(),
                 BubbleController.DISMISS_USER_GESTURE);
         verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow2.getEntry().getKey());
 
@@ -517,8 +513,7 @@
 
         // Dismiss that one
         mBubbleController.removeBubble(
-                mBubbleData.getBubbleInStackWithKey(
-                        stackView.getExpandedBubble().getKey()).getEntry().getKey(),
+                mBubbleData.getBubbleInStackWithKey(stackView.getExpandedBubble().getKey()).getEntry(),
                 BubbleController.DISMISS_USER_GESTURE);
 
         // Make sure state changes and collapse happens
@@ -618,7 +613,7 @@
     @Test
     public void testDeleteIntent_removeBubble_aged() throws PendingIntent.CanceledException {
         mBubbleController.updateBubble(mRow.getEntry());
-        mBubbleController.removeBubble(mRow.getEntry().getKey(), BubbleController.DISMISS_AGED);
+        mBubbleController.removeBubble(mRow.getEntry(), BubbleController.DISMISS_AGED);
         verify(mDeleteIntent, never()).send();
     }
 
@@ -626,7 +621,7 @@
     public void testDeleteIntent_removeBubble_user() throws PendingIntent.CanceledException {
         mBubbleController.updateBubble(mRow.getEntry());
         mBubbleController.removeBubble(
-                mRow.getEntry().getKey(), BubbleController.DISMISS_USER_GESTURE);
+                mRow.getEntry(), BubbleController.DISMISS_USER_GESTURE);
         verify(mDeleteIntent, times(1)).send();
     }
 
@@ -691,7 +686,7 @@
 
         // Dismiss the bubble
         mBubbleController.removeBubble(
-                mRow.getEntry().getKey(), BubbleController.DISMISS_USER_GESTURE);
+                mRow.getEntry(), BubbleController.DISMISS_USER_GESTURE);
         assertFalse(mBubbleController.hasBubbles());
 
         // Dismiss the notification
@@ -712,7 +707,7 @@
 
         // Dismiss the bubble
         mBubbleController.removeBubble(
-                mRow.getEntry().getKey(), BubbleController.DISMISS_NOTIF_CANCEL);
+                mRow.getEntry(), BubbleController.DISMISS_NOTIF_CANCEL);
         assertFalse(mBubbleController.hasBubbles());
 
         // Dismiss the notification
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
index 1117646..5a7dea4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
@@ -82,7 +82,8 @@
                 () -> mock(StatusBar.class),
                 mStateController,
                 Handler.createAsync(Looper.myLooper()),
-                mRemoteInputUriController);
+                mRemoteInputUriController,
+                mock(ActionClickLogger.class));
         mEntry = new NotificationEntryBuilder()
                 .setPkg(TEST_PACKAGE_NAME)
                 .setOpPkg(TEST_PACKAGE_NAME)
@@ -256,17 +257,26 @@
 
     private class TestableNotificationRemoteInputManager extends NotificationRemoteInputManager {
 
-        TestableNotificationRemoteInputManager(Context context,
+        TestableNotificationRemoteInputManager(
+                Context context,
                 NotificationLockscreenUserManager lockscreenUserManager,
                 SmartReplyController smartReplyController,
                 NotificationEntryManager notificationEntryManager,
                 Lazy<StatusBar> statusBarLazy,
                 StatusBarStateController statusBarStateController,
                 Handler mainHandler,
-                RemoteInputUriController remoteInputUriController) {
-            super(context, lockscreenUserManager, smartReplyController, notificationEntryManager,
-                    statusBarLazy, statusBarStateController, mainHandler,
-                    remoteInputUriController);
+                RemoteInputUriController remoteInputUriController,
+                ActionClickLogger actionClickLogger) {
+            super(
+                    context,
+                    lockscreenUserManager,
+                    smartReplyController,
+                    notificationEntryManager,
+                    statusBarLazy,
+                    statusBarStateController,
+                    mainHandler,
+                    remoteInputUriController,
+                    actionClickLogger);
         }
 
         public void setUpWithPresenterForTest(Callback callback,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/SmartReplyControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/SmartReplyControllerTest.java
index 22dc080..79507e9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/SmartReplyControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/SmartReplyControllerTest.java
@@ -92,7 +92,8 @@
                 mNotificationEntryManager, () -> mock(StatusBar.class),
                 mStatusBarStateController,
                 Handler.createAsync(Looper.myLooper()),
-                mRemoteInputUriController);
+                mRemoteInputUriController,
+                mock(ActionClickLogger.class));
         mRemoteInputManager.setUpWithCallback(mCallback, mDelegate);
         mNotification = new Notification.Builder(mContext, "")
                 .setSmallIcon(R.drawable.ic_person)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java
index d583048..a5a5f81 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java
@@ -210,6 +210,28 @@
     }
 
     @Test
+    public void testAddNotification_noDuplicateEntriesCreated() {
+        // GIVEN a notification has been added
+        mEntryManager.addNotification(mSbn, mRankingMap);
+
+        // WHEN the same notification is added multiple times before the previous entry (with
+        // the same key) didn't finish inflating
+        mEntryManager.addNotification(mSbn, mRankingMap);
+        mEntryManager.addNotification(mSbn, mRankingMap);
+        mEntryManager.addNotification(mSbn, mRankingMap);
+
+        // THEN getAllNotifs() only contains exactly one notification with this key
+        int count = 0;
+        for (NotificationEntry entry : mEntryManager.getAllNotifs()) {
+            if (entry.getKey().equals(mSbn.getKey())) {
+                count++;
+            }
+        }
+        assertEquals("Should only be one entry with key=" + mSbn.getKey() + " in mAllNotifs. "
+                        + "Instead there are " + count, 1, count);
+    }
+
+    @Test
     public void testAddNotification_setsUserSentiment() {
         mEntryManager.addNotification(mSbn, mRankingMap);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManagerTest.kt
index a83de13..9f47f4a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManagerTest.kt
@@ -21,6 +21,7 @@
 import android.app.NotificationManager.IMPORTANCE_DEFAULT
 import android.app.NotificationManager.IMPORTANCE_HIGH
 import android.app.NotificationManager.IMPORTANCE_LOW
+import android.os.SystemClock
 import android.service.notification.NotificationListenerService.RankingMap
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
@@ -58,17 +59,19 @@
     private lateinit var personNotificationIdentifier: PeopleNotificationIdentifier
     private lateinit var rankingManager: TestableNotificationRankingManager
     private lateinit var sectionsManager: NotificationSectionsFeatureManager
+    private lateinit var notificationFilter: NotificationFilter
 
     @Before
     fun setup() {
         personNotificationIdentifier =
                 mock(PeopleNotificationIdentifier::class.java)
         sectionsManager = mock(NotificationSectionsFeatureManager::class.java)
+        notificationFilter = mock(NotificationFilter::class.java)
         rankingManager = TestableNotificationRankingManager(
                 lazyMedia,
                 mock(NotificationGroupManager::class.java),
                 mock(HeadsUpManager::class.java),
-                mock(NotificationFilter::class.java),
+                notificationFilter,
                 mock(NotificationEntryManagerLogger::class.java),
                 sectionsManager,
                 personNotificationIdentifier,
@@ -324,6 +327,32 @@
         assertEquals(e.bucket, BUCKET_SILENT)
     }
 
+    @Test
+    fun testFilter_resetsInitalizationTime() {
+        // GIVEN an entry that was initialized 1 second ago
+        val notif = Notification.Builder(mContext, "test") .build()
+
+        val e = NotificationEntryBuilder()
+            .setPkg("pkg")
+            .setOpPkg("pkg")
+            .setTag("tag")
+            .setNotification(notif)
+            .setUser(mContext.user)
+            .setChannel(NotificationChannel("test", "", IMPORTANCE_DEFAULT))
+            .setOverrideGroupKey("")
+            .build()
+
+        e.setInitializationTime(SystemClock.elapsedRealtime() - 1000)
+        assertEquals(true, e.hasFinishedInitialization())
+
+        // WHEN we update ranking and filter out the notification entry
+        whenever(notificationFilter.shouldFilterOut(e)).thenReturn(true)
+        rankingManager.updateRanking(RankingMap(arrayOf(e.ranking)), listOf(e), "test")
+
+        // THEN the initialization time for the entry is reset
+        assertEquals(false, e.hasFinishedInitialization())
+    }
+
     internal class TestableNotificationRankingManager(
         mediaManager: Lazy<NotificationMediaManager>,
         groupManager: NotificationGroupManager,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
index 3adc3d0..a93b54a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
@@ -19,8 +19,10 @@
 import static com.android.systemui.statusbar.notification.collection.ListDumper.dumpTree;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyList;
 import static org.mockito.ArgumentMatchers.anyLong;
@@ -33,6 +35,7 @@
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 
+import android.os.SystemClock;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.util.ArrayMap;
@@ -475,6 +478,28 @@
     }
 
     @Test
+    public void testFilter_resetsInitalizationTime() {
+        // GIVEN a NotifFilter that filters out a specific package
+        NotifFilter filter1 = spy(new PackageFilter(PACKAGE_1));
+        mListBuilder.addFinalizeFilter(filter1);
+
+        // GIVEN a notification that was initialized 1 second ago that will be filtered out
+        final NotificationEntry entry = new NotificationEntryBuilder()
+                .setPkg(PACKAGE_1)
+                .setId(nextId(PACKAGE_1))
+                .setRank(nextRank())
+                .build();
+        entry.setInitializationTime(SystemClock.elapsedRealtime() - 1000);
+        assertTrue(entry.hasFinishedInitialization());
+
+        // WHEN the pipeline is kicked off
+        mReadyForBuildListener.onBuildList(Arrays.asList(entry));
+
+        // THEN the entry's initialization time is reset
+        assertFalse(entry.hasFinishedInitialization());
+    }
+
+    @Test
     public void testNotifFiltersCanBePreempted() {
         // GIVEN two notif filters
         NotifFilter filter1 = spy(new PackageFilter(PACKAGE_2));
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java
index cc2d1c2..e04d25b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java
@@ -83,6 +83,7 @@
     @Mock private NotificationStackScrollLayout mNotificationStackScrollLayout;
     @Mock private NotificationShadeDepthController mNotificationShadeDepthController;
     @Mock private SuperStatusBarViewFactory mStatusBarViewFactory;
+    @Mock private NotificationShadeWindowController mNotificationShadeWindowController;
 
     @Before
     public void setUp() {
@@ -121,7 +122,7 @@
                 mNotificationPanelViewController,
                 mStatusBarViewFactory);
         mController.setupExpandedStatusBar();
-        mController.setService(mStatusBar);
+        mController.setService(mStatusBar, mNotificationShadeWindowController);
         mController.setDragDownHelper(mDragDownHelper);
 
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java
index cd2c349..bf2bd38 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java
@@ -31,6 +31,7 @@
 
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.statusbar.ActionClickLogger;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
@@ -73,7 +74,8 @@
         mRemoteInputCallback = spy(new StatusBarRemoteInputCallback(mContext,
                 mock(NotificationGroupManager.class), mNotificationLockscreenUserManager,
                 mKeyguardStateController, mStatusBarStateController, mStatusBarKeyguardViewManager,
-                mActivityStarter, mShadeController, new CommandQueue(mContext)));
+                mActivityStarter, mShadeController, new CommandQueue(mContext),
+                mock(ActionClickLogger.class)));
         mRemoteInputCallback.mChallengeReceiver = mRemoteInputCallback.new ChallengeReceiver();
     }
 
diff --git a/services/core/java/com/android/server/display/DisplayModeDirector.java b/services/core/java/com/android/server/display/DisplayModeDirector.java
index c54ebf8..3c05080 100644
--- a/services/core/java/com/android/server/display/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/DisplayModeDirector.java
@@ -92,7 +92,7 @@
     private final AppRequestObserver mAppRequestObserver;
     private final SettingsObserver mSettingsObserver;
     private final DisplayObserver mDisplayObserver;
-    private final BrightnessObserver mBrightnessObserver;
+    private BrightnessObserver mBrightnessObserver;
 
     private final DeviceConfigDisplaySettings mDeviceConfigDisplaySettings;
     private DesiredDisplayModeSpecsListener mDesiredDisplayModeSpecsListener;
@@ -460,6 +460,21 @@
         mVotesByDisplay = votesByDisplay;
     }
 
+    @VisibleForTesting
+    void injectBrightnessObserver(BrightnessObserver brightnessObserver) {
+        mBrightnessObserver = brightnessObserver;
+    }
+
+    @VisibleForTesting
+    DesiredDisplayModeSpecs getDesiredDisplayModeSpecsWithInjectedFpsSettings(
+            float minRefreshRate, float peakRefreshRate, float defaultRefreshRate) {
+        synchronized (mLock) {
+            mSettingsObserver.updateRefreshRateSettingLocked(
+                    minRefreshRate, peakRefreshRate, defaultRefreshRate);
+            return getDesiredDisplayModeSpecs(Display.DEFAULT_DISPLAY);
+        }
+    }
+
     /**
      * Listens for changes refresh rate coordination.
      */
@@ -666,14 +681,18 @@
 
     @VisibleForTesting
     static final class Vote {
+        // DEFAULT_FRAME_RATE votes for [0, DEFAULT]. As the lowest priority vote, it's overridden
+        // by all other considerations. It acts to set a default frame rate for a device.
+        public static final int PRIORITY_DEFAULT_REFRESH_RATE = 0;
+
         // LOW_BRIGHTNESS votes for a single refresh rate like [60,60], [90,90] or null.
         // If the higher voters result is a range, it will fix the rate to a single choice.
         // It's used to avoid rate switch in certain conditions.
-        public static final int PRIORITY_LOW_BRIGHTNESS = 0;
+        public static final int PRIORITY_LOW_BRIGHTNESS = 1;
 
         // SETTING_MIN_REFRESH_RATE is used to propose a lower bound of display refresh rate.
         // It votes [MIN_REFRESH_RATE, Float.POSITIVE_INFINITY]
-        public static final int PRIORITY_USER_SETTING_MIN_REFRESH_RATE = 1;
+        public static final int PRIORITY_USER_SETTING_MIN_REFRESH_RATE = 2;
 
         // We split the app request into different priorities in case we can satisfy one desire
         // without the other.
@@ -683,20 +702,20 @@
         // @see android.view.WindowManager.LayoutParams#preferredDisplayModeId
         // System also forces some apps like blacklisted app to run at a lower refresh rate.
         // @see android.R.array#config_highRefreshRateBlacklist
-        public static final int PRIORITY_APP_REQUEST_REFRESH_RATE = 2;
-        public static final int PRIORITY_APP_REQUEST_SIZE = 3;
+        public static final int PRIORITY_APP_REQUEST_REFRESH_RATE = 3;
+        public static final int PRIORITY_APP_REQUEST_SIZE = 4;
 
         // SETTING_PEAK_REFRESH_RATE has a high priority and will restrict the bounds of the rest
         // of low priority voters. It votes [0, max(PEAK, MIN)]
-        public static final int PRIORITY_USER_SETTING_PEAK_REFRESH_RATE = 4;
+        public static final int PRIORITY_USER_SETTING_PEAK_REFRESH_RATE = 5;
 
         // LOW_POWER_MODE force display to [0, 60HZ] if Settings.Global.LOW_POWER_MODE is on.
-        public static final int PRIORITY_LOW_POWER_MODE = 5;
+        public static final int PRIORITY_LOW_POWER_MODE = 6;
 
         // Whenever a new priority is added, remember to update MIN_PRIORITY, MAX_PRIORITY, and
         // APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF, as well as priorityToString.
 
-        public static final int MIN_PRIORITY = PRIORITY_LOW_BRIGHTNESS;
+        public static final int MIN_PRIORITY = PRIORITY_DEFAULT_REFRESH_RATE;
         public static final int MAX_PRIORITY = PRIORITY_LOW_POWER_MODE;
 
         // The cutoff for the app request refresh rate range. Votes with priorities lower than this
@@ -740,6 +759,8 @@
 
         public static String priorityToString(int priority) {
             switch (priority) {
+                case PRIORITY_DEFAULT_REFRESH_RATE:
+                    return "PRIORITY_DEFAULT_REFRESH_RATE";
                 case PRIORITY_LOW_BRIGHTNESS:
                     return "PRIORITY_LOW_BRIGHTNESS";
                 case PRIORITY_USER_SETTING_MIN_REFRESH_RATE:
@@ -776,12 +797,15 @@
 
         private final Context mContext;
         private float mDefaultPeakRefreshRate;
+        private float mDefaultRefreshRate;
 
         SettingsObserver(@NonNull Context context, @NonNull Handler handler) {
             super(handler);
             mContext = context;
             mDefaultPeakRefreshRate = (float) context.getResources().getInteger(
                     R.integer.config_defaultPeakRefreshRate);
+            mDefaultRefreshRate =
+                    (float) context.getResources().getInteger(R.integer.config_defaultRefreshRate);
         }
 
         public void observe() {
@@ -849,17 +873,48 @@
                     Settings.System.MIN_REFRESH_RATE, 0f);
             float peakRefreshRate = Settings.System.getFloat(mContext.getContentResolver(),
                     Settings.System.PEAK_REFRESH_RATE, mDefaultPeakRefreshRate);
+            updateRefreshRateSettingLocked(minRefreshRate, peakRefreshRate, mDefaultRefreshRate);
+        }
 
-            updateVoteLocked(Vote.PRIORITY_USER_SETTING_PEAK_REFRESH_RATE,
-                    Vote.forRefreshRates(0f, Math.max(minRefreshRate, peakRefreshRate)));
+        private void updateRefreshRateSettingLocked(
+                float minRefreshRate, float peakRefreshRate, float defaultRefreshRate) {
+            // TODO(b/156304339): The logic in here, aside from updating the refresh rate votes, is
+            // used to predict if we're going to be doing frequent refresh rate switching, and if
+            // so, enable the brightness observer. The logic here is more complicated and fragile
+            // than necessary, and we should improve it. See b/156304339 for more info.
+            Vote peakVote = peakRefreshRate == 0f
+                    ? null
+                    : Vote.forRefreshRates(0f, Math.max(minRefreshRate, peakRefreshRate));
+            updateVoteLocked(Vote.PRIORITY_USER_SETTING_PEAK_REFRESH_RATE, peakVote);
             updateVoteLocked(Vote.PRIORITY_USER_SETTING_MIN_REFRESH_RATE,
                     Vote.forRefreshRates(minRefreshRate, Float.POSITIVE_INFINITY));
+            Vote defaultVote =
+                    defaultRefreshRate == 0f ? null : Vote.forRefreshRates(0f, defaultRefreshRate);
+            updateVoteLocked(Vote.PRIORITY_DEFAULT_REFRESH_RATE, defaultVote);
 
-            mBrightnessObserver.onRefreshRateSettingChangedLocked(minRefreshRate, peakRefreshRate);
+            float maxRefreshRate;
+            if (peakRefreshRate == 0f && defaultRefreshRate == 0f) {
+                // We require that at least one of the peak or default refresh rate values are
+                // set. The brightness observer requires that we're able to predict whether or not
+                // we're going to do frequent refresh rate switching, and with the way the code is
+                // currently written, we need either a default or peak refresh rate value for that.
+                Slog.e(TAG, "Default and peak refresh rates are both 0. One of them should be set"
+                        + " to a valid value.");
+                maxRefreshRate = minRefreshRate;
+            } else if (peakRefreshRate == 0f) {
+                maxRefreshRate = defaultRefreshRate;
+            } else if (defaultRefreshRate == 0f) {
+                maxRefreshRate = peakRefreshRate;
+            } else {
+                maxRefreshRate = Math.min(defaultRefreshRate, peakRefreshRate);
+            }
+
+            mBrightnessObserver.onRefreshRateSettingChangedLocked(minRefreshRate, maxRefreshRate);
         }
 
         public void dumpLocked(PrintWriter pw) {
             pw.println("  SettingsObserver");
+            pw.println("    mDefaultRefreshRate: " + mDefaultRefreshRate);
             pw.println("    mDefaultPeakRefreshRate: " + mDefaultPeakRefreshRate);
         }
     }
@@ -1014,7 +1069,8 @@
      * {@link R.array#config_brightnessThresholdsOfPeakRefreshRate} and
      * {@link R.array#config_ambientThresholdsOfPeakRefreshRate}.
      */
-    private class BrightnessObserver extends ContentObserver {
+    @VisibleForTesting
+    public class BrightnessObserver extends ContentObserver {
         // TODO: brightnessfloat: change this to the float setting
         private final Uri mDisplayBrightnessSetting =
                 Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS);
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index 4f5a02a..2a65b33 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -201,7 +201,6 @@
         private SurfaceControl.DisplayConfig[] mDisplayConfigs;
         private Spline mSystemBrightnessToNits;
         private Spline mNitsToHalBrightness;
-        private boolean mHalBrightnessSupport;
 
         private DisplayDeviceConfig mDisplayDeviceConfig;
 
@@ -225,7 +224,6 @@
             }
             mAllmSupported = SurfaceControl.getAutoLowLatencyModeSupport(displayToken);
             mGameContentTypeSupported = SurfaceControl.getGameContentTypeSupport(displayToken);
-            mHalBrightnessSupport = SurfaceControl.getDisplayBrightnessSupport(displayToken);
             mDisplayDeviceConfig = null;
             // Defer configuration file loading
             BackgroundThread.getHandler().sendMessage(PooledLambda.obtainMessage(
@@ -717,11 +715,10 @@
                         Trace.traceBegin(Trace.TRACE_TAG_POWER, "setDisplayBrightness("
                                 + "id=" + physicalDisplayId + ", brightness=" + brightness + ")");
                         try {
-                            // TODO: make it float
                             if (isHalBrightnessRangeSpecified()) {
                                 brightness = displayBrightnessToHalBrightness(
-                                        BrightnessSynchronizer.brightnessFloatToInt(getContext(),
-                                                brightness));
+                                        BrightnessSynchronizer.brightnessFloatToIntRange(
+                                                getContext(), brightness));
                             }
                             if (mBacklight != null) {
                                 mBacklight.setBrightness(brightness);
@@ -744,12 +741,13 @@
                      * Hal brightness space if the HAL brightness space has been provided via
                      * a display device configuration file.
                      */
-                    private float displayBrightnessToHalBrightness(int brightness) {
+                    private float displayBrightnessToHalBrightness(float brightness) {
                         if (!isHalBrightnessRangeSpecified()) {
                             return PowerManager.BRIGHTNESS_INVALID_FLOAT;
                         }
 
-                        if (brightness == 0) {
+                        if (BrightnessSynchronizer.floatEquals(
+                                brightness, PowerManager.BRIGHTNESS_OFF)) {
                             return PowerManager.BRIGHTNESS_OFF_FLOAT;
                         }
 
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 549e336..9de95ab 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -2208,8 +2208,12 @@
         public void setHdmiCecVolumeControlEnabled(final boolean isHdmiCecVolumeControlEnabled) {
             enforceAccessPermission();
             long token = Binder.clearCallingIdentity();
-            HdmiControlService.this.setHdmiCecVolumeControlEnabled(isHdmiCecVolumeControlEnabled);
-            Binder.restoreCallingIdentity(token);
+            try {
+                HdmiControlService.this.setHdmiCecVolumeControlEnabled(
+                        isHdmiCecVolumeControlEnabled);
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
         }
 
         @Override
diff --git a/services/core/java/com/android/server/media/BluetoothRouteProvider.java b/services/core/java/com/android/server/media/BluetoothRouteProvider.java
index fd8e03e..6acfd45 100644
--- a/services/core/java/com/android/server/media/BluetoothRouteProvider.java
+++ b/services/core/java/com/android/server/media/BluetoothRouteProvider.java
@@ -169,7 +169,9 @@
         for (BluetoothDevice device : mBluetoothAdapter.getBondedDevices()) {
             if (device.isConnected()) {
                 BluetoothRouteInfo newBtRoute = createBluetoothRoute(device);
-                mBluetoothRoutes.put(device.getAddress(), newBtRoute);
+                if (newBtRoute.connectedProfiles.size() > 0) {
+                    mBluetoothRoutes.put(device.getAddress(), newBtRoute);
+                }
             }
         }
     }
@@ -233,22 +235,32 @@
     private BluetoothRouteInfo createBluetoothRoute(BluetoothDevice device) {
         BluetoothRouteInfo newBtRoute = new BluetoothRouteInfo();
         newBtRoute.btDevice = device;
-        // Current / Max volume will be set when connected.
+        // Current volume will be set when connected.
         // TODO: Is there any BT device which has fixed volume?
         String deviceName = device.getName();
         if (TextUtils.isEmpty(deviceName)) {
             deviceName = mContext.getResources().getText(R.string.unknownName).toString();
         }
+        int type = MediaRoute2Info.TYPE_BLUETOOTH_A2DP;
+        newBtRoute.connectedProfiles = new SparseBooleanArray();
+        if (mA2dpProfile != null && mA2dpProfile.getConnectedDevices().contains(device)) {
+            newBtRoute.connectedProfiles.put(BluetoothProfile.A2DP, true);
+        }
+        if (mHearingAidProfile != null
+                && mHearingAidProfile.getConnectedDevices().contains(device)) {
+            newBtRoute.connectedProfiles.put(BluetoothProfile.HEARING_AID, true);
+            type = MediaRoute2Info.TYPE_HEARING_AID;
+        }
+
         newBtRoute.route = new MediaRoute2Info.Builder(device.getAddress(), deviceName)
                 .addFeature(MediaRoute2Info.FEATURE_LIVE_AUDIO)
                 .setConnectionState(MediaRoute2Info.CONNECTION_STATE_DISCONNECTED)
                 .setDescription(mContext.getResources().getText(
                         R.string.bluetooth_a2dp_audio_route_name).toString())
-                .setType(MediaRoute2Info.TYPE_BLUETOOTH_A2DP)
+                .setType(type)
                 .setVolumeHandling(MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE)
                 .setVolumeMax(mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC))
                 .build();
-        newBtRoute.connectedProfiles = new SparseBooleanArray();
         return newBtRoute;
     }
 
@@ -316,7 +328,6 @@
                     btRoute = createBluetoothRoute(device);
                     mBluetoothRoutes.put(device.getAddress(), btRoute);
                 }
-                btRoute.connectedProfiles.put(profile, true);
                 if (activeDevices.contains(device)) {
                     mSelectedRoute = btRoute;
                     setRouteConnectionState(mSelectedRoute,
@@ -378,18 +389,11 @@
                     BluetoothDevice.ERROR);
             BluetoothRouteInfo btRoute = mBluetoothRoutes.get(device.getAddress());
             if (bondState == BluetoothDevice.BOND_BONDED && btRoute == null) {
-                //TODO: The type of the new route is A2DP even when it's HEARING_AID.
-                // We may determine the type of route when create the route.
                 btRoute = createBluetoothRoute(device);
-                if (mA2dpProfile != null && mA2dpProfile.getConnectedDevices().contains(device)) {
-                    btRoute.connectedProfiles.put(BluetoothProfile.A2DP, true);
+                if (btRoute.connectedProfiles.size() > 0) {
+                    mBluetoothRoutes.put(device.getAddress(), btRoute);
+                    notifyBluetoothRoutesUpdated();
                 }
-                if (mHearingAidProfile != null
-                        && mHearingAidProfile.getConnectedDevices().contains(device)) {
-                    btRoute.connectedProfiles.put(BluetoothProfile.HEARING_AID, true);
-                }
-                mBluetoothRoutes.put(device.getAddress(), btRoute);
-                notifyBluetoothRoutesUpdated();
             } else if (bondState == BluetoothDevice.BOND_NONE
                     && mBluetoothRoutes.remove(device.getAddress()) != null) {
                 notifyBluetoothRoutesUpdated();
@@ -430,9 +434,10 @@
             if (state == BluetoothProfile.STATE_CONNECTED) {
                 if (btRoute == null) {
                     btRoute = createBluetoothRoute(device);
-                    mBluetoothRoutes.put(device.getAddress(), btRoute);
-                    btRoute.connectedProfiles.put(profile, true);
-                    notifyBluetoothRoutesUpdated();
+                    if (btRoute.connectedProfiles.size() > 0) {
+                        mBluetoothRoutes.put(device.getAddress(), btRoute);
+                        notifyBluetoothRoutesUpdated();
+                    }
                 } else {
                     btRoute.connectedProfiles.put(profile, true);
                 }
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index bc0e816..84ec440 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -1134,8 +1134,19 @@
                 if (cb == null) {
                     throw new IllegalArgumentException("Controller callback cannot be null");
                 }
-                return createSessionInternal(pid, uid, resolvedUserId, packageName, cb, tag,
-                        sessionInfo).getSessionBinder();
+                MediaSessionRecord session = createSessionInternal(
+                        pid, uid, resolvedUserId, packageName, cb, tag, sessionInfo);
+                if (session == null) {
+                    throw new IllegalStateException("Failed to create a new session record");
+                }
+                ISession sessionBinder = session.getSessionBinder();
+                if (sessionBinder == null) {
+                    throw new IllegalStateException("Invalid session record");
+                }
+                return sessionBinder;
+            } catch (Exception e) {
+                Slog.w(TAG, "Exception in creating a new session", e);
+                throw e;
             } finally {
                 Binder.restoreCallingIdentity(token);
             }
diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
index 12309f4..a859a42 100644
--- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
+++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
@@ -289,6 +289,7 @@
     private StatsPullAtomCallbackImpl mStatsCallbackImpl;
 
     private int mAppOpsSamplingRate = 0;
+    private final ArraySet<Integer> mDangerousAppOpsList = new ArraySet<>();
 
     // Baselines that stores list of NetworkStats right after initializing, with associated
     // information. This is used to calculate difference when pulling
@@ -526,6 +527,25 @@
         } catch (RemoteException e) {
             Slog.e(TAG, "failed to initialize healthHalWrapper");
         }
+
+        // Initialize list of AppOps related to DangerousPermissions
+        PackageManager pm = mContext.getPackageManager();
+        for (int op = 0; op < AppOpsManager._NUM_OP; op++) {
+            String perm = AppOpsManager.opToPermission(op);
+            if (perm == null) {
+                continue;
+            } else {
+                PermissionInfo permInfo;
+                try {
+                    permInfo = pm.getPermissionInfo(perm, 0);
+                    if (permInfo.getProtection() == PROTECTION_DANGEROUS) {
+                        mDangerousAppOpsList.add(op);
+                    }
+                } catch (PackageManager.NameNotFoundException exception) {
+                    continue;
+                }
+            }
+        }
     }
 
     void registerEventListeners() {
@@ -3113,22 +3133,8 @@
         e.writeLong(op.getBackgroundRejectCount(OP_FLAGS_PULLED));
         e.writeLong(op.getForegroundAccessDuration(OP_FLAGS_PULLED));
         e.writeLong(op.getBackgroundAccessDuration(OP_FLAGS_PULLED));
+        e.writeBoolean(mDangerousAppOpsList.contains(op.getOpCode()));
 
-        String perm = AppOpsManager.opToPermission(op.getOpCode());
-        if (perm == null) {
-            e.writeBoolean(false);
-        } else {
-            PermissionInfo permInfo;
-            try {
-                permInfo = mContext.getPackageManager().getPermissionInfo(
-                        perm,
-                        0);
-                e.writeBoolean(
-                        permInfo.getProtection() == PROTECTION_DANGEROUS);
-            } catch (PackageManager.NameNotFoundException exception) {
-                e.writeBoolean(false);
-            }
-        }
         if (atomTag == FrameworkStatsLog.ATTRIBUTED_APP_OPS) {
             e.writeInt(mAppOpsSamplingRate);
         }
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 91c849c..0598680 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -6684,6 +6684,13 @@
     }
 
     @Override
+    void getAnimationPosition(Point outPosition) {
+        // Always animate from zero because if the activity doesn't fill the task, the letterbox
+        // will fill the remaining area that should be included in the animation.
+        outPosition.set(0, 0);
+    }
+
+    @Override
     public void onConfigurationChanged(Configuration newParentConfig) {
         if (mCompatDisplayInsets != null) {
             Configuration overrideConfig = getRequestedOverrideConfiguration();
diff --git a/services/core/java/com/android/server/wm/ActivityStack.java b/services/core/java/com/android/server/wm/ActivityStack.java
index cb2d98c..2f868d9 100644
--- a/services/core/java/com/android/server/wm/ActivityStack.java
+++ b/services/core/java/com/android/server/wm/ActivityStack.java
@@ -382,6 +382,7 @@
             return mBehindFullscreenActivity;
         }
 
+        /** Returns {@code true} to stop the outer loop and indicate the result is computed. */
         private boolean processActivity(ActivityRecord r, ActivityRecord topActivity) {
             if (mAboveTop) {
                 if (r == topActivity) {
@@ -397,7 +398,10 @@
             }
 
             if (mHandlingOccluded) {
-                mHandleBehindFullscreenActivity.accept(r);
+                // Iterating through all occluded activities.
+                if (mBehindFullscreenActivity) {
+                    mHandleBehindFullscreenActivity.accept(r);
+                }
             } else if (r == mToCheck) {
                 return true;
             } else if (mBehindFullscreenActivity) {
@@ -1307,8 +1311,17 @@
     /**
      * Make sure that all activities that need to be visible in the stack (that is, they
      * currently can be seen by the user) actually are and update their configuration.
+     * @param starting The top most activity in the task.
+     *                 The activity is either starting or resuming.
+     *                 Caller should ensure starting activity is visible.
+     * @param preserveWindows Flag indicating whether windows should be preserved when updating
+     *                        configuration in {@link mEnsureActivitiesVisibleHelper}.
+     * @param configChanges Parts of the configuration that changed for this activity for evaluating
+     *                      if the screen should be frozen as part of
+     *                      {@link mEnsureActivitiesVisibleHelper}.
+     *
      */
-    void ensureActivitiesVisible(ActivityRecord starting, int configChanges,
+    void ensureActivitiesVisible(@Nullable ActivityRecord starting, int configChanges,
             boolean preserveWindows) {
         ensureActivitiesVisible(starting, configChanges, preserveWindows, true /* notifyClients */);
     }
@@ -1317,9 +1330,19 @@
      * Ensure visibility with an option to also update the configuration of visible activities.
      * @see #ensureActivitiesVisible(ActivityRecord, int, boolean)
      * @see RootWindowContainer#ensureActivitiesVisible(ActivityRecord, int, boolean)
+     * @param starting The top most activity in the task.
+     *                 The activity is either starting or resuming.
+     *                 Caller should ensure starting activity is visible.
+     * @param notifyClients Flag indicating whether the visibility updates should be sent to the
+     *                      clients in {@link mEnsureActivitiesVisibleHelper}.
+     * @param preserveWindows Flag indicating whether windows should be preserved when updating
+     *                        configuration in {@link mEnsureActivitiesVisibleHelper}.
+     * @param configChanges Parts of the configuration that changed for this activity for evaluating
+     *                      if the screen should be frozen as part of
+     *                      {@link mEnsureActivitiesVisibleHelper}.
      */
     // TODO: Should be re-worked based on the fact that each task as a stack in most cases.
-    void ensureActivitiesVisible(ActivityRecord starting, int configChanges,
+    void ensureActivitiesVisible(@Nullable ActivityRecord starting, int configChanges,
             boolean preserveWindows, boolean notifyClients) {
         mTopActivityOccludesKeyguard = false;
         mTopDismissingKeyguardActivity = null;
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 79e8ee3..bcdd6e3 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -1539,7 +1539,10 @@
      *
      * Note: This method should only be called from {@link #startActivityUnchecked}.
      */
-    private int startActivityInner(final ActivityRecord r, ActivityRecord sourceRecord,
+
+    // TODO(b/152429287): Make it easier to exercise code paths through startActivityInner
+    @VisibleForTesting
+    int startActivityInner(final ActivityRecord r, ActivityRecord sourceRecord,
             IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
             int startFlags, boolean doResume, ActivityOptions options, Task inTask,
             boolean restrictedBgActivity) {
@@ -1660,7 +1663,10 @@
                 // Also, we don't want to resume activities in a task that currently has an overlay
                 // as the starting activity just needs to be in the visible paused state until the
                 // over is removed.
-                mTargetStack.ensureActivitiesVisible(mStartActivity, 0, !PRESERVE_WINDOWS);
+                // Passing {@code null} as the start parameter ensures all activities are made
+                // visible.
+                mTargetStack.ensureActivitiesVisible(null /* starting */,
+                        0 /* configChanges */, !PRESERVE_WINDOWS);
                 // Go ahead and tell window manager to execute app transition for this activity
                 // since the app transition will not be triggered through the resume channel.
                 mTargetStack.getDisplay().mDisplayContent.executeAppTransition();
diff --git a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
index c92de2b..c4e03f5 100644
--- a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
+++ b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
@@ -21,6 +21,7 @@
 import static com.android.server.wm.ActivityStack.TAG_VISIBILITY;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_VISIBILITY;
 
+import android.annotation.Nullable;
 import android.util.Slog;
 
 import com.android.internal.util.function.pooled.PooledConsumer;
@@ -42,6 +43,16 @@
         mContiner = container;
     }
 
+    /**
+     * Update all attributes except {@link mContiner} to use in subsequent calculations.
+     *
+     * @param starting The activity that is being started
+     * @param configChanges Parts of the configuration that changed for this activity for evaluating
+     *                      if the screen should be frozen.
+     * @param preserveWindows Flag indicating whether windows should be preserved when updating.
+     * @param notifyClients Flag indicating whether the configuration and visibility changes shoulc
+     *                      be sent to the clients.
+     */
     void reset(ActivityRecord starting, int configChanges, boolean preserveWindows,
             boolean notifyClients) {
         mStarting = starting;
@@ -60,8 +71,17 @@
      * Ensure visibility with an option to also update the configuration of visible activities.
      * @see ActivityStack#ensureActivitiesVisible(ActivityRecord, int, boolean)
      * @see RootWindowContainer#ensureActivitiesVisible(ActivityRecord, int, boolean)
+     * @param starting The top most activity in the task.
+     *                 The activity is either starting or resuming.
+     *                 Caller should ensure starting activity is visible.
+     *
+     * @param configChanges Parts of the configuration that changed for this activity for evaluating
+     *                      if the screen should be frozen.
+     * @param preserveWindows Flag indicating whether windows should be preserved when updating.
+     * @param notifyClients Flag indicating whether the configuration and visibility changes shoulc
+     *                      be sent to the clients.
      */
-    void process(ActivityRecord starting, int configChanges, boolean preserveWindows,
+    void process(@Nullable ActivityRecord starting, int configChanges, boolean preserveWindows,
             boolean notifyClients) {
         reset(starting, configChanges, preserveWindows, notifyClients);
 
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index b9b6c08..d1cb210 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -238,23 +238,12 @@
         }
     }
 
-    private final boolean isWallpaperVisible(WindowState wallpaperTarget) {
-        final RecentsAnimationController recentsAnimationController =
-                mService.getRecentsAnimationController();
-        boolean isAnimatingWithRecentsComponent = recentsAnimationController != null
-                && recentsAnimationController.isWallpaperVisible(wallpaperTarget);
-        if (DEBUG_WALLPAPER) Slog.v(TAG, "Wallpaper vis: target " + wallpaperTarget + ", obscured="
-                + (wallpaperTarget != null ? Boolean.toString(wallpaperTarget.mObscured) : "??")
-                + " animating=" + ((wallpaperTarget != null && wallpaperTarget.mActivityRecord != null)
-                ? wallpaperTarget.mActivityRecord.isAnimating(TRANSITION | PARENTS) : null)
-                + " prev=" + mPrevWallpaperTarget
-                + " recentsAnimationWallpaperVisible=" + isAnimatingWithRecentsComponent);
-        return (wallpaperTarget != null
-                && (!wallpaperTarget.mObscured
-                        || isAnimatingWithRecentsComponent
-                        || (wallpaperTarget.mActivityRecord != null
-                        && wallpaperTarget.mActivityRecord.isAnimating(TRANSITION | PARENTS))))
-                || mPrevWallpaperTarget != null;
+    private boolean isWallpaperVisible(WindowState wallpaperTarget) {
+        if (DEBUG_WALLPAPER) {
+            Slog.v(TAG, "Wallpaper vis: target " + wallpaperTarget + " prev="
+                    + mPrevWallpaperTarget);
+        }
+        return wallpaperTarget != null || mPrevWallpaperTarget != null;
     }
 
     boolean isWallpaperTargetAnimating() {
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 5d7ec12..7bfddd7 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -2120,6 +2120,11 @@
         return getBounds();
     }
 
+    /** Gets the position relative to parent for animation. */
+    void getAnimationPosition(Point outPosition) {
+        getRelativePosition(outPosition);
+    }
+
     /**
      * Applies the app transition animation according the given the layout properties in the
      * window hierarchy.
@@ -2178,9 +2183,9 @@
 
         // Separate position and size for use in animators.
         mTmpRect.set(getAnimationBounds(appStackClipMode));
-        if (sHierarchicalAnimations) {
-            getRelativePosition(mTmpPoint);
-        } else {
+        getAnimationPosition(mTmpPoint);
+        if (!sHierarchicalAnimations) {
+            // Non-hierarchical animation uses position in global coordinates.
             mTmpPoint.set(mTmpRect.left, mTmpRect.top);
         }
         mTmpRect.offsetTo(0, 0);
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 60d59b2..18c25c1 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -247,6 +247,7 @@
 import android.telephony.TelephonyManager;
 import android.telephony.data.ApnSetting;
 import android.text.TextUtils;
+import android.text.format.DateUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.AtomicFile;
@@ -11965,10 +11966,21 @@
     }
 
     private void showLocationSettingsChangedNotification(UserHandle user) {
+        Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        // Fill the component explicitly to prevent the PendingIntent from being intercepted
+        // and fired with crafted target. b/155183624
+        ActivityInfo targetInfo = intent.resolveActivityInfo(
+                mInjector.getPackageManager(user.getIdentifier()),
+                PackageManager.MATCH_SYSTEM_ONLY);
+        if (targetInfo != null) {
+            intent.setComponent(targetInfo.getComponentName());
+        } else {
+            Slog.wtf(LOG_TAG, "Failed to resolve intent for location settings");
+        }
+
         PendingIntent locationSettingsIntent = mInjector.pendingIntentGetActivityAsUser(mContext, 0,
-                new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
-                        .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), PendingIntent.FLAG_UPDATE_CURRENT,
-                null, user);
+                intent, PendingIntent.FLAG_UPDATE_CURRENT, null, user);
         Notification notification = new Notification.Builder(mContext,
                 SystemNotificationChannels.DEVICE_ADMIN)
                 .setSmallIcon(R.drawable.ic_info_outline)
@@ -15994,31 +16006,27 @@
     private @PersonalAppsSuspensionReason int updatePersonalAppsSuspension(
             int profileUserId, boolean unlocked) {
         final boolean suspendedExplicitly;
-        final int deadlineState;
-        final String poPackage;
+        final boolean suspendedByTimeout;
         synchronized (getLockObject()) {
             final ActiveAdmin profileOwner = getProfileOwnerAdminLocked(profileUserId);
             if (profileOwner != null) {
-                deadlineState =
+                final int deadlineState =
                         updateProfileOffDeadlineLocked(profileUserId, profileOwner, unlocked);
                 suspendedExplicitly = profileOwner.mSuspendPersonalApps;
-                poPackage = profileOwner.info.getPackageName();
+                suspendedByTimeout = deadlineState == PROFILE_OFF_DEADLINE_REACHED;
+                Slog.d(LOG_TAG, String.format(
+                        "Personal apps suspended explicitly: %b, deadline state: %d",
+                        suspendedExplicitly, deadlineState));
+                final int notificationState =
+                        unlocked ? PROFILE_OFF_DEADLINE_DEFAULT : deadlineState;
+                updateProfileOffDeadlineNotificationLocked(
+                        profileUserId, profileOwner, notificationState);
             } else {
-                poPackage = null;
                 suspendedExplicitly = false;
-                deadlineState = PROFILE_OFF_DEADLINE_DEFAULT;
+                suspendedByTimeout = false;
             }
         }
 
-        Slog.d(LOG_TAG, String.format("Personal apps suspended explicitly: %b, deadline state: %d",
-                suspendedExplicitly, deadlineState));
-
-        if (poPackage != null) {
-            final int notificationState = unlocked ? PROFILE_OFF_DEADLINE_DEFAULT : deadlineState;
-            updateProfileOffDeadlineNotification(profileUserId, poPackage, notificationState);
-        }
-
-        final boolean suspendedByTimeout = deadlineState == PROFILE_OFF_DEADLINE_REACHED;
         final int parentUserId = getProfileParentId(profileUserId);
         suspendPersonalAppsInternal(parentUserId, suspendedExplicitly || suspendedByTimeout);
 
@@ -16130,9 +16138,9 @@
         }
     }
 
-    private void updateProfileOffDeadlineNotification(
-            int profileUserId, String profileOwnerPackage, int notificationState) {
-
+    @GuardedBy("getLockObject()")
+    private void updateProfileOffDeadlineNotificationLocked(
+            int profileUserId, ActiveAdmin profileOwner, int notificationState) {
         if (notificationState == PROFILE_OFF_DEADLINE_DEFAULT) {
             mInjector.getNotificationManager().cancel(SystemMessage.NOTE_PERSONAL_APPS_SUSPENDED);
             return;
@@ -16150,11 +16158,23 @@
         final Notification.Action turnProfileOnButton =
                 new Notification.Action.Builder(null /* icon */, buttonText, pendingIntent).build();
 
-        final String text = mContext.getString(
-                notificationState == PROFILE_OFF_DEADLINE_WARNING
-                ? R.string.personal_apps_suspension_tomorrow_text
-                : R.string.personal_apps_suspension_text);
-        final boolean ongoing = notificationState == PROFILE_OFF_DEADLINE_REACHED;
+        final String text;
+        final boolean ongoing;
+        if (notificationState == PROFILE_OFF_DEADLINE_WARNING) {
+            // Round to the closest integer number of days.
+            final int maxDays = (int)
+                    ((profileOwner.mProfileMaximumTimeOffMillis + MS_PER_DAY / 2) / MS_PER_DAY);
+            final String date = DateUtils.formatDateTime(
+                    mContext, profileOwner.mProfileOffDeadline, DateUtils.FORMAT_SHOW_DATE);
+            final String time = DateUtils.formatDateTime(
+                    mContext, profileOwner.mProfileOffDeadline, DateUtils.FORMAT_SHOW_TIME);
+            text = mContext.getString(
+                    R.string.personal_apps_suspension_soon_text, date, time, maxDays);
+            ongoing = false;
+        } else {
+            text = mContext.getString(R.string.personal_apps_suspension_text);
+            ongoing = true;
+        }
         final int color = mContext.getColor(R.color.personal_apps_suspension_notification_color);
         final Bundle extras = new Bundle();
         // TODO: Create a separate string for this.
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/RemoteBugreportUtils.java b/services/devicepolicy/java/com/android/server/devicepolicy/RemoteBugreportUtils.java
index 7cfbcc8..1630f27 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/RemoteBugreportUtils.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/RemoteBugreportUtils.java
@@ -22,9 +22,12 @@
 import android.app.admin.DevicePolicyManager;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.text.format.DateUtils;
+import android.util.Slog;
 
 import com.android.internal.R;
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
@@ -38,6 +41,7 @@
  */
 class RemoteBugreportUtils {
 
+    private static final String TAG = "RemoteBugreportUtils";
     static final int NOTIFICATION_ID = SystemMessage.NOTE_REMOTE_BUGREPORT;
 
     @Retention(RetentionPolicy.SOURCE)
@@ -60,6 +64,17 @@
         Intent dialogIntent = new Intent(Settings.ACTION_SHOW_REMOTE_BUGREPORT_DIALOG);
         dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
         dialogIntent.putExtra(DevicePolicyManager.EXTRA_BUGREPORT_NOTIFICATION_TYPE, type);
+
+        // Fill the component explicitly to prevent the PendingIntent from being intercepted
+        // and fired with crafted target. b/155183624
+        ActivityInfo targetInfo = dialogIntent.resolveActivityInfo(
+                context.getPackageManager(), PackageManager.MATCH_SYSTEM_ONLY);
+        if (targetInfo != null) {
+            dialogIntent.setComponent(targetInfo.getComponentName());
+        } else {
+            Slog.wtf(TAG, "Failed to resolve intent for remote bugreport dialog");
+        }
+
         PendingIntent pendingDialogIntent = PendingIntent.getActivityAsUser(context, type,
                 dialogIntent, 0, null, UserHandle.CURRENT);
 
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index 17dd286..724048b 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -204,7 +204,7 @@
     // Notification title and text for setManagedProfileMaximumTimeOff tests:
     private static final String PROFILE_OFF_SUSPENSION_TITLE = "suspension_title";
     private static final String PROFILE_OFF_SUSPENSION_TEXT = "suspension_text";
-    private static final String PROFILE_OFF_SUSPENSION_TOMORROW_TEXT = "suspension_tomorrow_text";
+    private static final String PROFILE_OFF_SUSPENSION_SOON_TEXT = "suspension_tomorrow_text";
 
     @Override
     protected void setUp() throws Exception {
@@ -6331,7 +6331,7 @@
         // Now the user should see a warning notification.
         verify(getServices().notificationManager, times(1))
                 .notify(anyInt(), argThat(hasExtra(EXTRA_TITLE, PROFILE_OFF_SUSPENSION_TITLE,
-                        EXTRA_TEXT, PROFILE_OFF_SUSPENSION_TOMORROW_TEXT)));
+                        EXTRA_TEXT, PROFILE_OFF_SUSPENSION_SOON_TEXT)));
         // Apps shouldn't be suspended yet.
         verifyZeroInteractions(getServices().ipackageManager);
         clearInvocations(getServices().alarmManager);
@@ -6482,7 +6482,7 @@
         // To allow creation of Notification via Notification.Builder
         mContext.applicationInfo = mRealTestContext.getApplicationInfo();
 
-        // Setup notification titles.
+        // Setup resources to render notification titles and texts.
         when(mServiceContext.resources
                 .getString(R.string.personal_apps_suspension_title))
                 .thenReturn(PROFILE_OFF_SUSPENSION_TITLE);
@@ -6490,14 +6490,19 @@
                 .getString(R.string.personal_apps_suspension_text))
                 .thenReturn(PROFILE_OFF_SUSPENSION_TEXT);
         when(mServiceContext.resources
-                .getString(R.string.personal_apps_suspension_tomorrow_text))
-                .thenReturn(PROFILE_OFF_SUSPENSION_TOMORROW_TEXT);
+                .getString(eq(R.string.personal_apps_suspension_soon_text),
+                        anyString(), anyString(), anyInt()))
+                .thenReturn(PROFILE_OFF_SUSPENSION_SOON_TEXT);
+
+        // Make locale available for date formatting:
+        when(mServiceContext.resources.getConfiguration())
+                .thenReturn(mRealTestContext.getResources().getConfiguration());
 
         clearInvocations(getServices().ipackageManager);
     }
 
     private static Matcher<Notification> hasExtra(String... extras) {
-        assertEquals("Odd numebr of extra key-values", 0, extras.length % 2);
+        assertEquals("Odd number of extra key-values", 0, extras.length % 2);
         return new BaseMatcher<Notification>() {
             @Override
             public boolean matches(Object item) {
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
index 8137c36..43a396d 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
@@ -17,6 +17,7 @@
 package com.android.server.display;
 
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.verify;
 
 import android.content.Context;
 import android.os.Handler;
@@ -28,6 +29,7 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.server.display.DisplayModeDirector.BrightnessObserver;
 import com.android.server.display.DisplayModeDirector.DesiredDisplayModeSpecs;
 import com.android.server.display.DisplayModeDirector.Vote;
 
@@ -36,6 +38,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
 @SmallTest
@@ -52,16 +55,15 @@
         mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
     }
 
-    private DisplayModeDirector createDisplayModeDirectorWithDisplayFpsRange(
-            int minFps, int maxFps) {
+    private DisplayModeDirector createDirectorFromRefreshRateArray(
+            float[] refreshRates, int baseModeId) {
         DisplayModeDirector director =
                 new DisplayModeDirector(mContext, new Handler(Looper.getMainLooper()));
         int displayId = 0;
-        int numModes = maxFps - minFps + 1;
-        Display.Mode[] modes = new Display.Mode[numModes];
-        for (int i = minFps; i <= maxFps; i++) {
-            modes[i - minFps] = new Display.Mode(
-                    /*modeId=*/i, /*width=*/1000, /*height=*/1000, /*refreshRate=*/i);
+        Display.Mode[] modes = new Display.Mode[refreshRates.length];
+        for (int i = 0; i < refreshRates.length; i++) {
+            modes[i] = new Display.Mode(
+                    /*modeId=*/baseModeId + i, /*width=*/1000, /*height=*/1000, refreshRates[i]);
         }
         SparseArray<Display.Mode[]> supportedModesByDisplay = new SparseArray<>();
         supportedModesByDisplay.put(displayId, modes);
@@ -72,14 +74,22 @@
         return director;
     }
 
+    private DisplayModeDirector createDirectorFromFpsRange(int minFps, int maxFps) {
+        int numRefreshRates = maxFps - minFps + 1;
+        float[] refreshRates = new float[numRefreshRates];
+        for (int i = 0; i < numRefreshRates; i++) {
+            refreshRates[i] = minFps + i;
+        }
+        return createDirectorFromRefreshRateArray(refreshRates, /*baseModeId=*/minFps);
+    }
+
     @Test
     public void testDisplayModeVoting() {
         int displayId = 0;
 
         // With no votes present, DisplayModeDirector should allow any refresh rate.
         DesiredDisplayModeSpecs modeSpecs =
-                createDisplayModeDirectorWithDisplayFpsRange(60, 90).getDesiredDisplayModeSpecs(
-                        displayId);
+                createDirectorFromFpsRange(60, 90).getDesiredDisplayModeSpecs(displayId);
         Truth.assertThat(modeSpecs.baseModeId).isEqualTo(60);
         Truth.assertThat(modeSpecs.primaryRefreshRateRange.min).isEqualTo(0f);
         Truth.assertThat(modeSpecs.primaryRefreshRateRange.max).isEqualTo(Float.POSITIVE_INFINITY);
@@ -92,7 +102,7 @@
         {
             int minFps = 60;
             int maxFps = 90;
-            DisplayModeDirector director = createDisplayModeDirectorWithDisplayFpsRange(60, 90);
+            DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
             assertTrue(2 * numPriorities < maxFps - minFps + 1);
             SparseArray<Vote> votes = new SparseArray<>();
             SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
@@ -114,7 +124,7 @@
         // presence of higher priority votes.
         {
             assertTrue(numPriorities >= 2);
-            DisplayModeDirector director = createDisplayModeDirectorWithDisplayFpsRange(60, 90);
+            DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
             SparseArray<Vote> votes = new SparseArray<>();
             SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
             votesByDisplay.put(displayId, votes);
@@ -131,7 +141,7 @@
     @Test
     public void testVotingWithFloatingPointErrors() {
         int displayId = 0;
-        DisplayModeDirector director = createDisplayModeDirectorWithDisplayFpsRange(50, 90);
+        DisplayModeDirector director = createDirectorFromFpsRange(50, 90);
         SparseArray<Vote> votes = new SparseArray<>();
         SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
         votesByDisplay.put(displayId, votes);
@@ -154,7 +164,7 @@
         assertTrue(Vote.PRIORITY_LOW_BRIGHTNESS < Vote.PRIORITY_APP_REQUEST_SIZE);
 
         int displayId = 0;
-        DisplayModeDirector director = createDisplayModeDirectorWithDisplayFpsRange(60, 90);
+        DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
         SparseArray<Vote> votes = new SparseArray<>();
         SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
         votesByDisplay.put(displayId, votes);
@@ -202,7 +212,7 @@
                 >= Vote.APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF);
 
         int displayId = 0;
-        DisplayModeDirector director = createDisplayModeDirectorWithDisplayFpsRange(60, 90);
+        DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
         SparseArray<Vote> votes = new SparseArray<>();
         SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
         votesByDisplay.put(displayId, votes);
@@ -235,4 +245,61 @@
                 .isWithin(FLOAT_TOLERANCE)
                 .of(75);
     }
+
+    void verifySpecsWithRefreshRateSettings(DisplayModeDirector director, float minFps,
+            float peakFps, float defaultFps, float primaryMin, float primaryMax,
+            float appRequestMin, float appRequestMax) {
+        DesiredDisplayModeSpecs specs = director.getDesiredDisplayModeSpecsWithInjectedFpsSettings(
+                minFps, peakFps, defaultFps);
+        Truth.assertThat(specs.primaryRefreshRateRange.min).isEqualTo(primaryMin);
+        Truth.assertThat(specs.primaryRefreshRateRange.max).isEqualTo(primaryMax);
+        Truth.assertThat(specs.appRequestRefreshRateRange.min).isEqualTo(appRequestMin);
+        Truth.assertThat(specs.appRequestRefreshRateRange.max).isEqualTo(appRequestMax);
+    }
+
+    @Test
+    public void testSpecsFromRefreshRateSettings() {
+        // Confirm that, with varying settings for min, peak, and default refresh rate,
+        // DesiredDisplayModeSpecs is calculated correctly.
+        float[] refreshRates = {30.f, 60.f, 90.f, 120.f, 150.f};
+        DisplayModeDirector director =
+                createDirectorFromRefreshRateArray(refreshRates, /*baseModeId=*/0);
+
+        float inf = Float.POSITIVE_INFINITY;
+        verifySpecsWithRefreshRateSettings(director, 0, 0, 0, 0, inf, 0, inf);
+        verifySpecsWithRefreshRateSettings(director, 0, 0, 90, 0, 90, 0, inf);
+        verifySpecsWithRefreshRateSettings(director, 0, 90, 0, 0, 90, 0, 90);
+        verifySpecsWithRefreshRateSettings(director, 0, 90, 60, 0, 60, 0, 90);
+        verifySpecsWithRefreshRateSettings(director, 0, 90, 120, 0, 90, 0, 90);
+        verifySpecsWithRefreshRateSettings(director, 90, 0, 0, 90, inf, 0, inf);
+        verifySpecsWithRefreshRateSettings(director, 90, 0, 120, 90, 120, 0, inf);
+        verifySpecsWithRefreshRateSettings(director, 90, 0, 60, 90, inf, 0, inf);
+        verifySpecsWithRefreshRateSettings(director, 90, 120, 0, 90, 120, 0, 120);
+        verifySpecsWithRefreshRateSettings(director, 90, 60, 0, 90, 90, 0, 90);
+        verifySpecsWithRefreshRateSettings(director, 60, 120, 90, 60, 90, 0, 120);
+    }
+
+    void verifyBrightnessObserverCall(DisplayModeDirector director, float minFps, float peakFps,
+            float defaultFps, float brightnessObserverMin, float brightnessObserverMax) {
+        BrightnessObserver brightnessObserver = Mockito.mock(BrightnessObserver.class);
+        director.injectBrightnessObserver(brightnessObserver);
+        director.getDesiredDisplayModeSpecsWithInjectedFpsSettings(minFps, peakFps, defaultFps);
+        verify(brightnessObserver)
+                .onRefreshRateSettingChangedLocked(brightnessObserverMin, brightnessObserverMax);
+    }
+
+    @Test
+    public void testBrightnessObserverCallWithRefreshRateSettings() {
+        // Confirm that, with varying settings for min, peak, and default refresh rate, we make the
+        // correct call to the brightness observer.
+        float[] refreshRates = {60.f, 90.f, 120.f};
+        DisplayModeDirector director =
+                createDirectorFromRefreshRateArray(refreshRates, /*baseModeId=*/0);
+        verifyBrightnessObserverCall(director, 0, 0, 0, 0, 0);
+        verifyBrightnessObserverCall(director, 0, 0, 90, 0, 90);
+        verifyBrightnessObserverCall(director, 0, 90, 0, 0, 90);
+        verifyBrightnessObserverCall(director, 0, 90, 60, 0, 60);
+        verifyBrightnessObserverCall(director, 90, 90, 0, 90, 90);
+        verifyBrightnessObserverCall(director, 120, 90, 0, 120, 90);
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStackTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStackTests.java
index 822cb5a..64e08c6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStackTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStackTests.java
@@ -76,6 +76,9 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.ArrayList;
+import java.util.function.Consumer;
+
 /**
  * Tests for the {@link ActivityStack} class.
  *
@@ -1327,6 +1330,8 @@
 
     @Test
     public void testCheckBehindFullscreenActivity() {
+        final ArrayList<ActivityRecord> occludedActivities = new ArrayList<>();
+        final Consumer<ActivityRecord> handleBehindFullscreenActivity = occludedActivities::add;
         final ActivityRecord bottomActivity =
                 new ActivityBuilder(mService).setStack(mStack).setTask(mTask).build();
         final ActivityRecord topActivity =
@@ -1337,12 +1342,21 @@
         assertFalse(mStack.checkBehindFullscreenActivity(topActivity,
                 null /* handleBehindFullscreenActivity */));
 
+        // Top activity occludes bottom activity.
+        mStack.checkBehindFullscreenActivity(null /* toCheck */, handleBehindFullscreenActivity);
+        assertThat(occludedActivities).containsExactly(bottomActivity);
+
         doReturn(false).when(topActivity).occludesParent();
         assertFalse(mStack.checkBehindFullscreenActivity(bottomActivity,
                 null /* handleBehindFullscreenActivity */));
         assertFalse(mStack.checkBehindFullscreenActivity(topActivity,
                 null /* handleBehindFullscreenActivity */));
 
+        occludedActivities.clear();
+        // Top activity doesn't occlude parent, so the bottom activity is not occluded.
+        mStack.checkBehindFullscreenActivity(null /* toCheck */, handleBehindFullscreenActivity);
+        assertThat(occludedActivities).isEmpty();
+
         final ActivityRecord finishingActivity =
                 new ActivityBuilder(mService).setStack(mStack).setTask(mTask).build();
         finishingActivity.finishing = true;
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index edc9756..02d1f9b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -49,6 +49,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.server.wm.ActivityStackSupervisor.PRESERVE_WINDOWS;
 import static com.android.server.wm.WindowContainer.POSITION_BOTTOM;
 import static com.android.server.wm.WindowContainer.POSITION_TOP;
 
@@ -56,10 +57,10 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyObject;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 
@@ -99,7 +100,6 @@
 @Presubmit
 @RunWith(WindowTestRunner.class)
 public class ActivityStarterTests extends ActivityTestsBase {
-    private ActivityStarter mStarter;
     private ActivityStartController mController;
     private ActivityMetricsLogger mActivityMetricsLogger;
     private PackageManagerInternal mMockPackageManager;
@@ -127,8 +127,6 @@
         mController = mock(ActivityStartController.class);
         mActivityMetricsLogger = mock(ActivityMetricsLogger.class);
         clearInvocations(mActivityMetricsLogger);
-        mStarter = new ActivityStarter(mController, mService, mService.mStackSupervisor,
-                mock(ActivityStartInterceptor.class));
     }
 
     @Test
@@ -181,6 +179,7 @@
      * {@link ActivityStarter#execute} based on these preconditions and ensures the result matches
      * the expected. It is important to note that the method also checks side effects of the start,
      * such as ensuring {@link ActivityOptions#abort()} is called in the relevant scenarios.
+     *
      * @param preconditions A bitmask representing the preconditions for the launch
      * @param launchFlags The launch flags to be provided by the launch {@link Intent}.
      * @param expectedResult The expected result from the launch.
@@ -202,7 +201,7 @@
         final WindowProcessController wpc =
                 containsConditions(preconditions, PRECONDITION_NO_CALLER_APP)
                 ? null : new WindowProcessController(service, ai, null, 0, -1, null, listener);
-        doReturn(wpc).when(service).getProcessController(anyObject());
+        doReturn(wpc).when(service).getProcessController(any());
 
         final Intent intent = new Intent();
         intent.setFlags(launchFlags);
@@ -1034,4 +1033,46 @@
 
         verify(recentTasks, times(1)).add(any());
     }
+
+    @Test
+    public void testStartActivityInner_allSplitScreenPrimaryActivitiesVisible() {
+        // Given
+        final ActivityStarter starter = prepareStarter(0, false);
+
+        starter.setReason("testAllSplitScreenPrimaryActivitiesAreResumed");
+
+        final ActivityRecord targetRecord = new ActivityBuilder(mService).build();
+        targetRecord.setFocusable(false);
+        targetRecord.setVisibility(false);
+        final ActivityRecord sourceRecord = new ActivityBuilder(mService).build();
+
+        final ActivityStack stack = spy(
+                mRootWindowContainer.getDefaultTaskDisplayArea()
+                        .createStack(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD,
+                                /* onTop */true));
+
+        stack.addChild(targetRecord);
+
+        doReturn(stack).when(mRootWindowContainer)
+                .getLaunchStack(any(), any(), any(), anyBoolean(), any(), anyInt(), anyInt());
+
+        starter.mStartActivity = new ActivityBuilder(mService).build();
+
+        // When
+        starter.startActivityInner(
+                /* r */targetRecord,
+                /* sourceRecord */ sourceRecord,
+                /* voiceSession */null,
+                /* voiceInteractor */ null,
+                /* startFlags */ 0,
+                /* doResume */true,
+                /* options */null,
+                /* inTask */null,
+                /* restrictedBgActivity */false);
+
+        // Then
+        verify(stack).ensureActivitiesVisible(null, 0, !PRESERVE_WINDOWS);
+        verify(targetRecord).makeVisibleIfNeeded(null, true);
+        assertTrue(targetRecord.mVisibleRequested);
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenTests.java b/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenTests.java
index 8f18f64..07050d9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenTests.java
@@ -52,6 +52,7 @@
 import static org.mockito.Mockito.verify;
 
 import android.content.res.Configuration;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.platform.test.annotations.Presubmit;
 import android.view.IWindowManager;
@@ -432,20 +433,35 @@
         removeGlobalMinSizeRestriction();
         final Rect stackBounds = new Rect(0, 0, 1000, 600);
         final Rect taskBounds = new Rect(100, 400, 600, 800);
-        mStack.setBounds(stackBounds);
-        mTask.setBounds(taskBounds);
+        // Set the bounds and windowing mode to window configuration directly, otherwise the
+        // testing setups may be discarded by configuration resolving.
+        mStack.getWindowConfiguration().setBounds(stackBounds);
+        mTask.getWindowConfiguration().setBounds(taskBounds);
+        mActivity.getWindowConfiguration().setBounds(taskBounds);
 
         // Check that anim bounds for freeform window match task bounds
-        mTask.setWindowingMode(WINDOWING_MODE_FREEFORM);
+        mTask.getWindowConfiguration().setWindowingMode(WINDOWING_MODE_FREEFORM);
         assertEquals(mTask.getBounds(), mActivity.getAnimationBounds(STACK_CLIP_NONE));
 
         // STACK_CLIP_AFTER_ANIM should use task bounds since they will be clipped by
         // bounds animation layer.
-        mTask.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+        mTask.getWindowConfiguration().setWindowingMode(WINDOWING_MODE_FULLSCREEN);
         assertEquals(mTask.getBounds(), mActivity.getAnimationBounds(STACK_CLIP_AFTER_ANIM));
 
+        // Even the activity is smaller than task and it is not aligned to the top-left corner of
+        // task, the animation bounds the same as task and position should be zero because in real
+        // case the letterbox will fill the remaining area in task.
+        final Rect halfBounds = new Rect(taskBounds);
+        halfBounds.scale(0.5f);
+        mActivity.getWindowConfiguration().setBounds(halfBounds);
+        final Point animationPosition = new Point();
+        mActivity.getAnimationPosition(animationPosition);
+
+        assertEquals(taskBounds, mActivity.getAnimationBounds(STACK_CLIP_AFTER_ANIM));
+        assertEquals(new Point(0, 0), animationPosition);
+
         // STACK_CLIP_BEFORE_ANIM should use stack bounds since it won't be clipped later.
-        mTask.setWindowingMode(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
+        mTask.getWindowConfiguration().setWindowingMode(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
         assertEquals(mStack.getBounds(), mActivity.getAnimationBounds(STACK_CLIP_BEFORE_ANIM));
     }