Merge "[Expand metrics for Autofill]" into tm-qpr-dev
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java
index 5ef6855..30eacf3 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java
@@ -68,6 +68,11 @@
      */
     private final ArraySet<JobStatus> mChangedJobs = new ArraySet<>();
 
+    @GuardedBy("mLock")
+    private Boolean mLastReportedStatsdBatteryNotLow = null;
+    @GuardedBy("mLock")
+    private Boolean mLastReportedStatsdStablePower = null;
+
     public BatteryController(JobSchedulerService service) {
         super(service);
         mPowerTracker = new PowerTracker();
@@ -173,6 +178,19 @@
             Slog.d(TAG, "maybeReportNewChargingStateLocked: "
                     + powerConnected + "/" + stablePower + "/" + batteryNotLow);
         }
+
+        if (mLastReportedStatsdStablePower == null
+                || mLastReportedStatsdStablePower != stablePower) {
+            logDeviceWideConstraintStateToStatsd(JobStatus.CONSTRAINT_CHARGING, stablePower);
+            mLastReportedStatsdStablePower = stablePower;
+        }
+        if (mLastReportedStatsdBatteryNotLow == null
+                || mLastReportedStatsdBatteryNotLow != stablePower) {
+            logDeviceWideConstraintStateToStatsd(JobStatus.CONSTRAINT_BATTERY_NOT_LOW,
+                    batteryNotLow);
+            mLastReportedStatsdBatteryNotLow = batteryNotLow;
+        }
+
         final long nowElapsed = sElapsedRealtimeClock.millis();
         for (int i = mTrackedTasks.size() - 1; i >= 0; i--) {
             final JobStatus ts = mTrackedTasks.valueAt(i);
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java
index f6de109..abbe177 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java
@@ -153,6 +153,8 @@
                 changed = true;
             }
             mDeviceIdleMode = enabled;
+            logDeviceWideConstraintStateToStatsd(JobStatus.CONSTRAINT_DEVICE_NOT_DOZING,
+                    !mDeviceIdleMode);
             if (DEBUG) Slog.d(TAG, "mDeviceIdleMode=" + mDeviceIdleMode);
             mDeviceIdleUpdateFunctor.prepare();
             if (enabled) {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java
index a6fae2c..8311dc3 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java
@@ -93,6 +93,8 @@
     @Override
     public void reportNewIdleState(boolean isIdle) {
         synchronized (mLock) {
+            logDeviceWideConstraintStateToStatsd(JobStatus.CONSTRAINT_IDLE, isIdle);
+
             final long nowElapsed = sElapsedRealtimeClock.millis();
             for (int i = mTrackedTasks.size()-1; i >= 0; i--) {
                 mTrackedTasks.valueAt(i).setIdleConstraintSatisfied(nowElapsed, isIdle);
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
index 866dc41..0d85dfd 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
@@ -151,13 +151,12 @@
      */
     private static final int STATSD_CONSTRAINTS_TO_LOG = CONSTRAINT_CONTENT_TRIGGER
             | CONSTRAINT_DEADLINE
-            | CONSTRAINT_IDLE
             | CONSTRAINT_PREFETCH
             | CONSTRAINT_TARE_WEALTH
             | CONSTRAINT_TIMING_DELAY
             | CONSTRAINT_WITHIN_QUOTA;
 
-    // TODO(b/129954980)
+    // TODO(b/129954980): ensure this doesn't spam statsd, especially at boot
     private static final boolean STATS_LOG_ENABLED = false;
 
     // No override.
@@ -1864,7 +1863,7 @@
     }
 
     /** Returns a {@link JobServerProtoEnums.Constraint} enum value for the given constraint. */
-    private int getProtoConstraint(int constraint) {
+    static int getProtoConstraint(int constraint) {
         switch (constraint) {
             case CONSTRAINT_BACKGROUND_NOT_RESTRICTED:
                 return JobServerProtoEnums.CONSTRAINT_BACKGROUND_NOT_RESTRICTED;
@@ -1882,8 +1881,12 @@
                 return JobServerProtoEnums.CONSTRAINT_DEVICE_NOT_DOZING;
             case CONSTRAINT_IDLE:
                 return JobServerProtoEnums.CONSTRAINT_IDLE;
+            case CONSTRAINT_PREFETCH:
+                return JobServerProtoEnums.CONSTRAINT_PREFETCH;
             case CONSTRAINT_STORAGE_NOT_LOW:
                 return JobServerProtoEnums.CONSTRAINT_STORAGE_NOT_LOW;
+            case CONSTRAINT_TARE_WEALTH:
+                return JobServerProtoEnums.CONSTRAINT_TARE_WEALTH;
             case CONSTRAINT_TIMING_DELAY:
                 return JobServerProtoEnums.CONSTRAINT_TIMING_DELAY;
             case CONSTRAINT_WITHIN_QUOTA:
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java
index 2a2d602..8453e53 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java
@@ -26,6 +26,7 @@
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.FrameworkStatsLog;
 import com.android.server.job.JobSchedulerService;
 import com.android.server.job.JobSchedulerService.Constants;
 import com.android.server.job.StateChangedListener;
@@ -165,6 +166,15 @@
         return mService.areComponentsInPlaceLocked(jobStatus);
     }
 
+    protected void logDeviceWideConstraintStateToStatsd(int constraint, boolean satisfied) {
+        FrameworkStatsLog.write(
+                FrameworkStatsLog.DEVICE_WIDE_JOB_CONSTRAINT_CHANGED,
+                JobStatus.getProtoConstraint(constraint),
+                satisfied
+                        ? FrameworkStatsLog.DEVICE_WIDE_JOB_CONSTRAINT_CHANGED__STATE__SATISFIED
+                        : FrameworkStatsLog.DEVICE_WIDE_JOB_CONSTRAINT_CHANGED__STATE__UNSATISFIED);
+    }
+
     public abstract void dumpControllerStateLocked(IndentingPrintWriter pw,
             Predicate<JobStatus> predicate);
     public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index f5ee467..93c0c4d 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -3406,6 +3406,7 @@
     method @NonNull public android.window.WindowContainerTransaction createTaskFragment(@NonNull android.window.TaskFragmentCreationParams);
     method @NonNull public android.window.WindowContainerTransaction deleteTaskFragment(@NonNull android.window.WindowContainerToken);
     method public int describeContents();
+    method @NonNull public android.window.WindowContainerTransaction finishActivity(@NonNull android.os.IBinder);
     method @NonNull public android.window.WindowContainerTransaction removeTask(@NonNull android.window.WindowContainerToken);
     method @NonNull public android.window.WindowContainerTransaction reorder(@NonNull android.window.WindowContainerToken, boolean);
     method @NonNull public android.window.WindowContainerTransaction reparent(@NonNull android.window.WindowContainerToken, @Nullable android.window.WindowContainerToken, boolean);
diff --git a/core/java/android/service/dreams/DreamOverlayService.java b/core/java/android/service/dreams/DreamOverlayService.java
index 163d6ed..4324442 100644
--- a/core/java/android/service/dreams/DreamOverlayService.java
+++ b/core/java/android/service/dreams/DreamOverlayService.java
@@ -20,6 +20,7 @@
 import android.annotation.Nullable;
 import android.annotation.TestApi;
 import android.app.Service;
+import android.content.ComponentName;
 import android.content.Intent;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -36,6 +37,7 @@
     private static final String TAG = "DreamOverlayService";
     private static final boolean DEBUG = false;
     private boolean mShowComplications;
+    private ComponentName mDreamComponent;
 
     private IDreamOverlay mDreamOverlay = new IDreamOverlay.Stub() {
         @Override
@@ -56,6 +58,8 @@
     public final IBinder onBind(@NonNull Intent intent) {
         mShowComplications = intent.getBooleanExtra(DreamService.EXTRA_SHOW_COMPLICATIONS,
                 DreamService.DEFAULT_SHOW_COMPLICATIONS);
+        mDreamComponent = intent.getParcelableExtra(DreamService.EXTRA_DREAM_COMPONENT,
+                ComponentName.class);
         return mDreamOverlay.asBinder();
     }
 
@@ -84,4 +88,12 @@
     public final boolean shouldShowComplications() {
         return mShowComplications;
     }
+
+    /**
+     * Returns the active dream component.
+     * @hide
+     */
+    public final ComponentName getDreamComponent() {
+        return mDreamComponent;
+    }
 }
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index 7515538..d066ee7 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -218,6 +218,12 @@
             "android.service.dreams.SHOW_COMPLICATIONS";
 
     /**
+     * Extra containing the component name for the active dream.
+     * @hide
+     */
+    public static final String EXTRA_DREAM_COMPONENT = "android.service.dreams.DREAM_COMPONENT";
+
+    /**
      * The default value for whether to show complications on the overlay.
      * @hide
      */
@@ -271,6 +277,7 @@
             overlayIntent.setComponent(overlayService);
             overlayIntent.putExtra(EXTRA_SHOW_COMPLICATIONS,
                     fetchShouldShowComplications(context, serviceInfo));
+            overlayIntent.putExtra(EXTRA_DREAM_COMPONENT, dreamService);
 
             context.bindService(overlayIntent,
                     this, Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE);
diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java
index ffbdf08..cfad1af 100644
--- a/core/java/android/window/WindowContainerTransaction.java
+++ b/core/java/android/window/WindowContainerTransaction.java
@@ -706,6 +706,23 @@
     }
 
     /**
+     * Finishes the Activity.
+     * Comparing to directly calling {@link android.app.Activity#finish()}, calling this can make
+     * sure the finishing happens in the same transaction with other operations.
+     * @param activityToken activity to be finished.
+     */
+    @NonNull
+    public WindowContainerTransaction finishActivity(@NonNull IBinder activityToken) {
+        final HierarchyOp hierarchyOp =
+                new HierarchyOp.Builder(
+                        HierarchyOp.HIERARCHY_OP_TYPE_FINISH_ACTIVITY)
+                        .setContainer(activityToken)
+                        .build();
+        mHierarchyOps.add(hierarchyOp);
+        return this;
+    }
+
+    /**
      * Sets/removes the always on top flag for this {@code windowContainer}. See
      * {@link com.android.server.wm.ConfigurationContainer#setAlwaysOnTop(boolean)}.
      * Please note that this method is only intended to be used for a
@@ -1163,6 +1180,7 @@
         public static final int HIERARCHY_OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT = 18;
         public static final int HIERARCHY_OP_TYPE_SET_ALWAYS_ON_TOP = 19;
         public static final int HIERARCHY_OP_TYPE_REMOVE_TASK = 20;
+        public static final int HIERARCHY_OP_TYPE_FINISH_ACTIVITY = 21;
 
         // The following key(s) are for use with mLaunchOptions:
         // When launching a task (eg. from recents), this is the taskId to be launched.
@@ -1484,6 +1502,8 @@
                             + " alwaysOnTop=" + mAlwaysOnTop + "}";
                 case HIERARCHY_OP_TYPE_REMOVE_TASK:
                     return "{RemoveTask: task=" + mContainer + "}";
+                case HIERARCHY_OP_TYPE_FINISH_ACTIVITY:
+                    return "{finishActivity: activity=" + mContainer + "}";
                 default:
                     return "{mType=" + mType + " container=" + mContainer + " reparent=" + mReparent
                             + " mToTop=" + mToTop
diff --git a/core/java/com/android/internal/app/ILogAccessDialogCallback.aidl b/core/java/com/android/internal/app/ILogAccessDialogCallback.aidl
new file mode 100644
index 0000000..b2236c9
--- /dev/null
+++ b/core/java/com/android/internal/app/ILogAccessDialogCallback.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2022 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.internal.app;
+
+/**
+ * IPC interface for an application to receive callbacks from the log access dialog callback.
+ */
+oneway interface ILogAccessDialogCallback {
+    void approveAccessForClient(int uid, String packageName);
+    void declineAccessForClient(int uid, String packageName);
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/logcat/LogAccessDialogActivity.java b/core/java/com/android/internal/app/LogAccessDialogActivity.java
similarity index 71%
rename from services/core/java/com/android/server/logcat/LogAccessDialogActivity.java
rename to core/java/com/android/internal/app/LogAccessDialogActivity.java
index 811e96c..4adb867 100644
--- a/services/core/java/com/android/server/logcat/LogAccessDialogActivity.java
+++ b/core/java/com/android/internal/app/LogAccessDialogActivity.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.logcat;
+package com.android.internal.app;
 
 import android.annotation.StyleRes;
 import android.app.Activity;
@@ -27,7 +27,14 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.RemoteException;
 import android.os.UserHandle;
+import android.text.Html;
+import android.text.Spannable;
+import android.text.TextUtils;
+import android.text.method.LinkMovementMethod;
+import android.text.style.TypefaceSpan;
+import android.text.style.URLSpan;
 import android.util.Slog;
 import android.view.ContextThemeWrapper;
 import android.view.InflateException;
@@ -37,7 +44,6 @@
 import android.widget.TextView;
 
 import com.android.internal.R;
-import com.android.server.LocalServices;
 
 /**
  * Dialog responsible for obtaining user consent per-use log access
@@ -45,17 +51,19 @@
 public class LogAccessDialogActivity extends Activity implements
         View.OnClickListener {
     private static final String TAG = LogAccessDialogActivity.class.getSimpleName();
+    public static final String EXTRA_CALLBACK = "EXTRA_CALLBACK";
+
 
     private static final int DIALOG_TIME_OUT = Build.IS_DEBUGGABLE ? 60000 : 300000;
     private static final int MSG_DISMISS_DIALOG = 0;
 
-    private final LogcatManagerService.LogcatManagerServiceInternal mLogcatManagerInternal =
-            LocalServices.getService(LogcatManagerService.LogcatManagerServiceInternal.class);
-
     private String mPackageName;
     private int mUid;
+    private ILogAccessDialogCallback mCallback;
 
     private String mAlertTitle;
+    private String mAlertBody;
+    private String mAlertLearnMore;
     private AlertDialog.Builder mAlertDialog;
     private AlertDialog mAlert;
     private View mAlertView;
@@ -81,6 +89,9 @@
             return;
         }
 
+        mAlertBody = getResources().getString(R.string.log_access_confirmation_body);
+        mAlertLearnMore = getResources().getString(R.string.log_access_confirmation_learn_more);
+
         // create View
         boolean isDarkTheme = (getResources().getConfiguration().uiMode
                 & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
@@ -118,6 +129,13 @@
             return false;
         }
 
+        mCallback = ILogAccessDialogCallback.Stub.asInterface(
+                intent.getExtras().getBinder(EXTRA_CALLBACK));
+        if (mCallback == null) {
+            Slog.e(TAG, "Missing callback");
+            return false;
+        }
+
         mPackageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME);
         if (mPackageName == null || mPackageName.length() == 0) {
             Slog.e(TAG, "Missing package name extra");
@@ -165,13 +183,22 @@
         return titleString;
     }
 
+    private Spannable styleFont(String text) {
+        Spannable s = (Spannable) Html.fromHtml(text);
+        for (URLSpan span : s.getSpans(0, s.length(), URLSpan.class)) {
+            TypefaceSpan typefaceSpan = new TypefaceSpan("google-sans");
+            s.setSpan(typefaceSpan, s.getSpanStart(span), s.getSpanEnd(span), 0);
+        }
+        return s;
+    }
+
     /**
      * Returns the dialog view.
      * If we cannot retrieve the package name, it returns null and we decline the full device log
      * access
      */
     private View createView(@StyleRes int themeId) {
-        Context themedContext = new ContextThemeWrapper(getApplicationContext(), themeId);
+        Context themedContext = new ContextThemeWrapper(this, themeId);
         final View view = LayoutInflater.from(themedContext).inflate(
                 R.layout.log_access_user_consent_dialog_permission, null /*root*/);
 
@@ -182,6 +209,19 @@
         ((TextView) view.findViewById(R.id.log_access_dialog_title))
             .setText(mAlertTitle);
 
+        if (!TextUtils.isEmpty(mAlertLearnMore)) {
+            Spannable mSpannableLearnMore = styleFont(mAlertLearnMore);
+
+            ((TextView) view.findViewById(R.id.log_access_dialog_body))
+                    .setText(TextUtils.concat(mAlertBody, "\n\n", mSpannableLearnMore));
+
+            ((TextView) view.findViewById(R.id.log_access_dialog_body))
+                    .setMovementMethod(LinkMovementMethod.getInstance());
+        } else {
+            ((TextView) view.findViewById(R.id.log_access_dialog_body))
+                    .setText(mAlertBody);
+        }
+
         Button button_allow = (Button) view.findViewById(R.id.log_access_dialog_allow_button);
         button_allow.setOnClickListener(this);
 
@@ -194,19 +234,27 @@
 
     @Override
     public void onClick(View view) {
-        switch (view.getId()) {
-            case R.id.log_access_dialog_allow_button:
-                mLogcatManagerInternal.approveAccessForClient(mUid, mPackageName);
-                finish();
-                break;
-            case R.id.log_access_dialog_deny_button:
-                declineLogAccess();
-                finish();
-                break;
+        try {
+            switch (view.getId()) {
+                case R.id.log_access_dialog_allow_button:
+                    mCallback.approveAccessForClient(mUid, mPackageName);
+                    finish();
+                    break;
+                case R.id.log_access_dialog_deny_button:
+                    declineLogAccess();
+                    finish();
+                    break;
+            }
+        } catch (RemoteException e) {
+            finish();
         }
     }
 
     private void declineLogAccess() {
-        mLogcatManagerInternal.declineAccessForClient(mUid, mPackageName);
+        try {
+            mCallback.declineAccessForClient(mUid, mPackageName);
+        } catch (RemoteException e) {
+            finish();
+        }
     }
 }
diff --git a/core/java/com/android/internal/app/procstats/ProcessState.java b/core/java/com/android/internal/app/procstats/ProcessState.java
index 87e8ac1..72b9cd2 100644
--- a/core/java/com/android/internal/app/procstats/ProcessState.java
+++ b/core/java/com/android/internal/app/procstats/ProcessState.java
@@ -473,7 +473,10 @@
                 }
             }
             mCurCombinedState = state;
-            mStats.mUidStates.get(mUid).updateCombinedState(state, now);
+            final UidState uidState = mStats.mUidStates.get(mUid);
+            if (uidState != null) {
+                uidState.updateCombinedState(state, now);
+            }
         }
     }
 
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index 72de78c..708713b 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -29,7 +29,9 @@
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_LAUNCH_FROM_WIDGET;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_OPEN_ALL_APPS;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_QUICK_SWITCH;
+import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_UNLOCK_ENTRANCE_ANIMATION;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_LAUNCH_CAMERA;
+import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_OCCLUSION;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_PASSWORD_APPEAR;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_PASSWORD_DISAPPEAR;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_PATTERN_APPEAR;
@@ -219,6 +221,8 @@
     public static final int CUJ_TASKBAR_EXPAND = 60;
     public static final int CUJ_TASKBAR_COLLAPSE = 61;
     public static final int CUJ_SHADE_CLEAR_ALL = 62;
+    public static final int CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION = 63;
+    public static final int CUJ_LOCKSCREEN_OCCLUSION = 64;
 
     private static final int NO_STATSD_LOGGING = -1;
 
@@ -290,6 +294,8 @@
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__TASKBAR_EXPAND,
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__TASKBAR_COLLAPSE,
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_CLEAR_ALL,
+            UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_UNLOCK_ENTRANCE_ANIMATION,
+            UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_OCCLUSION,
     };
 
     private static volatile InteractionJankMonitor sInstance;
@@ -372,7 +378,9 @@
             CUJ_USER_DIALOG_OPEN,
             CUJ_TASKBAR_EXPAND,
             CUJ_TASKBAR_COLLAPSE,
-            CUJ_SHADE_CLEAR_ALL
+            CUJ_SHADE_CLEAR_ALL,
+            CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION,
+            CUJ_LOCKSCREEN_OCCLUSION
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CujType {
@@ -864,6 +872,10 @@
                 return "TASKBAR_COLLAPSE";
             case CUJ_SHADE_CLEAR_ALL:
                 return "SHADE_CLEAR_ALL";
+            case CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION:
+                return "LAUNCHER_UNLOCK_ENTRANCE_ANIMATION";
+            case CUJ_LOCKSCREEN_OCCLUSION:
+                return "LOCKSCREEN_OCCLUSION";
         }
         return "UNKNOWN";
     }
diff --git a/core/java/com/android/internal/util/LatencyTracker.java b/core/java/com/android/internal/util/LatencyTracker.java
index d3f9e0a..8fcb6d5 100644
--- a/core/java/com/android/internal/util/LatencyTracker.java
+++ b/core/java/com/android/internal/util/LatencyTracker.java
@@ -29,6 +29,7 @@
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_ROTATE_SCREEN_SENSOR;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_SHOW_BACK_ARROW;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_SHOW_SELECTION_TOOLBAR;
+import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_SHOW_VOICE_INTERACTION;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_START_RECENTS_ANIMATION;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_SWITCH_DISPLAY_UNFOLD;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_TOGGLE_RECENTS;
@@ -174,6 +175,12 @@
      */
     public static final int ACTION_FOLD_TO_AOD = 18;
 
+    /**
+     * Time it takes to show the {@link android.service.voice.VoiceInteractionSession} system UI
+     * after a {@link android.hardware.soundtrigger3.ISoundTriggerHw} voice trigger.
+     */
+    public static final int ACTION_SHOW_VOICE_INTERACTION = 19;
+
     private static final int[] ACTIONS_ALL = {
         ACTION_EXPAND_PANEL,
         ACTION_TOGGLE_RECENTS,
@@ -194,6 +201,7 @@
         ACTION_LOAD_SHARE_SHEET,
         ACTION_SHOW_SELECTION_TOOLBAR,
         ACTION_FOLD_TO_AOD,
+        ACTION_SHOW_VOICE_INTERACTION,
     };
 
     /** @hide */
@@ -217,6 +225,7 @@
         ACTION_LOAD_SHARE_SHEET,
         ACTION_SHOW_SELECTION_TOOLBAR,
         ACTION_FOLD_TO_AOD,
+        ACTION_SHOW_VOICE_INTERACTION,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface Action {
@@ -243,6 +252,7 @@
             UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOAD_SHARE_SHEET,
             UIACTION_LATENCY_REPORTED__ACTION__ACTION_SHOW_SELECTION_TOOLBAR,
             UIACTION_LATENCY_REPORTED__ACTION__ACTION_FOLD_TO_AOD,
+            UIACTION_LATENCY_REPORTED__ACTION__ACTION_SHOW_VOICE_INTERACTION,
     };
 
     private static LatencyTracker sLatencyTracker;
@@ -340,6 +350,8 @@
                 return "ACTION_SHOW_SELECTION_TOOLBAR";
             case UIACTION_LATENCY_REPORTED__ACTION__ACTION_FOLD_TO_AOD:
                 return "ACTION_FOLD_TO_AOD";
+            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_SHOW_VOICE_INTERACTION:
+                return "ACTION_SHOW_VOICE_INTERACTION";
             default:
                 throw new IllegalArgumentException("Invalid action");
         }
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index b91bd18..5ae133b 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -6783,8 +6783,9 @@
                   android:exported="false">
         </activity>
 
-        <activity android:name="com.android.server.logcat.LogAccessDialogActivity"
+        <activity android:name="com.android.internal.app.LogAccessDialogActivity"
                   android:theme="@style/Theme.Translucent.NoTitleBar"
+                  android:process=":ui"
                   android:excludeFromRecents="true"
                   android:exported="false">
         </activity>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 6ec98e8..5763345 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -5755,10 +5755,21 @@
     <string name="log_access_confirmation_deny">Don\u2019t allow</string>
 
     <!-- Content for the log access confirmation dialog. [CHAR LIMIT=NONE]-->
-    <string name="log_access_confirmation_body">Device logs record what happens on your device. Apps can use these logs to find and fix issues.\n\nSome logs may contain sensitive info, so only allow apps you trust to access all device logs.
+    <string name="log_access_confirmation_body" product="default">Device logs record what happens on your device. Apps can use these logs to find and fix issues.\n\nSome logs may contain sensitive info, so only allow apps you trust to access all device logs.
         \n\nIf you don’t allow this app to access all device logs, it can still access its own logs. Your device manufacturer may still be able to access some logs or info on your device.
     </string>
 
+    <!-- Content for the log access confirmation dialog. [CHAR LIMIT=NONE]-->
+    <string name="log_access_confirmation_body" product="tv">Device logs record what happens on your device. Apps can use these logs to find and fix issues.\n\nSome logs may contain sensitive info, so only allow apps you trust to access all device logs.
+        \n\nIf you don’t allow this app to access all device logs, it can still access its own logs. Your device manufacturer may still be able to access some logs or info on your device.\n\nLearn more at g.co/android/devicelogs.
+    </string>
+
+    <!-- Learn more URL for the log access confirmation dialog. [DO NOT TRANSLATE]-->
+    <string name="log_access_confirmation_learn_more" product="default" translatable="false">&lt;a href="https://support.google.com/android?p=system_logs#topic=7313011"&gt;Learn more&lt;/a&gt;</string>
+
+    <!-- Learn more URL for the log access confirmation dialog. [DO NOT TRANSLATE]-->
+    <string name="log_access_confirmation_learn_more" product="tv" translatable="false"></string>
+
     <!-- Privacy notice do not show [CHAR LIMIT=20] -->
     <string name="log_access_do_not_show_again">Don\u2019t show again</string>
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 546bb21..6e574bd 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3925,8 +3925,10 @@
   <java-symbol type="string" name="log_access_confirmation_deny" />
   <java-symbol type="string" name="log_access_confirmation_title" />
   <java-symbol type="string" name="log_access_confirmation_body" />
+  <java-symbol type="string" name="log_access_confirmation_learn_more" />
   <java-symbol type="layout" name="log_access_user_consent_dialog_permission" />
   <java-symbol type="id" name="log_access_dialog_title" />
+  <java-symbol type="id" name="log_access_dialog_body" />
   <java-symbol type="id" name="log_access_dialog_allow_button" />
   <java-symbol type="id" name="log_access_dialog_deny_button" />
 
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
index 626e0d9..18712ae 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -432,7 +432,7 @@
                     // In case we have requested to reparent the activity to another container (as
                     // pendingAppeared), we don't want to finish it with this container.
                     && mController.getContainerWithActivity(activity) == this) {
-                activity.finish();
+                wct.finishActivity(activity.getActivityToken());
             }
         }
 
@@ -457,7 +457,7 @@
                     || controller.shouldRetainAssociatedActivity(this, activity)) {
                 continue;
             }
-            activity.finish();
+            wct.finishActivity(activity.getActivityToken());
         }
         mActivitiesToFinishOnExit.clear();
     }
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
index 179696a..25d0347 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
@@ -207,7 +207,7 @@
 
         verify(mSplitPresenter, never()).deleteTaskFragment(any(), any());
         verify(mSplitController).removeContainer(tf);
-        verify(mActivity, never()).finish();
+        verify(mTransaction, never()).finishActivity(any());
     }
 
     @Test
@@ -1004,9 +1004,9 @@
         assertTrue(primaryContainer.isFinished());
         assertTrue(secondaryContainer0.isFinished());
         assertTrue(secondaryContainer1.isFinished());
-        verify(mActivity).finish();
-        verify(secondaryActivity0).finish();
-        verify(secondaryActivity1).finish();
+        verify(mTransaction).finishActivity(mActivity.getActivityToken());
+        verify(mTransaction).finishActivity(secondaryActivity0.getActivityToken());
+        verify(mTransaction).finishActivity(secondaryActivity1.getActivityToken());
         assertTrue(taskContainer.mContainers.isEmpty());
         assertTrue(taskContainer.mSplitContainers.isEmpty());
     }
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
index 73428a2..35415d8 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
@@ -107,30 +107,29 @@
         final TaskFragmentContainer container = new TaskFragmentContainer(mActivity,
                 null /* pendingAppearedIntent */, taskContainer, mController);
         doReturn(container).when(mController).getContainerWithActivity(mActivity);
-        final WindowContainerTransaction wct = new WindowContainerTransaction();
 
         // Only remove the activity, but not clear the reference until appeared.
-        container.finish(true /* shouldFinishDependent */, mPresenter, wct, mController);
+        container.finish(true /* shouldFinishDependent */, mPresenter, mTransaction, mController);
 
-        verify(mActivity).finish();
+        verify(mTransaction).finishActivity(mActivity.getActivityToken());
         verify(mPresenter, never()).deleteTaskFragment(any(), any());
         verify(mController, never()).removeContainer(any());
 
         // Calling twice should not finish activity again.
-        clearInvocations(mActivity);
-        container.finish(true /* shouldFinishDependent */, mPresenter, wct, mController);
+        clearInvocations(mTransaction);
+        container.finish(true /* shouldFinishDependent */, mPresenter, mTransaction, mController);
 
-        verify(mActivity, never()).finish();
+        verify(mTransaction, never()).finishActivity(any());
         verify(mPresenter, never()).deleteTaskFragment(any(), any());
         verify(mController, never()).removeContainer(any());
 
         // Remove all references after the container has appeared in server.
         doReturn(new ArrayList<>()).when(mInfo).getActivities();
         container.setInfo(mTransaction, mInfo);
-        container.finish(true /* shouldFinishDependent */, mPresenter, wct, mController);
+        container.finish(true /* shouldFinishDependent */, mPresenter, mTransaction, mController);
 
-        verify(mActivity, never()).finish();
-        verify(mPresenter).deleteTaskFragment(wct, container.getTaskFragmentToken());
+        verify(mTransaction, never()).finishActivity(any());
+        verify(mPresenter).deleteTaskFragment(mTransaction, container.getTaskFragmentToken());
         verify(mController).removeContainer(container);
     }
 
@@ -150,7 +149,7 @@
         // The activity is requested to be reparented, so don't finish it.
         container0.finish(true /* shouldFinishDependent */, mPresenter, wct, mController);
 
-        verify(mActivity, never()).finish();
+        verify(mTransaction, never()).finishActivity(any());
         verify(mPresenter).deleteTaskFragment(wct, container0.getTaskFragmentToken());
         verify(mController).removeContainer(container0);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index b5a5754..922472a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -59,6 +59,8 @@
 public class Bubble implements BubbleViewProvider {
     private static final String TAG = "Bubble";
 
+    public static final String KEY_APP_BUBBLE = "key_app_bubble";
+
     private final String mKey;
     @Nullable
     private final String mGroupKey;
@@ -164,6 +166,14 @@
     private PendingIntent mDeleteIntent;
 
     /**
+     * Used only for a special bubble in the stack that has the key {@link #KEY_APP_BUBBLE}.
+     * There can only be one of these bubbles in the stack and this intent will be populated for
+     * that bubble.
+     */
+    @Nullable
+    private Intent mAppIntent;
+
+    /**
      * 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.
      */
@@ -192,6 +202,22 @@
         mBubbleMetadataFlagListener = listener;
     }
 
+    public Bubble(Intent intent,
+            UserHandle user,
+            Executor mainExecutor) {
+        mKey = KEY_APP_BUBBLE;
+        mGroupKey = null;
+        mLocusId = null;
+        mFlags = 0;
+        mUser = user;
+        mShowBubbleUpdateDot = false;
+        mMainExecutor = mainExecutor;
+        mTaskId = INVALID_TASK_ID;
+        mAppIntent = intent;
+        mDesiredHeight = Integer.MAX_VALUE;
+        mPackageName = intent.getPackage();
+    }
+
     @VisibleForTesting(visibility = PRIVATE)
     public Bubble(@NonNull final BubbleEntry entry,
             final Bubbles.BubbleMetadataFlagListener listener,
@@ -417,6 +443,9 @@
 
         mShortcutInfo = info.shortcutInfo;
         mAppName = info.appName;
+        if (mTitle == null) {
+            mTitle = mAppName;
+        }
         mFlyoutMessage = info.flyoutMessage;
 
         mBadgeBitmap = info.badgeBitmap;
@@ -520,7 +549,7 @@
      * @return the last time this bubble was updated or accessed, whichever is most recent.
      */
     long getLastActivity() {
-        return Math.max(mLastUpdated, mLastAccessed);
+        return isAppBubble() ? Long.MAX_VALUE : Math.max(mLastUpdated, mLastAccessed);
     }
 
     /**
@@ -719,6 +748,15 @@
         return mDeleteIntent;
     }
 
+    @Nullable
+    Intent getAppBubbleIntent() {
+        return mAppIntent;
+    }
+
+    boolean isAppBubble() {
+        return KEY_APP_BUBBLE.equals(mKey);
+    }
+
     Intent getSettingsIntent(final Context context) {
         final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS);
         intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 0dfba34..93413db 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -1017,6 +1017,20 @@
     }
 
     /**
+     * Adds a bubble for a specific intent. These bubbles are <b>not</b> backed by a notification
+     * and remain until the user dismisses the bubble or bubble stack. Only one intent bubble
+     * is supported at a time.
+     *
+     * @param intent the intent to display in the bubble expanded view.
+     */
+    public void addAppBubble(Intent intent) {
+        if (intent == null || intent.getPackage() == null) return;
+        Bubble b = new Bubble(intent, UserHandle.of(mCurrentUserId), mMainExecutor);
+        b.setShouldAutoExpand(true);
+        inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false);
+    }
+
+    /**
      * Fills the overflow bubbles by loading them from disk.
      */
     void loadOverflowBubblesFromDisk() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
index 840b285..06f232c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
@@ -224,15 +224,23 @@
                 try {
                     options.setTaskAlwaysOnTop(true);
                     options.setLaunchedFromBubble(true);
-                    if (!mIsOverflow && mBubble.hasMetadataShortcutId()) {
+
+                    Intent fillInIntent = new Intent();
+                    // Apply flags to make behaviour match documentLaunchMode=always.
+                    fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT);
+                    fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+
+                    if (mBubble.isAppBubble()) {
+                        PendingIntent pi = PendingIntent.getActivity(mContext, 0,
+                                mBubble.getAppBubbleIntent(),
+                                PendingIntent.FLAG_MUTABLE,
+                                null);
+                        mTaskView.startActivity(pi, fillInIntent, options, launchBounds);
+                    } else if (!mIsOverflow && mBubble.hasMetadataShortcutId()) {
                         options.setApplyActivityFlagsForBubbles(true);
                         mTaskView.startShortcutActivity(mBubble.getShortcutInfo(),
                                 options, launchBounds);
                     } else {
-                        Intent fillInIntent = new Intent();
-                        // Apply flags to make behaviour match documentLaunchMode=always.
-                        fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT);
-                        fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
                         if (mBubble != null) {
                             mBubble.setIntentActive();
                         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index c9d523c..33074de 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -591,6 +591,7 @@
             ShellInit shellInit,
             ShellController shellController,
             ShellCommandHandler shellCommandHandler,
+            Optional<BubbleController> bubbleController,
             WindowManager windowManager,
             ShellTaskOrganizer organizer,
             TaskViewTransitions taskViewTransitions,
@@ -602,6 +603,7 @@
                     shellInit,
                     shellController,
                     shellCommandHandler,
+                    bubbleController,
                     windowManager,
                     organizer,
                     taskViewTransitions,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasksController.java
index c79b9b8..6755299 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasksController.java
@@ -39,6 +39,7 @@
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.TaskViewTransitions;
+import com.android.wm.shell.bubbles.BubbleController;
 import com.android.wm.shell.common.RemoteCallable;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
@@ -54,6 +55,7 @@
 
 import java.io.PrintWriter;
 import java.util.Objects;
+import java.util.Optional;
 
 /**
  * Entry point for creating and managing floating tasks.
@@ -70,6 +72,8 @@
 
     public static final boolean FLOATING_TASKS_ENABLED =
             SystemProperties.getBoolean("persist.wm.debug.floating_tasks", false);
+    public static final boolean SHOW_FLOATING_TASKS_AS_BUBBLES =
+            SystemProperties.getBoolean("persist.wm.debug.floating_tasks_as_bubbles", false);
 
     @VisibleForTesting
     static final int SMALLEST_SCREEN_WIDTH_DP_TO_BE_TABLET = 600;
@@ -82,6 +86,7 @@
     private Context mContext;
     private ShellController mShellController;
     private ShellCommandHandler mShellCommandHandler;
+    private @Nullable BubbleController mBubbleController;
     private WindowManager mWindowManager;
     private ShellTaskOrganizer mTaskOrganizer;
     private TaskViewTransitions mTaskViewTransitions;
@@ -111,6 +116,7 @@
             ShellInit shellInit,
             ShellController shellController,
             ShellCommandHandler shellCommandHandler,
+            Optional<BubbleController> bubbleController,
             WindowManager windowManager,
             ShellTaskOrganizer organizer,
             TaskViewTransitions transitions,
@@ -120,6 +126,7 @@
         mContext = context;
         mShellController = shellController;
         mShellCommandHandler = shellCommandHandler;
+        mBubbleController = bubbleController.get();
         mWindowManager = windowManager;
         mTaskOrganizer = organizer;
         mTaskViewTransitions = transitions;
@@ -187,6 +194,22 @@
         return true;
     }
 
+    /** Returns true if the task was or should be shown as a bubble. */
+    private boolean maybeShowTaskAsBubble(Intent intent) {
+        if (SHOW_FLOATING_TASKS_AS_BUBBLES && mBubbleController != null) {
+            removeFloatingLayer();
+            if (intent.getPackage() != null) {
+                mBubbleController.addAppBubble(intent);
+                ProtoLog.d(WM_SHELL_FLOATING_APPS, "showing floating task as bubble: %s", intent);
+            } else {
+                ProtoLog.d(WM_SHELL_FLOATING_APPS,
+                        "failed to show floating task as bubble: %s; unknown package", intent);
+            }
+            return true;
+        }
+        return false;
+    }
+
     /**
      * Shows, stashes, or un-stashes the floating task depending on state:
      * - If there is no floating task for this intent, it shows this the provided task.
@@ -195,6 +218,7 @@
      */
     public void showOrSetStashed(Intent intent) {
         if (!canShowTask(intent)) return;
+        if (maybeShowTaskAsBubble(intent)) return;
 
         addFloatingLayer();
 
@@ -215,6 +239,7 @@
      */
     public void showTask(Intent intent) {
         if (!canShowTask(intent)) return;
+        if (maybeShowTaskAsBubble(intent)) return;
 
         addFloatingLayer();
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/floating/FloatingTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/floating/FloatingTasksControllerTest.java
index f1b4311..a88c837 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/floating/FloatingTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/floating/FloatingTasksControllerTest.java
@@ -62,6 +62,8 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.Optional;
+
 /**
  * Tests for the floating tasks controller.
  */
@@ -123,6 +125,7 @@
                 mShellInit,
                 mShellController,
                 mock(ShellCommandHandler.class),
+                Optional.empty(),
                 mWindowManager,
                 mTaskOrganizer,
                 mock(TaskViewTransitions.class),
diff --git a/media/TEST_MAPPING b/media/TEST_MAPPING
index 05fbc7a..23ee505 100644
--- a/media/TEST_MAPPING
+++ b/media/TEST_MAPPING
@@ -1,6 +1,9 @@
 {
   "presubmit": [
     {
+      "name": "mediaroutertest"
+    },
+    {
       "name": "CtsCameraTestCases",
       "options" : [
         {
diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java
index d8995b4..891ab45 100644
--- a/media/java/android/media/MediaRouter2.java
+++ b/media/java/android/media/MediaRouter2.java
@@ -48,6 +48,7 @@
 import java.util.Set;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
 
@@ -118,6 +119,7 @@
     private final Map<String, RoutingController> mNonSystemRoutingControllers = new ArrayMap<>();
 
     private final AtomicInteger mNextRequestId = new AtomicInteger(1);
+    private final AtomicBoolean mIsScanning = new AtomicBoolean(/* initialValue= */ false);
 
     final Handler mHandler;
 
@@ -234,7 +236,9 @@
     @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
     public void startScan() {
         if (isSystemRouter()) {
-            sManager.startScan();
+            if (!mIsScanning.getAndSet(true)) {
+                sManager.registerScanRequest();
+            }
         }
     }
 
@@ -260,7 +264,9 @@
     @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
     public void stopScan() {
         if (isSystemRouter()) {
-            sManager.stopScan();
+            if (mIsScanning.getAndSet(false)) {
+                sManager.unregisterScanRequest();
+            }
         }
     }
 
diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java
index 071667a..d79740c 100644
--- a/media/java/android/media/MediaRouter2Manager.java
+++ b/media/java/android/media/MediaRouter2Manager.java
@@ -79,9 +79,11 @@
     final String mPackageName;
 
     private final Context mContext;
-    @GuardedBy("sLock")
-    private Client mClient;
+
+    private final Client mClient;
+
     private final IMediaRouterService mMediaRouterService;
+    private final AtomicInteger mScanRequestCount = new AtomicInteger(/* initialValue= */ 0);
     final Handler mHandler;
     final CopyOnWriteArrayList<CallbackRecord> mCallbackRecords = new CopyOnWriteArrayList<>();
 
@@ -119,7 +121,12 @@
                 .getSystemService(Context.MEDIA_SESSION_SERVICE);
         mPackageName = mContext.getPackageName();
         mHandler = new Handler(context.getMainLooper());
-        mHandler.post(this::getOrCreateClient);
+        mClient = new Client();
+        try {
+            mMediaRouterService.registerManager(mClient, mPackageName);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
+        }
     }
 
     /**
@@ -155,48 +162,47 @@
     }
 
     /**
-     * Starts scanning remote routes.
-     * <p>
-     * Route discovery can happen even when the {@link #startScan()} is not called.
-     * This is because the scanning could be started before by other apps.
-     * Therefore, calling this method after calling {@link #stopScan()} does not necessarily mean
-     * that the routes found before are removed and added again.
-     * <p>
-     * Use {@link Callback} to get the route related events.
-     * <p>
-     * @see #stopScan()
+     * Registers a request to scan for remote routes.
+     *
+     * <p>Increases the count of active scanning requests. When the count transitions from zero to
+     * one, sends a request to the system server to start scanning.
+     *
+     * <p>Clients must {@link #unregisterScanRequest() unregister their scan requests} when scanning
+     * is no longer needed, to avoid unnecessary resource usage.
      */
-    public void startScan() {
-        Client client = getOrCreateClient();
-        if (client != null) {
+    public void registerScanRequest() {
+        if (mScanRequestCount.getAndIncrement() == 0) {
             try {
-                mMediaRouterService.startScan(client);
+                mMediaRouterService.startScan(mClient);
             } catch (RemoteException ex) {
-                Log.e(TAG, "Unable to get sessions. Service probably died.", ex);
+                throw ex.rethrowFromSystemServer();
             }
         }
     }
 
     /**
-     * Stops scanning remote routes to reduce resource consumption.
-     * <p>
-     * Route discovery can be continued even after this method is called.
-     * This is because the scanning is only turned off when all the apps stop scanning.
-     * Therefore, calling this method does not necessarily mean the routes are removed.
-     * Also, for the same reason it does not mean that {@link Callback#onRoutesAdded(List)}
-     * is not called afterwards.
-     * <p>
-     * Use {@link Callback} to get the route related events.
+     * Unregisters a scan request made by {@link #registerScanRequest()}.
      *
-     * @see #startScan()
+     * <p>Decreases the count of active scanning requests. When the count transitions from one to
+     * zero, sends a request to the system server to stop scanning.
+     *
+     * @throws IllegalStateException If called while there are no active scan requests.
      */
-    public void stopScan() {
-        Client client = getOrCreateClient();
-        if (client != null) {
+    public void unregisterScanRequest() {
+        if (mScanRequestCount.updateAndGet(
+                count -> {
+                    if (count == 0) {
+                        throw new IllegalStateException(
+                                "No active scan requests to unregister.");
+                    } else {
+                        return --count;
+                    }
+                })
+                == 0) {
             try {
-                mMediaRouterService.stopScan(client);
+                mMediaRouterService.stopScan(mClient);
             } catch (RemoteException ex) {
-                Log.e(TAG, "Unable to get sessions. Service probably died.", ex);
+                throw ex.rethrowFromSystemServer();
             }
         }
     }
@@ -358,12 +364,10 @@
     @Nullable
     public RoutingSessionInfo getSystemRoutingSession(@Nullable String packageName) {
         try {
-            return mMediaRouterService.getSystemSessionInfoForPackage(
-                    getOrCreateClient(), packageName);
+            return mMediaRouterService.getSystemSessionInfoForPackage(mClient, packageName);
         } catch (RemoteException ex) {
-            Log.e(TAG, "Unable to get current system session info", ex);
+            throw ex.rethrowFromSystemServer();
         }
-        return null;
     }
 
     /**
@@ -424,15 +428,11 @@
      */
     @NonNull
     public List<RoutingSessionInfo> getRemoteSessions() {
-        Client client = getOrCreateClient();
-        if (client != null) {
-            try {
-                return mMediaRouterService.getRemoteSessions(client);
-            } catch (RemoteException ex) {
-                Log.e(TAG, "Unable to get sessions. Service probably died.", ex);
-            }
+        try {
+            return mMediaRouterService.getRemoteSessions(mClient);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
         }
-        return Collections.emptyList();
     }
 
     /**
@@ -515,14 +515,11 @@
             return;
         }
 
-        Client client = getOrCreateClient();
-        if (client != null) {
-            try {
-                int requestId = mNextRequestId.getAndIncrement();
-                mMediaRouterService.setRouteVolumeWithManager(client, requestId, route, volume);
-            } catch (RemoteException ex) {
-                Log.e(TAG, "Unable to set route volume.", ex);
-            }
+        try {
+            int requestId = mNextRequestId.getAndIncrement();
+            mMediaRouterService.setRouteVolumeWithManager(mClient, requestId, route, volume);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
         }
     }
 
@@ -544,15 +541,12 @@
             return;
         }
 
-        Client client = getOrCreateClient();
-        if (client != null) {
-            try {
-                int requestId = mNextRequestId.getAndIncrement();
-                mMediaRouterService.setSessionVolumeWithManager(
-                        client, requestId, sessionInfo.getId(), volume);
-            } catch (RemoteException ex) {
-                Log.e(TAG, "Unable to set session volume.", ex);
-            }
+        try {
+            int requestId = mNextRequestId.getAndIncrement();
+            mMediaRouterService.setSessionVolumeWithManager(
+                    mClient, requestId, sessionInfo.getId(), volume);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
         }
     }
 
@@ -809,15 +803,12 @@
             return;
         }
 
-        Client client = getOrCreateClient();
-        if (client != null) {
-            try {
-                int requestId = mNextRequestId.getAndIncrement();
-                mMediaRouterService.selectRouteWithManager(
-                        client, requestId, sessionInfo.getId(), route);
-            } catch (RemoteException ex) {
-                Log.e(TAG, "selectRoute: Failed to send a request.", ex);
-            }
+        try {
+            int requestId = mNextRequestId.getAndIncrement();
+            mMediaRouterService.selectRouteWithManager(
+                    mClient, requestId, sessionInfo.getId(), route);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
         }
     }
 
@@ -851,15 +842,12 @@
             return;
         }
 
-        Client client = getOrCreateClient();
-        if (client != null) {
-            try {
-                int requestId = mNextRequestId.getAndIncrement();
-                mMediaRouterService.deselectRouteWithManager(
-                        client, requestId, sessionInfo.getId(), route);
-            } catch (RemoteException ex) {
-                Log.e(TAG, "deselectRoute: Failed to send a request.", ex);
-            }
+        try {
+            int requestId = mNextRequestId.getAndIncrement();
+            mMediaRouterService.deselectRouteWithManager(
+                    mClient, requestId, sessionInfo.getId(), route);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
         }
     }
 
@@ -876,15 +864,11 @@
     public void releaseSession(@NonNull RoutingSessionInfo sessionInfo) {
         Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
 
-        Client client = getOrCreateClient();
-        if (client != null) {
-            try {
-                int requestId = mNextRequestId.getAndIncrement();
-                mMediaRouterService.releaseSessionWithManager(
-                        client, requestId, sessionInfo.getId());
-            } catch (RemoteException ex) {
-                Log.e(TAG, "releaseSession: Failed to send a request", ex);
-            }
+        try {
+            int requestId = mNextRequestId.getAndIncrement();
+            mMediaRouterService.releaseSessionWithManager(mClient, requestId, sessionInfo.getId());
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
         }
     }
 
@@ -897,14 +881,11 @@
             @NonNull MediaRoute2Info route) {
         int requestId = createTransferRequest(session, route);
 
-        Client client = getOrCreateClient();
-        if (client != null) {
-            try {
-                mMediaRouterService.transferToRouteWithManager(
-                        client, requestId, session.getId(), route);
-            } catch (RemoteException ex) {
-                Log.e(TAG, "transferToRoute: Failed to send a request.", ex);
-            }
+        try {
+            mMediaRouterService.transferToRouteWithManager(
+                    mClient, requestId, session.getId(), route);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
         }
     }
 
@@ -917,14 +898,11 @@
 
         int requestId = createTransferRequest(oldSession, route);
 
-        Client client = getOrCreateClient();
-        if (client != null) {
-            try {
-                mMediaRouterService.requestCreateSessionWithManager(
-                        client, requestId, oldSession, route);
-            } catch (RemoteException ex) {
-                Log.e(TAG, "requestCreateSession: Failed to send a request", ex);
-            }
+        try {
+            mMediaRouterService.requestCreateSessionWithManager(
+                    mClient, requestId, oldSession, route);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
         }
     }
 
@@ -968,23 +946,6 @@
                 sessionInfo.getOwnerPackageName());
     }
 
-    private Client getOrCreateClient() {
-        synchronized (sLock) {
-            if (mClient != null) {
-                return mClient;
-            }
-            Client client = new Client();
-            try {
-                mMediaRouterService.registerManager(client, mPackageName);
-                mClient = client;
-                return client;
-            } catch (RemoteException ex) {
-                Log.e(TAG, "Unable to register media router manager.", ex);
-            }
-        }
-        return null;
-    }
-
     /**
      * Interface for receiving events about media routing changes.
      */
diff --git a/media/jni/android_media_MediaProfiles.cpp b/media/jni/android_media_MediaProfiles.cpp
index ecb1d51..9868a14 100644
--- a/media/jni/android_media_MediaProfiles.cpp
+++ b/media/jni/android_media_MediaProfiles.cpp
@@ -255,21 +255,21 @@
     jmethodID audioProfileConstructorMethodID =
         env->GetMethodID(audioProfileClazz, "<init>", "(IIIII)V");
 
-    jobjectArray videoCodecs = (jobjectArray)env->NewObjectArray(
-            cp->getVideoCodecs().size(), videoProfileClazz, nullptr);
+    jobjectArray videoCodecs = nullptr;
     {
-        int i = 0;
+        auto isAdvancedCodec = [](const MediaProfiles::VideoCodec *vc) -> bool {
+                                  return ((vc->getBitDepth() != 8
+                                        || vc->getChromaSubsampling() != CHROMA_SUBSAMPLING_YUV_420
+                                        || vc->getHdrFormat() != HDR_FORMAT_NONE));
+                              };
+        std::vector<jobject> codecVector;
         for (const MediaProfiles::VideoCodec *vc : cp->getVideoCodecs()) {
+            if (isAdvancedCodec(vc) && !static_cast<bool>(advanced)) {
+                continue;
+            }
             chroma_subsampling cs = vc->getChromaSubsampling();
             int bitDepth = vc->getBitDepth();
             hdr_format hdr = vc->getHdrFormat();
-
-            bool isAdvanced =
-                (bitDepth != 8 || cs != CHROMA_SUBSAMPLING_YUV_420 || hdr != HDR_FORMAT_NONE);
-            if (static_cast<bool>(advanced) && !isAdvanced) {
-                continue;
-            }
-
             jobject videoCodec = env->NewObject(videoProfileClazz,
                                                 videoProfileConstructorMethodID,
                                                 vc->getCodec(),
@@ -281,10 +281,17 @@
                                                 static_cast<int>(cs),
                                                 bitDepth,
                                                 static_cast<int>(hdr));
-            env->SetObjectArrayElement(videoCodecs, i++, videoCodec);
+
+            codecVector.push_back(videoCodec);
+        }
+        videoCodecs = (jobjectArray)env->NewObjectArray(codecVector.size(),
+                                                        videoProfileClazz, nullptr);
+
+        int i = 0;
+        for (jobject codecObj : codecVector) {
+             env->SetObjectArrayElement(videoCodecs, i++, codecObj);
         }
     }
-
     jobjectArray audioCodecs;
     if (quality >= CAMCORDER_QUALITY_TIME_LAPSE_LIST_START
             && quality <= CAMCORDER_QUALITY_TIME_LAPSE_LIST_END) {
diff --git a/media/tests/MediaRouter/Android.bp b/media/tests/MediaRouter/Android.bp
index 2da6c98..4f9c6f1 100644
--- a/media/tests/MediaRouter/Android.bp
+++ b/media/tests/MediaRouter/Android.bp
@@ -25,7 +25,7 @@
         "testng",
         "truth-prebuilt",
     ],
-
+    test_suites: ["general-tests"],
     platform_apis: true,
     certificate: "platform",
 }
diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java
index b4aad9d..4086dec 100644
--- a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java
+++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java
@@ -39,6 +39,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 
 import android.Manifest;
@@ -121,7 +122,7 @@
         MediaRouter2ManagerTestActivity.startActivity(mContext);
 
         mManager = MediaRouter2Manager.getInstance(mContext);
-        mManager.startScan();
+        mManager.registerScanRequest();
         mRouter2 = MediaRouter2.getInstance(mContext);
 
         // If we need to support thread pool executors, change this to thread pool executor.
@@ -152,7 +153,7 @@
 
     @After
     public void tearDown() {
-        mManager.stopScan();
+        mManager.unregisterScanRequest();
 
         // order matters (callbacks should be cleared at the last)
         releaseAllSessions();
@@ -818,6 +819,13 @@
         assertFalse(failureLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
     }
 
+    @Test
+    public void unregisterScanRequest_enforcesANonNegativeCount() {
+        mManager.unregisterScanRequest(); // One request was made in the test setup.
+        assertThrows(IllegalStateException.class, () -> mManager.unregisterScanRequest());
+        mManager.registerScanRequest(); // So that the cleanup doesn't fail.
+    }
+
     /**
      * Tests if getSelectableRoutes and getDeselectableRoutes filter routes based on
      * selected routes
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
index 58c15eb..7ec0fcd 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
@@ -96,14 +96,14 @@
     public void startScan() {
         mMediaDevices.clear();
         mRouterManager.registerCallback(mExecutor, mMediaRouterCallback);
-        mRouterManager.startScan();
+        mRouterManager.registerScanRequest();
         refreshDevices();
     }
 
     @Override
     public void stopScan() {
         mRouterManager.unregisterCallback(mMediaRouterCallback);
-        mRouterManager.stopScan();
+        mRouterManager.unregisterScanRequest();
     }
 
     /**
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index f50dc74..df6f08d 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -124,6 +124,7 @@
         "dagger2",
         "jsr330",
         "lottie",
+        "LowLightDreamLib",
     ],
     manifest: "AndroidManifest.xml",
 
@@ -227,6 +228,7 @@
         "dagger2",
         "jsr330",
         "WindowManager-Shell",
+        "LowLightDreamLib",
     ],
     libs: [
         "android.test.runner",
diff --git a/packages/SystemUI/res/values-sw600dp-land/dimens.xml b/packages/SystemUI/res/values-sw600dp-land/dimens.xml
index f4d4824..b24ce12 100644
--- a/packages/SystemUI/res/values-sw600dp-land/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp-land/dimens.xml
@@ -66,8 +66,6 @@
     <dimen name="lockscreen_shade_qs_transition_distance">@dimen/lockscreen_shade_notifications_scrim_transition_distance</dimen>
     <dimen name="lockscreen_shade_qs_transition_delay">@dimen/lockscreen_shade_notifications_scrim_transition_delay</dimen>
     <dimen name="lockscreen_shade_qs_squish_transition_distance">@dimen/lockscreen_shade_qs_transition_distance</dimen>
-    <!-- On split-shade, the QS squish transition should start from half height.  -->
-    <item name="lockscreen_shade_qs_squish_start_fraction" type="dimen" format="float" >0.5</item>
     <!-- On split-shade, there should be no depth effect, so setting the value to 0.  -->
     <dimen name="lockscreen_shade_depth_controller_transition_distance">0dp</dimen>
     <dimen name="lockscreen_shade_udfps_keyguard_transition_distance">@dimen/lockscreen_shade_full_transition_distance</dimen>
diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml
index a587e5a..5dcbeb5 100644
--- a/packages/SystemUI/res/values-sw600dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp/dimens.xml
@@ -86,8 +86,6 @@
     <dimen name="lockscreen_shade_qs_transition_distance">@dimen/lockscreen_shade_full_transition_distance</dimen>
     <dimen name="lockscreen_shade_qs_transition_delay">@dimen/lockscreen_shade_scrim_transition_distance</dimen>
     <dimen name="lockscreen_shade_qs_squish_transition_distance">@dimen/lockscreen_shade_qs_transition_distance</dimen>
-    <!-- On large screen portrait, the QS squish transition should start from half height.  -->
-    <item name="lockscreen_shade_qs_squish_start_fraction" type="dimen" format="float" >0.5</item>
     <dimen name="lockscreen_shade_depth_controller_transition_distance">@dimen/lockscreen_shade_full_transition_distance</dimen>
     <dimen name="lockscreen_shade_udfps_keyguard_transition_distance">@dimen/lockscreen_shade_full_transition_distance</dimen>
     <dimen name="lockscreen_shade_status_bar_transition_distance">@dimen/lockscreen_shade_full_transition_distance</dimen>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index e09a6ee..f7019dc 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1174,6 +1174,7 @@
 
     <!-- Output switcher panel related dimensions -->
     <dimen name="media_output_dialog_list_max_height">355dp</dimen>
+    <dimen name="media_output_dialog_list_item_height">76dp</dimen>
     <dimen name="media_output_dialog_header_album_icon_size">72dp</dimen>
     <dimen name="media_output_dialog_header_back_icon_size">32dp</dimen>
     <dimen name="media_output_dialog_header_icon_padding">16dp</dimen>
@@ -1221,7 +1222,7 @@
 
     <!-- The fraction at which the QS "squish" transition should start during the lockscreen shade
          expansion. 0 is fully collapsed, 1 is fully expanded. -->
-    <item type="dimen" format="float" name="lockscreen_shade_qs_squish_start_fraction">0</item>
+    <item type="dimen" format="float" name="lockscreen_shade_qs_squish_start_fraction">0.5</item>
 
     <!-- Distance that the full shade transition takes in order for depth of the wallpaper to fully
          change.  -->
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java
index f0210fd..5d6598d 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java
@@ -49,6 +49,8 @@
             InteractionJankMonitor.CUJ_LAUNCHER_APP_LAUNCH_FROM_WIDGET;
     public static final int CUJ_SPLIT_SCREEN_ENTER =
             InteractionJankMonitor.CUJ_SPLIT_SCREEN_ENTER;
+    public static final int CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION =
+            InteractionJankMonitor.CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION;
 
     @IntDef({
             CUJ_APP_LAUNCH_FROM_RECENTS,
@@ -57,6 +59,7 @@
             CUJ_APP_CLOSE_TO_PIP,
             CUJ_QUICK_SWITCH,
             CUJ_APP_LAUNCH_FROM_WIDGET,
+            CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CujType {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt
index f82e7db..b78fa9a 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt
@@ -52,7 +52,6 @@
     val becauseCannotSkipBouncer: Boolean,
     val biometricSettingEnabledForUser: Boolean,
     val bouncerFullyShown: Boolean,
-    val bouncerIsOrWillShow: Boolean,
     val faceAuthenticated: Boolean,
     val faceDisabled: Boolean,
     val faceLockedOut: Boolean,
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 32c1cf9..6745cab 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -2597,7 +2597,7 @@
         // on bouncer if both fp and fingerprint are enrolled.
         final boolean awakeKeyguardExcludingBouncerShowing = mKeyguardIsVisible
                 && mDeviceInteractive && !mGoingToSleep
-                && !statusBarShadeLocked && !mBouncerIsOrWillBeShowing;
+                && !statusBarShadeLocked && !mBouncerFullyShown;
         final int user = getCurrentUser();
         final int strongAuth = mStrongAuthTracker.getStrongAuthForUser(user);
         final boolean isLockDown =
@@ -2667,7 +2667,6 @@
                     becauseCannotSkipBouncer,
                     biometricEnabledForUser,
                     mBouncerFullyShown,
-                    mBouncerIsOrWillBeShowing,
                     faceAuthenticated,
                     faceDisabledForUser,
                     isFaceLockedOut(),
@@ -3243,8 +3242,7 @@
                     cb.onKeyguardBouncerStateChanged(mBouncerIsOrWillBeShowing);
                 }
             }
-            updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE,
-                    FaceAuthUiEvent.FACE_AUTH_UPDATED_PRIMARY_BOUNCER_SHOWN_OR_WILL_BE_SHOWN);
+            updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
         }
 
         if (wasBouncerFullyShown != mBouncerFullyShown) {
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index 7fc8123..a5fdc68 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -103,7 +103,6 @@
 import com.android.systemui.statusbar.phone.SystemUIDialogManager;
 import com.android.systemui.statusbar.policy.AccessibilityController;
 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
-import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.BluetoothController;
 import com.android.systemui.statusbar.policy.CastController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -253,7 +252,6 @@
     @Inject Lazy<UserInfoController> mUserInfoController;
     @Inject Lazy<KeyguardStateController> mKeyguardMonitor;
     @Inject Lazy<KeyguardUpdateMonitor> mKeyguardUpdateMonitor;
-    @Inject Lazy<BatteryController> mBatteryController;
     @Inject Lazy<NightDisplayListener> mNightDisplayListener;
     @Inject Lazy<ReduceBrightColorsController> mReduceBrightColorsController;
     @Inject Lazy<ManagedProfileController> mManagedProfileController;
@@ -404,8 +402,6 @@
 
         mProviders.put(UserInfoController.class, mUserInfoController::get);
 
-        mProviders.put(BatteryController.class, mBatteryController::get);
-
         mProviders.put(NightDisplayListener.class, mNightDisplayListener::get);
 
         mProviders.put(ReduceBrightColorsController.class, mReduceBrightColorsController::get);
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java
index bfbf37a..d53e56f 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java
@@ -220,7 +220,7 @@
             return r;
         }).collect(Collectors.toList());
 
-        logDebug("False Gesture: " + localResult[0]);
+        logDebug("False Gesture (type: " + interactionType + "): " + localResult[0]);
 
         return localResult[0];
     }
@@ -454,6 +454,12 @@
         }
     }
 
+    static void logVerbose(String msg) {
+        if (DEBUG) {
+            Log.v(TAG, msg);
+        }
+    }
+
     static void logInfo(String msg) {
         Log.i(TAG, msg);
         RECENT_INFO_LOG.add(msg);
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java b/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java
index f18413b..c292296 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java
@@ -42,8 +42,9 @@
     public static final int QS_COLLAPSE = 12;
     public static final int UDFPS_AUTHENTICATION = 13;
     public static final int LOCK_ICON = 14;
-    public static final int QS_SWIPE = 15;
+    public static final int QS_SWIPE_SIDE = 15;
     public static final int BACK_GESTURE = 16;
+    public static final int QS_SWIPE_NESTED = 17;
 
     @IntDef({
             QUICK_SETTINGS,
@@ -62,7 +63,8 @@
             BRIGHTNESS_SLIDER,
             UDFPS_AUTHENTICATION,
             LOCK_ICON,
-            QS_SWIPE,
+            QS_SWIPE_SIDE,
+            QS_SWIPE_NESTED,
             BACK_GESTURE
     })
     @Retention(RetentionPolicy.SOURCE)
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/DistanceClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/DistanceClassifier.java
index d0fe1c3..5e4f149 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/DistanceClassifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/DistanceClassifier.java
@@ -24,6 +24,7 @@
 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.BRIGHTLINE_FALSING_DISTANCE_VERTICAL_SWIPE_THRESHOLD_IN;
 import static com.android.systemui.classifier.Classifier.BRIGHTNESS_SLIDER;
 import static com.android.systemui.classifier.Classifier.QS_COLLAPSE;
+import static com.android.systemui.classifier.Classifier.QS_SWIPE_NESTED;
 import static com.android.systemui.classifier.Classifier.SHADE_DRAG;
 
 import android.provider.DeviceConfig;
@@ -156,7 +157,8 @@
                 || interactionType == QS_COLLAPSE
                 || interactionType == Classifier.UDFPS_AUTHENTICATION
                 || interactionType == Classifier.LOCK_ICON
-                || interactionType == Classifier.QS_SWIPE) {
+                || interactionType == Classifier.QS_SWIPE_SIDE
+                || interactionType == QS_SWIPE_NESTED) {
             return Result.passed(0);
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingClassifier.java
index d757528..d18d62f 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingClassifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingClassifier.java
@@ -148,6 +148,11 @@
     }
 
     /** */
+    public static void logVerbose(String msg) {
+        BrightLineFalsingManager.logVerbose(msg);
+    }
+
+    /** */
     public static void logInfo(String msg) {
         BrightLineFalsingManager.logInfo(msg);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java
index a3ecb0c..3991a35 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java
@@ -78,10 +78,10 @@
 
     void onMotionEvent(MotionEvent motionEvent) {
         List<MotionEvent> motionEvents = unpackMotionEvent(motionEvent);
-        FalsingClassifier.logDebug("Unpacked into: " + motionEvents.size());
+        FalsingClassifier.logVerbose("Unpacked into: " + motionEvents.size());
         if (BrightLineFalsingManager.DEBUG) {
             for (MotionEvent m : motionEvents) {
-                FalsingClassifier.logDebug(
+                FalsingClassifier.logVerbose(
                         "x,y,t: " + m.getX() + "," + m.getY() + "," + m.getEventTime());
             }
         }
@@ -92,7 +92,7 @@
         }
         mRecentMotionEvents.addAll(motionEvents);
 
-        FalsingClassifier.logDebug("Size: " + mRecentMotionEvents.size());
+        FalsingClassifier.logVerbose("Size: " + mRecentMotionEvents.size());
 
         mMotionEventListeners.forEach(listener -> listener.onMotionEvent(motionEvent));
 
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/ProximityClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/ProximityClassifier.java
index 32d9ca59..07f94e7 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/ProximityClassifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/ProximityClassifier.java
@@ -19,7 +19,7 @@
 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.BRIGHTLINE_FALSING_PROXIMITY_PERCENT_COVERED_THRESHOLD;
 import static com.android.systemui.classifier.Classifier.BRIGHTNESS_SLIDER;
 import static com.android.systemui.classifier.Classifier.QS_COLLAPSE;
-import static com.android.systemui.classifier.Classifier.QS_SWIPE;
+import static com.android.systemui.classifier.Classifier.QS_SWIPE_SIDE;
 import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
 
 import android.provider.DeviceConfig;
@@ -119,7 +119,7 @@
             @Classifier.InteractionType int interactionType,
             double historyBelief, double historyConfidence) {
         if (interactionType == QUICK_SETTINGS || interactionType == BRIGHTNESS_SLIDER
-                || interactionType == QS_COLLAPSE || interactionType == QS_SWIPE) {
+                || interactionType == QS_COLLAPSE || interactionType == QS_SWIPE_SIDE) {
             return Result.passed(0);
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/TypeClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/TypeClassifier.java
index f040712..776bc88 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/TypeClassifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/TypeClassifier.java
@@ -24,7 +24,8 @@
 import static com.android.systemui.classifier.Classifier.NOTIFICATION_DRAG_DOWN;
 import static com.android.systemui.classifier.Classifier.PULSE_EXPAND;
 import static com.android.systemui.classifier.Classifier.QS_COLLAPSE;
-import static com.android.systemui.classifier.Classifier.QS_SWIPE;
+import static com.android.systemui.classifier.Classifier.QS_SWIPE_NESTED;
+import static com.android.systemui.classifier.Classifier.QS_SWIPE_SIDE;
 import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
 import static com.android.systemui.classifier.Classifier.RIGHT_AFFORDANCE;
 import static com.android.systemui.classifier.Classifier.SHADE_DRAG;
@@ -86,9 +87,12 @@
             case QS_COLLAPSE:
                 wrongDirection = !vertical || !up;
                 break;
-            case QS_SWIPE:
+            case QS_SWIPE_SIDE:
                 wrongDirection = vertical;
                 break;
+            case QS_SWIPE_NESTED:
+                wrongDirection = !vertical;
+                break;
             default:
                 wrongDirection = true;
                 break;
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/ZigZagClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/ZigZagClassifier.java
index 40c28fa..de2bdf7 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/ZigZagClassifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/ZigZagClassifier.java
@@ -137,8 +137,8 @@
             runningAbsDy += Math.abs(point.y - pY);
             pX = point.x;
             pY = point.y;
-            logDebug("(x, y, runningAbsDx, runningAbsDy) - (" + pX + ", " + pY + ", " + runningAbsDx
-                    + ", " + runningAbsDy + ")");
+            logVerbose("(x, y, runningAbsDx, runningAbsDy) - ("
+                    + pX + ", " + pY + ", " + runningAbsDx + ", " + runningAbsDy + ")");
         }
 
         float devianceX = runningAbsDx - actualDx;
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
index fbfc94a..a996699 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
@@ -35,6 +35,7 @@
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.dock.DockManagerImpl;
 import com.android.systemui.doze.DozeHost;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.media.dagger.MediaModule;
 import com.android.systemui.navigationbar.gestural.GestureModule;
 import com.android.systemui.plugins.qs.QSFactory;
@@ -126,6 +127,7 @@
             PowerManager powerManager,
             BroadcastDispatcher broadcastDispatcher,
             DemoModeController demoModeController,
+            DumpManager dumpManager,
             @Main Handler mainHandler,
             @Background Handler bgHandler) {
         BatteryController bC = new BatteryControllerImpl(
@@ -134,6 +136,7 @@
                 powerManager,
                 broadcastDispatcher,
                 demoModeController,
+                dumpManager,
                 mainHandler,
                 bgHandler);
         bC.init();
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index 96f77b3..696fc72 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.dreams;
 
+import android.content.ComponentName;
 import android.content.Context;
 import android.graphics.drawable.ColorDrawable;
 import android.util.Log;
@@ -26,11 +27,13 @@
 import android.view.WindowManager;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 import androidx.lifecycle.Lifecycle;
 import androidx.lifecycle.LifecycleRegistry;
 import androidx.lifecycle.ViewModelStore;
 
+import com.android.dream.lowlight.dagger.LowLightDreamModule;
 import com.android.internal.logging.UiEvent;
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.policy.PhoneWindow;
@@ -44,6 +47,7 @@
 import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
+import javax.inject.Named;
 
 /**
  * The {@link DreamOverlayService} is responsible for placing an overlay on top of a dream. The
@@ -62,6 +66,8 @@
     // content area).
     private final DreamOverlayContainerViewController mDreamOverlayContainerViewController;
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    @Nullable
+    private final ComponentName mLowLightDreamComponent;
     private final UiEventLogger mUiEventLogger;
 
     // A reference to the {@link Window} used to hold the dream overlay.
@@ -125,10 +131,13 @@
             DreamOverlayComponent.Factory dreamOverlayComponentFactory,
             DreamOverlayStateController stateController,
             KeyguardUpdateMonitor keyguardUpdateMonitor,
-            UiEventLogger uiEventLogger) {
+            UiEventLogger uiEventLogger,
+            @Nullable @Named(LowLightDreamModule.LOW_LIGHT_DREAM_COMPONENT)
+                    ComponentName lowLightDreamComponent) {
         mContext = context;
         mExecutor = executor;
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
+        mLowLightDreamComponent = lowLightDreamComponent;
         mKeyguardUpdateMonitor.registerCallback(mKeyguardCallback);
         mStateController = stateController;
         mUiEventLogger = uiEventLogger;
@@ -155,6 +164,7 @@
             windowManager.removeView(mWindow.getDecorView());
         }
         mStateController.setOverlayActive(false);
+        mStateController.setLowLightActive(false);
         mDestroyed = true;
         super.onDestroy();
     }
@@ -163,6 +173,9 @@
     public void onStartDream(@NonNull WindowManager.LayoutParams layoutParams) {
         mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_ENTER_START);
         setCurrentState(Lifecycle.State.STARTED);
+        final ComponentName dreamComponent = getDreamComponent();
+        mStateController.setLowLightActive(
+                dreamComponent != null && dreamComponent.equals(mLowLightDreamComponent));
         mExecutor.execute(() -> {
             if (mDestroyed) {
                 // The task could still be executed after the service has been destroyed. Bail if
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
index 69e41ba..72feaca 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
@@ -50,6 +50,7 @@
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     public static final int STATE_DREAM_OVERLAY_ACTIVE = 1 << 0;
+    public static final int STATE_LOW_LIGHT_ACTIVE = 1 << 1;
 
     private static final int OP_CLEAR_STATE = 1;
     private static final int OP_SET_STATE = 2;
@@ -193,6 +194,14 @@
         return containsState(STATE_DREAM_OVERLAY_ACTIVE);
     }
 
+    /**
+     * Returns whether low light mode is active.
+     * @return {@code true} if in low light mode, {@code false} otherwise.
+     */
+    public boolean isLowLightActive() {
+        return containsState(STATE_LOW_LIGHT_ACTIVE);
+    }
+
     private boolean containsState(int state) {
         return (mState & state) != 0;
     }
@@ -222,6 +231,14 @@
     }
 
     /**
+     * Sets whether low light mode is active.
+     * @param active {@code true} if low light mode is active, {@code false} otherwise.
+     */
+    public void setLowLightActive(boolean active) {
+        modifyState(active ? OP_SET_STATE : OP_CLEAR_STATE, STATE_LOW_LIGHT_ACTIVE);
+    }
+
+    /**
      * Returns the available complication types.
      */
     @Complication.ComplicationType
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
index aa59cc6..bb1c430 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
@@ -20,7 +20,6 @@
 import static android.app.StatusBarManager.WINDOW_STATE_HIDING;
 import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
 
-import android.annotation.Nullable;
 import android.app.AlarmManager;
 import android.app.StatusBarManager;
 import android.content.res.Resources;
@@ -36,6 +35,8 @@
 import android.util.PluralsMessageFormatter;
 import android.view.View;
 
+import androidx.annotation.Nullable;
+
 import com.android.systemui.R;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dreams.DreamOverlayStatusBarItemsProvider.StatusBarItem;
@@ -73,6 +74,8 @@
     private final Optional<DreamOverlayNotificationCountProvider>
             mDreamOverlayNotificationCountProvider;
     private final ZenModeController mZenModeController;
+    private final DreamOverlayStateController mDreamOverlayStateController;
+    private final StatusBarWindowStateController mStatusBarWindowStateController;
     private final DreamOverlayStatusBarItemsProvider mStatusBarItemsProvider;
     private final Executor mMainExecutor;
     private final List<DreamOverlayStatusBarItemsProvider.StatusBarItem> mExtraStatusBarItems =
@@ -102,6 +105,14 @@
         }
     };
 
+    private final DreamOverlayStateController.Callback mDreamOverlayStateCallback =
+            new DreamOverlayStateController.Callback() {
+                @Override
+                public void onStateChanged() {
+                    updateLowLightState();
+                }
+            };
+
     private final IndividualSensorPrivacyController.Callback mSensorCallback =
             (sensor, blocked) -> updateMicCameraBlockedStatusIcon();
 
@@ -140,7 +151,8 @@
             Optional<DreamOverlayNotificationCountProvider> dreamOverlayNotificationCountProvider,
             ZenModeController zenModeController,
             StatusBarWindowStateController statusBarWindowStateController,
-            DreamOverlayStatusBarItemsProvider statusBarItemsProvider) {
+            DreamOverlayStatusBarItemsProvider statusBarItemsProvider,
+            DreamOverlayStateController dreamOverlayStateController) {
         super(view);
         mResources = resources;
         mMainExecutor = mainExecutor;
@@ -151,8 +163,10 @@
         mDateFormatUtil = dateFormatUtil;
         mSensorPrivacyController = sensorPrivacyController;
         mDreamOverlayNotificationCountProvider = dreamOverlayNotificationCountProvider;
+        mStatusBarWindowStateController = statusBarWindowStateController;
         mStatusBarItemsProvider = statusBarItemsProvider;
         mZenModeController = zenModeController;
+        mDreamOverlayStateController = dreamOverlayStateController;
 
         // Register to receive show/hide updates for the system status bar. Our custom status bar
         // needs to hide when the system status bar is showing to ovoid overlapping status bars.
@@ -180,6 +194,9 @@
 
         mStatusBarItemsProvider.addCallback(mStatusBarItemsProviderCallback);
 
+        mDreamOverlayStateController.addCallback(mDreamOverlayStateCallback);
+        updateLowLightState();
+
         mTouchInsetSession.addViewToTracking(mView);
     }
 
@@ -193,6 +210,7 @@
                 provider -> provider.removeCallback(mNotificationCountCallback));
         mStatusBarItemsProvider.removeCallback(mStatusBarItemsProviderCallback);
         mView.removeAllExtraStatusBarItemViews();
+        mDreamOverlayStateController.removeCallback(mDreamOverlayStateCallback);
         mTouchInsetSession.clear();
 
         mIsAttached = false;
@@ -217,6 +235,15 @@
                 hasAlarm ? buildAlarmContentDescription(alarm) : null);
     }
 
+    private void updateLowLightState() {
+        int visibility = View.VISIBLE;
+        if (mDreamOverlayStateController.isLowLightActive()
+                || mStatusBarWindowStateController.windowIsShowing()) {
+            visibility = View.INVISIBLE;
+        }
+        mView.setVisibility(visibility);
+    }
+
     private String buildAlarmContentDescription(AlarmManager.AlarmClockInfo alarm) {
         final String skeleton = mDateFormatUtil.is24HourFormat() ? "EHm" : "Ehma";
         final String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton);
@@ -272,7 +299,7 @@
 
     private void onSystemStatusBarStateChanged(@StatusBarManager.WindowVisibleState int state) {
         mMainExecutor.execute(() -> {
-            if (!mIsAttached) {
+            if (!mIsAttached || mDreamOverlayStateController.isLowLightActive()) {
                 return;
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
index 2dd2098..f9dca08 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.content.res.Resources;
 
+import com.android.dream.lowlight.dagger.LowLightDreamModule;
 import com.android.settingslib.dream.DreamBackend;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -37,6 +38,7 @@
  */
 @Module(includes = {
             RegisteredComplicationsModule.class,
+            LowLightDreamModule.class,
         },
         subcomponents = {
             DreamOverlayComponent.class,
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index 030c7ce..44feac1 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -238,6 +238,10 @@
     public static final SysPropBooleanFlag FLOATING_TASKS_ENABLED =
             new SysPropBooleanFlag(1106, "persist.wm.debug.floating_tasks", false);
 
+    @Keep
+    public static final SysPropBooleanFlag SHOW_FLOATING_TASKS_AS_BUBBLES =
+            new SysPropBooleanFlag(1107, "persist.wm.debug.floating_tasks_as_bubbles", false);
+
     // 1200 - predictive back
     @Keep
     public static final SysPropBooleanFlag WM_ENABLE_PREDICTIVE_BACK = new SysPropBooleanFlag(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 4214240..38b98eb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -22,6 +22,7 @@
 import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_WITH_WALLPAPER;
 
 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.NAV_BAR_HANDLE_SHOW_OVER_LOCKSCREEN;
+import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_OCCLUSION;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_TRANSITION_FROM_AOD;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_UNLOCK_ANIMATION;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED;
@@ -845,6 +846,8 @@
                     if (launchIsFullScreen) {
                         mCentralSurfaces.instantCollapseNotificationPanel();
                     }
+
+                    mInteractionJankMonitor.end(CUJ_LOCKSCREEN_OCCLUSION);
                 }
 
                 @NonNull
@@ -991,6 +994,8 @@
                     setOccluded(isKeyguardOccluded /* isOccluded */, false /* animate */);
                     Log.d(TAG, "Unocclude animation cancelled. Occluded state is now: "
                             + mOccluded);
+
+                    mInteractionJankMonitor.cancel(CUJ_LOCKSCREEN_OCCLUSION);
                 }
 
                 @Override
@@ -999,6 +1004,9 @@
                         RemoteAnimationTarget[] nonApps,
                         IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException {
                     Log.d(TAG, "UnoccludeAnimator#onAnimationStart. Set occluded = false.");
+                    mInteractionJankMonitor.begin(
+                            createInteractionJankMonitorConf(CUJ_LOCKSCREEN_OCCLUSION)
+                                    .setTag("UNOCCLUDE"));
                     setOccluded(false /* isOccluded */, true /* animate */);
 
                     if (apps == null || apps.length == 0 || apps[0] == null) {
@@ -1057,6 +1065,8 @@
                                 try {
                                     finishedCallback.onAnimationFinished();
                                     mUnoccludeAnimator = null;
+
+                                    mInteractionJankMonitor.end(CUJ_LOCKSCREEN_OCCLUSION);
                                 } catch (RemoteException e) {
                                     e.printStackTrace();
                                 }
@@ -2573,7 +2583,8 @@
                         };
                 try {
                     mInteractionJankMonitor.begin(
-                            createInteractionJankMonitorConf("RunRemoteAnimation"));
+                            createInteractionJankMonitorConf(
+                                    CUJ_LOCKSCREEN_UNLOCK_ANIMATION, "RunRemoteAnimation"));
                     runner.onAnimationStart(WindowManager.TRANSIT_KEYGUARD_GOING_AWAY, apps,
                             wallpapers, nonApps, callback);
                 } catch (RemoteException e) {
@@ -2589,7 +2600,8 @@
                 mSurfaceBehindRemoteAnimationRunning = true;
 
                 mInteractionJankMonitor.begin(
-                        createInteractionJankMonitorConf("DismissPanel"));
+                        createInteractionJankMonitorConf(
+                                CUJ_LOCKSCREEN_UNLOCK_ANIMATION, "DismissPanel"));
 
                 // Pass the surface and metadata to the unlock animation controller.
                 mKeyguardUnlockAnimationControllerLazy.get()
@@ -2597,7 +2609,8 @@
                                 apps, startTime, mSurfaceBehindRemoteAnimationRequested);
             } else {
                 mInteractionJankMonitor.begin(
-                        createInteractionJankMonitorConf("RemoteAnimationDisabled"));
+                        createInteractionJankMonitorConf(
+                                CUJ_LOCKSCREEN_UNLOCK_ANIMATION, "RemoteAnimationDisabled"));
 
                 mKeyguardViewControllerLazy.get().hide(startTime, fadeoutDuration);
 
@@ -2677,10 +2690,15 @@
         sendUserPresentBroadcast();
     }
 
-    private Configuration.Builder createInteractionJankMonitorConf(String tag) {
-        return Configuration.Builder.withView(CUJ_LOCKSCREEN_UNLOCK_ANIMATION,
-                mKeyguardViewControllerLazy.get().getViewRootImpl().getView())
-                .setTag(tag);
+    private Configuration.Builder createInteractionJankMonitorConf(int cuj) {
+        return createInteractionJankMonitorConf(cuj, null /* tag */);
+    }
+
+    private Configuration.Builder createInteractionJankMonitorConf(int cuj, @Nullable String tag) {
+        final Configuration.Builder builder = Configuration.Builder.withView(
+                cuj, mKeyguardViewControllerLazy.get().getViewRootImpl().getView());
+
+        return tag != null ? builder.setTag(tag) : builder;
     }
 
     /**
@@ -3291,6 +3309,10 @@
                 IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException {
             super.onAnimationStart(transit, apps, wallpapers, nonApps, finishedCallback);
 
+            mInteractionJankMonitor.begin(
+                    createInteractionJankMonitorConf(CUJ_LOCKSCREEN_OCCLUSION)
+                            .setTag("OCCLUDE"));
+
             // This is the first signal we have from WM that we're going to be occluded. Set our
             // internal state to reflect that immediately, vs. waiting for the launch animator to
             // begin. Otherwise, calls to setShowingLocked, etc. will not know that we're about to
@@ -3307,6 +3329,7 @@
                     + "Setting occluded state to: " + isKeyguardOccluded);
             setOccluded(isKeyguardOccluded /* occluded */, false /* animate */);
 
+            mInteractionJankMonitor.cancel(CUJ_LOCKSCREEN_OCCLUSION);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
index 267c1f5..b3a4ddf 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
@@ -188,24 +188,28 @@
 
         @AnyThread
         fun start() = bgExecutor.execute {
-            localMediaManager.registerCallback(this)
-            localMediaManager.startScan()
-            muteAwaitConnectionManager?.startListening()
-            playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN
-            controller?.registerCallback(this)
-            updateCurrent()
-            started = true
-            configurationController.addCallback(configListener)
+            if (!started) {
+                localMediaManager.registerCallback(this)
+                localMediaManager.startScan()
+                muteAwaitConnectionManager?.startListening()
+                playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN
+                controller?.registerCallback(this)
+                updateCurrent()
+                started = true
+                configurationController.addCallback(configListener)
+            }
         }
 
         @AnyThread
         fun stop() = bgExecutor.execute {
-            started = false
-            controller?.unregisterCallback(this)
-            localMediaManager.stopScan()
-            localMediaManager.unregisterCallback(this)
-            muteAwaitConnectionManager?.stopListening()
-            configurationController.removeCallback(configListener)
+            if (started) {
+                started = false
+                controller?.unregisterCallback(this)
+                localMediaManager.stopScan()
+                localMediaManager.unregisterCallback(this)
+                muteAwaitConnectionManager?.stopListening()
+                configurationController.removeCallback(configListener)
+            }
         }
 
         fun dump(pw: PrintWriter) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
index 85d8f3f..a9e1a4d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -99,6 +99,7 @@
     private Button mStopButton;
     private Button mAppButton;
     private int mListMaxHeight;
+    private int mItemHeight;
     private WallpaperColors mWallpaperColors;
     private Executor mExecutor;
     private boolean mShouldLaunchLeBroadcastDialog;
@@ -106,10 +107,12 @@
     MediaOutputBaseAdapter mAdapter;
 
     private final ViewTreeObserver.OnGlobalLayoutListener mDeviceListLayoutListener = () -> {
+        ViewGroup.LayoutParams params = mDeviceListLayout.getLayoutParams();
+        int totalItemsHeight = mAdapter.getItemCount() * mItemHeight;
+        int correctHeight = Math.min(totalItemsHeight, mListMaxHeight);
         // Set max height for list
-        if (mDeviceListLayout.getHeight() > mListMaxHeight) {
-            ViewGroup.LayoutParams params = mDeviceListLayout.getLayoutParams();
-            params.height = mListMaxHeight;
+        if (correctHeight != params.height) {
+            params.height = correctHeight;
             mDeviceListLayout.setLayoutParams(params);
         }
     };
@@ -212,6 +215,8 @@
         mLayoutManager = new LayoutManagerWrapper(mContext);
         mListMaxHeight = context.getResources().getDimensionPixelSize(
                 R.dimen.media_output_dialog_list_max_height);
+        mItemHeight = context.getResources().getDimensionPixelSize(
+                R.dimen.media_output_dialog_list_item_height);
         mExecutor = Executors.newSingleThreadExecutor();
     }
 
@@ -246,8 +251,10 @@
         mDeviceListLayout.getViewTreeObserver().addOnGlobalLayoutListener(
                 mDeviceListLayoutListener);
         // Init device list
+        mLayoutManager.setAutoMeasureEnabled(true);
         mDevicesRecyclerView.setLayoutManager(mLayoutManager);
         mDevicesRecyclerView.setAdapter(mAdapter);
+        mDevicesRecyclerView.setHasFixedSize(false);
         // Init header icon
         mHeaderIcon.setOnClickListener(v -> onHeaderIconClick());
         // Init bottom buttons
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
index 7b4ac12..19b401d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -125,13 +125,16 @@
     private final NearbyMediaDevicesManager mNearbyMediaDevicesManager;
     private final Map<String, Integer> mNearbyDeviceInfoMap = new ConcurrentHashMap<>();
 
-    private boolean mIsRefreshing = false;
-    private boolean mNeedRefresh = false;
+    @VisibleForTesting
+    boolean mIsRefreshing = false;
+    @VisibleForTesting
+    boolean mNeedRefresh = false;
     private MediaController mMediaController;
     @VisibleForTesting
     Callback mCallback;
     @VisibleForTesting
     LocalMediaManager mLocalMediaManager;
+    @VisibleForTesting
     private MediaOutputMetricLogger mMetricLogger;
     private int mCurrentState;
 
@@ -221,15 +224,7 @@
                 Log.d(TAG, "No media controller for " + mPackageName);
             }
         }
-        if (mLocalMediaManager == null) {
-            if (DEBUG) {
-                Log.d(TAG, "No local media manager " + mPackageName);
-            }
-            return;
-        }
         mCallback = cb;
-        mLocalMediaManager.unregisterCallback(this);
-        mLocalMediaManager.stopScan();
         mLocalMediaManager.registerCallback(this);
         mLocalMediaManager.startScan();
     }
@@ -251,10 +246,8 @@
         if (mMediaController != null) {
             mMediaController.unregisterCallback(mCb);
         }
-        if (mLocalMediaManager != null) {
-            mLocalMediaManager.unregisterCallback(this);
-            mLocalMediaManager.stopScan();
-        }
+        mLocalMediaManager.unregisterCallback(this);
+        mLocalMediaManager.stopScan();
         synchronized (mMediaDevicesLock) {
             mCachedMediaDevices.clear();
             mMediaDevices.clear();
@@ -658,10 +651,6 @@
         return mLocalMediaManager.getCurrentConnectedDevice();
     }
 
-    private MediaDevice getMediaDeviceById(String id) {
-        return mLocalMediaManager.getMediaDeviceById(new ArrayList<>(mMediaDevices), id);
-    }
-
     boolean addDeviceToPlayMedia(MediaDevice device) {
         mMetricLogger.logInteractionExpansion(device);
         return mLocalMediaManager.addDeviceToPlayMedia(device);
@@ -683,10 +672,6 @@
         return mLocalMediaManager.getDeselectableMediaDevice();
     }
 
-    void adjustSessionVolume(String sessionId, int volume) {
-        mLocalMediaManager.adjustSessionVolume(sessionId, volume);
-    }
-
     void adjustSessionVolume(int volume) {
         mLocalMediaManager.adjustSessionVolume(volume);
     }
@@ -1013,7 +998,7 @@
                 return;
             }
 
-            if (newState == PlaybackState.STATE_STOPPED || newState == PlaybackState.STATE_PAUSED) {
+            if (newState == PlaybackState.STATE_STOPPED) {
                 mCallback.onMediaStoppedOrPaused();
             }
             mCurrentState = newState;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/NonInterceptingScrollView.java b/packages/SystemUI/src/com/android/systemui/qs/NonInterceptingScrollView.java
index cd36091..1d05874 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/NonInterceptingScrollView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/NonInterceptingScrollView.java
@@ -28,21 +28,29 @@
 public class NonInterceptingScrollView extends ScrollView {
 
     private final int mTouchSlop;
+
     private float mDownY;
     private boolean mScrollEnabled = true;
+    private boolean mPreventingIntercept;
 
     public NonInterceptingScrollView(Context context, AttributeSet attrs) {
         super(context, attrs);
         mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
     }
 
+    public boolean isPreventingIntercept() {
+        return mPreventingIntercept;
+    }
+
     @Override
     public boolean onTouchEvent(MotionEvent ev) {
         int action = ev.getActionMasked();
         switch (action) {
             case MotionEvent.ACTION_DOWN:
+                mPreventingIntercept = false;
                 if (canScrollVertically(1)) {
                     // If we can scroll down, make sure we're not intercepted by the parent
+                    mPreventingIntercept = true;
                     final ViewParent parent = getParent();
                     if (parent != null) {
                         parent.requestDisallowInterceptTouchEvent(true);
@@ -62,10 +70,13 @@
     public boolean onInterceptTouchEvent(MotionEvent ev) {
         // If there's a touch on this view and we can scroll down, we don't want to be intercepted
         int action = ev.getActionMasked();
+
         switch (action) {
             case MotionEvent.ACTION_DOWN:
-                // If we can scroll down, make sure non of our parents intercepts us.
+                mPreventingIntercept = false;
+                // If we can scroll down, make sure none of our parents intercepts us.
                 if (canScrollVertically(1)) {
+                    mPreventingIntercept = true;
                     final ViewParent parent = getParent();
                     if (parent != null) {
                         parent.requestDisallowInterceptTouchEvent(true);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
index 7b1ddd6..ef87fb4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
@@ -131,6 +131,10 @@
         updateClippingPath();
     }
 
+    public NonInterceptingScrollView getQSPanelContainer() {
+        return mQSPanelContainer;
+    }
+
     public void disable(int state1, int state2, boolean animate) {
         final boolean disabled = (state2 & DISABLE2_QUICK_SETTINGS) != 0;
         if (disabled == mQsDisabled) return;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java
index 7d61991..dea7bb5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java
@@ -16,8 +16,13 @@
 
 package com.android.systemui.qs;
 
-import android.content.res.Configuration;
+import static com.android.systemui.classifier.Classifier.QS_SWIPE_NESTED;
 
+import android.content.res.Configuration;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.qs.dagger.QSScope;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.util.ViewController;
@@ -30,6 +35,8 @@
     private final QSPanelController mQsPanelController;
     private final QuickStatusBarHeaderController mQuickStatusBarHeaderController;
     private final ConfigurationController mConfigurationController;
+    private final FalsingManager mFalsingManager;
+    private final NonInterceptingScrollView mQSPanelContainer;
 
     private final ConfigurationController.ConfigurationListener mConfigurationListener =
             new ConfigurationController.ConfigurationListener() {
@@ -39,14 +46,32 @@
         }
     };
 
+    private final View.OnTouchListener mContainerTouchHandler = new View.OnTouchListener() {
+        @Override
+        public boolean onTouch(View v, MotionEvent event) {
+            if (event.getActionMasked() == MotionEvent.ACTION_UP) {
+                if (mQSPanelContainer.isPreventingIntercept()) {
+                    // There's really no action here to take, but we need to tell the FalsingManager
+                    mFalsingManager.isFalseTouch(QS_SWIPE_NESTED);
+                }
+            }
+            return false;
+        }
+    };
+
     @Inject
-    QSContainerImplController(QSContainerImpl view, QSPanelController qsPanelController,
+    QSContainerImplController(
+            QSContainerImpl view,
+            QSPanelController qsPanelController,
             QuickStatusBarHeaderController quickStatusBarHeaderController,
-            ConfigurationController configurationController) {
+            ConfigurationController configurationController,
+            FalsingManager falsingManager) {
         super(view);
         mQsPanelController = qsPanelController;
         mQuickStatusBarHeaderController = quickStatusBarHeaderController;
         mConfigurationController = configurationController;
+        mFalsingManager = falsingManager;
+        mQSPanelContainer = mView.getQSPanelContainer();
     }
 
     @Override
@@ -62,11 +87,13 @@
     protected void onViewAttached() {
         mView.updateResources(mQsPanelController, mQuickStatusBarHeaderController);
         mConfigurationController.addCallback(mConfigurationListener);
+        mQSPanelContainer.setOnTouchListener(mContainerTouchHandler);
     }
 
     @Override
     protected void onViewDetached() {
         mConfigurationController.removeCallback(mConfigurationListener);
+        mQSPanelContainer.setOnTouchListener(null);
     }
 
     public QSContainerImpl getView() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 448e180..184089f7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -473,6 +473,8 @@
                     ? Math.max(mMediaTotalBottomMargin - getPaddingBottom(), 0) : 0;
             layoutParams.topMargin = mediaNeedsTopMargin() && !horizontal
                     ? mMediaTopMargin : 0;
+            // Call setLayoutParams explicitly to ensure that requestLayout happens
+            hostView.setLayoutParams(layoutParams);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
index 59b871c..f41b905 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
@@ -16,12 +16,11 @@
 
 package com.android.systemui.qs;
 
-import static com.android.systemui.classifier.Classifier.QS_SWIPE;
+import static com.android.systemui.classifier.Classifier.QS_SWIPE_SIDE;
 import static com.android.systemui.media.dagger.MediaModule.QS_PANEL;
 import static com.android.systemui.qs.QSPanel.QS_SHOW_BRIGHTNESS;
 import static com.android.systemui.qs.dagger.QSFragmentModule.QS_USING_MEDIA_PLAYER;
 
-import android.content.res.Configuration;
 import android.view.MotionEvent;
 import android.view.View;
 
@@ -61,22 +60,11 @@
     private final BrightnessMirrorHandler mBrightnessMirrorHandler;
     private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
 
-    private final QSPanel.OnConfigurationChangedListener mOnConfigurationChangedListener =
-            new QSPanel.OnConfigurationChangedListener() {
-        @Override
-        public void onConfigurationChange(Configuration newConfig) {
-            mView.updateResources();
-            if (mView.isListening()) {
-                refreshAllTiles();
-            }
-        }
-    };
-
     private View.OnTouchListener mTileLayoutTouchListener = new View.OnTouchListener() {
         @Override
         public boolean onTouch(View v, MotionEvent event) {
             if (event.getActionMasked() == MotionEvent.ACTION_UP) {
-                mFalsingManager.isFalseTouch(QS_SWIPE);
+                mFalsingManager.isFalseTouch(QS_SWIPE_SIDE);
             }
             return false;
         }
@@ -130,7 +118,6 @@
         if (mView.isListening()) {
             refreshAllTiles();
         }
-        mView.addOnConfigurationChangedListener(mOnConfigurationChangedListener);
         switchTileLayout(true);
         mBrightnessMirrorHandler.onQsPanelAttached();
 
@@ -147,11 +134,18 @@
     @Override
     protected void onViewDetached() {
         mTunerService.removeTunable(mView);
-        mView.removeOnConfigurationChangedListener(mOnConfigurationChangedListener);
         mBrightnessMirrorHandler.onQsPanelDettached();
         super.onViewDetached();
     }
 
+    @Override
+    protected void onConfigurationChanged() {
+        mView.updateResources();
+        if (mView.isListening()) {
+            refreshAllTiles();
+        }
+    }
+
     /** */
     public void setVisibility(int visibility) {
         mView.setVisibility(visibility);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
index 6e4c858..a5c60a4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
@@ -101,11 +101,11 @@
                                     + newConfig.windowConfiguration);
                     mQSLogger.logOnConfigurationChanged(mLastOrientation, newConfig.orientation,
                             mView.getDumpableTag());
-                    onConfigurationChanged();
                     if (newConfig.orientation != mLastOrientation) {
                         mLastOrientation = newConfig.orientation;
                         switchTileLayout(false);
                     }
+                    onConfigurationChanged();
                 }
             };
 
@@ -422,6 +422,8 @@
         }
         if (mMediaHost != null) {
             pw.println("  media bounds: " + mMediaHost.getCurrentBounds());
+            pw.println("  horizontal layout: " + mUsingHorizontalLayout);
+            pw.println("  last orientation: " + mLastOrientation);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
index c86e6e8..7ce0ad0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
@@ -46,14 +46,6 @@
 @QSScope
 public class QuickQSPanelController extends QSPanelControllerBase<QuickQSPanel> {
 
-    private final QSPanel.OnConfigurationChangedListener mOnConfigurationChangedListener =
-            newConfig -> {
-                int newMaxTiles = getResources().getInteger(R.integer.quick_qs_panel_max_tiles);
-                if (newMaxTiles != mView.getNumQuickTiles()) {
-                    setMaxTiles(newMaxTiles);
-                }
-            };
-
     private final Provider<Boolean> mUsingCollapsedLandscapeMediaProvider;
 
     @Inject
@@ -99,13 +91,11 @@
     @Override
     protected void onViewAttached() {
         super.onViewAttached();
-        mView.addOnConfigurationChangedListener(mOnConfigurationChangedListener);
     }
 
     @Override
     protected void onViewDetached() {
         super.onViewDetached();
-        mView.removeOnConfigurationChangedListener(mOnConfigurationChangedListener);
     }
 
     private void setMaxTiles(int parseNumTiles) {
@@ -115,6 +105,10 @@
 
     @Override
     protected void onConfigurationChanged() {
+        int newMaxTiles = getResources().getInteger(R.integer.quick_qs_panel_max_tiles);
+        if (newMaxTiles != mView.getNumQuickTiles()) {
+            setMaxTiles(newMaxTiles);
+        }
         updateMediaExpansion();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index e06c977..073ab8b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -1199,7 +1199,8 @@
             return ((!updateMonitor.isUnlockingWithBiometricAllowed(true /* isStrongBiometric */)
                     && msgId != FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT)
                     || msgId == FingerprintManager.FINGERPRINT_ERROR_CANCELED
-                    || msgId == FingerprintManager.FINGERPRINT_ERROR_USER_CANCELED);
+                    || msgId == FingerprintManager.FINGERPRINT_ERROR_USER_CANCELED
+                    || msgId == FingerprintManager.BIOMETRIC_ERROR_POWER_PRESSED);
         }
 
         private boolean shouldSuppressFaceError(int msgId, KeyguardUpdateMonitor updateMonitor) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
index 753e940..149ed0a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
@@ -19,14 +19,17 @@
 import android.annotation.Nullable;
 import android.view.View;
 
-import com.android.systemui.Dumpable;
 import com.android.systemui.demomode.DemoMode;
 import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
 
 import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
 
-public interface BatteryController extends DemoMode, Dumpable,
+/**
+ * Controller for battery related information, including the charge level, power save mode,
+ * and time remaining information
+ */
+public interface BatteryController extends DemoMode,
         CallbackController<BatteryStateChangeCallback> {
     /**
      * Prints the current state of the {@link BatteryController} to the given {@link PrintWriter}.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
index 33ddf7e..c7ad767 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
@@ -38,11 +38,13 @@
 import com.android.settingslib.fuelgauge.BatterySaverUtils;
 import com.android.settingslib.fuelgauge.Estimate;
 import com.android.settingslib.utils.PowerUtil;
+import com.android.systemui.Dumpable;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.demomode.DemoMode;
 import com.android.systemui.demomode.DemoModeController;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.power.EnhancedEstimates;
 import com.android.systemui.util.Assert;
 
@@ -56,7 +58,8 @@
  * Default implementation of a {@link BatteryController}. This controller monitors for battery
  * level change events that are broadcasted by the system.
  */
-public class BatteryControllerImpl extends BroadcastReceiver implements BatteryController {
+public class BatteryControllerImpl extends BroadcastReceiver implements BatteryController,
+        Dumpable {
     private static final String TAG = "BatteryController";
 
     private static final String ACTION_LEVEL_TEST = "com.android.systemui.BATTERY_LEVEL_TEST";
@@ -70,6 +73,7 @@
     private final ArrayList<EstimateFetchCompletion> mFetchCallbacks = new ArrayList<>();
     private final PowerManager mPowerManager;
     private final DemoModeController mDemoModeController;
+    private final DumpManager mDumpManager;
     private final Handler mMainHandler;
     private final Handler mBgHandler;
     protected final Context mContext;
@@ -101,6 +105,7 @@
             PowerManager powerManager,
             BroadcastDispatcher broadcastDispatcher,
             DemoModeController demoModeController,
+            DumpManager dumpManager,
             @Main Handler mainHandler,
             @Background Handler bgHandler) {
         mContext = context;
@@ -110,6 +115,7 @@
         mEstimates = enhancedEstimates;
         mBroadcastDispatcher = broadcastDispatcher;
         mDemoModeController = demoModeController;
+        mDumpManager = dumpManager;
     }
 
     private void registerReceiver() {
@@ -134,6 +140,7 @@
             }
         }
         mDemoModeController.addCallback(this);
+        mDumpManager.registerDumpable(TAG, this);
         updatePowerSave();
         updateEstimateInBackground();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
index 27746c0..00ed3d6 100644
--- a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
@@ -36,6 +36,7 @@
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.dock.DockManagerImpl;
 import com.android.systemui.doze.DozeHost;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.navigationbar.gestural.GestureModule;
 import com.android.systemui.plugins.qs.QSFactory;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -116,9 +117,12 @@
     static BatteryController provideBatteryController(Context context,
             EnhancedEstimates enhancedEstimates, PowerManager powerManager,
             BroadcastDispatcher broadcastDispatcher, DemoModeController demoModeController,
+            DumpManager dumpManager,
             @Main Handler mainHandler, @Background Handler bgHandler) {
         BatteryController bC = new BatteryControllerImpl(context, enhancedEstimates, powerManager,
-                broadcastDispatcher, demoModeController, mainHandler, bgHandler);
+                broadcastDispatcher, demoModeController,
+                dumpManager,
+                mainHandler, bgHandler);
         bC.init();
         return bC;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt
index d7ad3ce..938417f 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt
@@ -174,6 +174,7 @@
             setOnItemClickListener { _, _, position, _ ->
                 val itemPositionExcludingHeader = position - 1
                 adapter.getItem(itemPositionExcludingHeader).onClicked()
+                dismiss()
             }
 
             show()
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt
index 2714cf4..485a7e5 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt
@@ -86,7 +86,6 @@
     becauseCannotSkipBouncer = false,
     biometricSettingEnabledForUser = false,
     bouncerFullyShown = false,
-    bouncerIsOrWillShow = false,
     onlyFaceEnrolled = false,
     faceAuthenticated = false,
     faceDisabled = false,
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index ae980f5..409457e 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -1554,7 +1554,7 @@
                 anyBoolean());
         CancellationSignal cancelSignal = mCancellationSignalCaptor.getValue();
 
-        bouncerWillBeVisibleSoon();
+        bouncerFullyVisible();
         mTestableLooper.processAllMessages();
 
         assertThat(cancelSignal.isCanceled()).isTrue();
@@ -1752,11 +1752,6 @@
         setKeyguardBouncerVisibility(true);
     }
 
-    private void bouncerWillBeVisibleSoon() {
-        mKeyguardUpdateMonitor.sendKeyguardBouncerChanged(true, false);
-        mTestableLooper.processAllMessages();
-    }
-
     private void setKeyguardBouncerVisibility(boolean isVisible) {
         mKeyguardUpdateMonitor.sendKeyguardBouncerChanged(isVisible, isVisible);
         mTestableLooper.processAllMessages();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/DistanceClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/DistanceClassifierTest.java
index 8e00d10..faa5db4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/classifier/DistanceClassifierTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/DistanceClassifierTest.java
@@ -17,7 +17,7 @@
 package com.android.systemui.classifier;
 
 import static com.android.systemui.classifier.Classifier.BRIGHTNESS_SLIDER;
-import static com.android.systemui.classifier.Classifier.QS_SWIPE;
+import static com.android.systemui.classifier.Classifier.QS_SWIPE_SIDE;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -106,9 +106,9 @@
     }
 
     @Test
-    public void testPass_QsSwipeAlwaysPasses() {
+    public void testPass_QsSwipeSideAlwaysPasses() {
         mClassifier.onTouchEvent(appendDownEvent(1, 1));
-        assertThat(mClassifier.classifyGesture(QS_SWIPE, 0.5, 1).isFalse())
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_SIDE, 0.5, 1).isFalse())
                 .isFalse();
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/TypeClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/TypeClassifierTest.java
index 1d61e29..d70d6fc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/classifier/TypeClassifierTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/TypeClassifierTest.java
@@ -22,7 +22,8 @@
 import static com.android.systemui.classifier.Classifier.NOTIFICATION_DISMISS;
 import static com.android.systemui.classifier.Classifier.NOTIFICATION_DRAG_DOWN;
 import static com.android.systemui.classifier.Classifier.PULSE_EXPAND;
-import static com.android.systemui.classifier.Classifier.QS_SWIPE;
+import static com.android.systemui.classifier.Classifier.QS_SWIPE_NESTED;
+import static com.android.systemui.classifier.Classifier.QS_SWIPE_SIDE;
 import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
 import static com.android.systemui.classifier.Classifier.RIGHT_AFFORDANCE;
 import static com.android.systemui.classifier.Classifier.UNLOCK;
@@ -323,44 +324,86 @@
     }
 
     @Test
-    public void testPass_QsSwipe() {
+    public void testPass_QsSwipeSide() {
         when(mDataProvider.isVertical()).thenReturn(false);
 
         when(mDataProvider.isUp()).thenReturn(false);  // up and right should cause no effect.
         when(mDataProvider.isRight()).thenReturn(false);
-        assertThat(mClassifier.classifyGesture(QS_SWIPE, 0.5, 0).isFalse()).isFalse();
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_SIDE, 0.5, 0).isFalse()).isFalse();
 
         when(mDataProvider.isUp()).thenReturn(true);
         when(mDataProvider.isRight()).thenReturn(false);
-        assertThat(mClassifier.classifyGesture(QS_SWIPE, 0.5, 0).isFalse()).isFalse();
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_SIDE, 0.5, 0).isFalse()).isFalse();
 
         when(mDataProvider.isUp()).thenReturn(false);
         when(mDataProvider.isRight()).thenReturn(true);
-        assertThat(mClassifier.classifyGesture(QS_SWIPE, 0.5, 0).isFalse()).isFalse();
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_SIDE, 0.5, 0).isFalse()).isFalse();
 
         when(mDataProvider.isUp()).thenReturn(true);
         when(mDataProvider.isRight()).thenReturn(true);
-        assertThat(mClassifier.classifyGesture(QS_SWIPE, 0.5, 0).isFalse()).isFalse();
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_SIDE, 0.5, 0).isFalse()).isFalse();
     }
 
     @Test
-    public void testFalse_QsSwipe() {
+    public void testFalse_QsSwipeSide() {
         when(mDataProvider.isVertical()).thenReturn(true);
 
         when(mDataProvider.isUp()).thenReturn(false);  // up and right should cause no effect.
         when(mDataProvider.isRight()).thenReturn(false);
-        assertThat(mClassifier.classifyGesture(QS_SWIPE, 0.5, 0).isFalse()).isTrue();
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_SIDE, 0.5, 0).isFalse()).isTrue();
 
         when(mDataProvider.isUp()).thenReturn(true);
         when(mDataProvider.isRight()).thenReturn(false);
-        assertThat(mClassifier.classifyGesture(QS_SWIPE, 0.5, 0).isFalse()).isTrue();
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_SIDE, 0.5, 0).isFalse()).isTrue();
 
         when(mDataProvider.isUp()).thenReturn(false);
         when(mDataProvider.isRight()).thenReturn(true);
-        assertThat(mClassifier.classifyGesture(QS_SWIPE, 0.5, 0).isFalse()).isTrue();
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_SIDE, 0.5, 0).isFalse()).isTrue();
 
         when(mDataProvider.isUp()).thenReturn(true);
         when(mDataProvider.isRight()).thenReturn(true);
-        assertThat(mClassifier.classifyGesture(QS_SWIPE, 0.5, 0).isFalse()).isTrue();
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_SIDE, 0.5, 0).isFalse()).isTrue();
+    }
+
+    @Test
+    public void testPass_QsNestedSwipe() {
+        when(mDataProvider.isVertical()).thenReturn(true);
+
+        when(mDataProvider.isUp()).thenReturn(false);  // up and right should cause no effect.
+        when(mDataProvider.isRight()).thenReturn(false);
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_NESTED, 0.5, 0).isFalse()).isFalse();
+
+        when(mDataProvider.isUp()).thenReturn(true);
+        when(mDataProvider.isRight()).thenReturn(false);
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_NESTED, 0.5, 0).isFalse()).isFalse();
+
+        when(mDataProvider.isUp()).thenReturn(false);
+        when(mDataProvider.isRight()).thenReturn(true);
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_NESTED, 0.5, 0).isFalse()).isFalse();
+
+        when(mDataProvider.isUp()).thenReturn(true);
+        when(mDataProvider.isRight()).thenReturn(true);
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_NESTED, 0.5, 0).isFalse()).isFalse();
+    }
+
+    @Test
+    public void testFalse_QsNestedSwipe() {
+        when(mDataProvider.isVertical()).thenReturn(false);
+
+        when(mDataProvider.isUp()).thenReturn(false);  // up and right should cause no effect.
+        when(mDataProvider.isRight()).thenReturn(false);
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_NESTED, 0.5, 0).isFalse()).isTrue();
+
+        when(mDataProvider.isUp()).thenReturn(true);
+        when(mDataProvider.isRight()).thenReturn(false);
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_NESTED, 0.5, 0).isFalse()).isTrue();
+
+        when(mDataProvider.isUp()).thenReturn(false);
+        when(mDataProvider.isRight()).thenReturn(true);
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_NESTED, 0.5, 0).isFalse()).isTrue();
+
+        when(mDataProvider.isUp()).thenReturn(true);
+        when(mDataProvider.isRight()).thenReturn(true);
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_NESTED, 0.5, 0).isFalse()).isTrue();
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
index 9d4275e..eec33ca 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
@@ -23,8 +23,10 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.content.ComponentName;
 import android.content.Intent;
 import android.os.IBinder;
+import android.os.RemoteException;
 import android.service.dreams.DreamService;
 import android.service.dreams.IDreamOverlay;
 import android.service.dreams.IDreamOverlayCallback;
@@ -57,6 +59,8 @@
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 public class DreamOverlayServiceTest extends SysuiTestCase {
+    private static final ComponentName LOW_LIGHT_COMPONENT = new ComponentName("package",
+            "lowlight");
     private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
     private final FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock);
 
@@ -129,7 +133,8 @@
                 mDreamOverlayComponentFactory,
                 mStateController,
                 mKeyguardUpdateMonitor,
-                mUiEventLogger);
+                mUiEventLogger,
+                LOW_LIGHT_COMPONENT);
     }
 
     @Test
@@ -204,6 +209,22 @@
     }
 
     @Test
+    public void testLowLightSetByIntentExtra() throws RemoteException {
+        final Intent intent = new Intent();
+        intent.putExtra(DreamService.EXTRA_DREAM_COMPONENT, LOW_LIGHT_COMPONENT);
+
+        final IBinder proxy = mService.onBind(intent);
+        final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+        assertThat(mService.getDreamComponent()).isEqualTo(LOW_LIGHT_COMPONENT);
+
+        // Inform the overlay service of dream starting.
+        overlay.startDream(mWindowParams, mDreamOverlayCallback);
+        mMainExecutor.runAllReady();
+
+        verify(mStateController).setLowLightActive(true);
+    }
+
+    @Test
     public void testDestroy() {
         mService.onDestroy();
         mMainExecutor.runAllReady();
@@ -211,6 +232,7 @@
         verify(mKeyguardUpdateMonitor).removeCallback(any());
         verify(mLifecycleRegistry).setCurrentState(Lifecycle.State.DESTROYED);
         verify(mStateController).setOverlayActive(false);
+        verify(mStateController).setLowLightActive(false);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
index 2adf285..d1d32a1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
@@ -218,4 +218,20 @@
         assertThat(stateController.getComplications(true).contains(complication))
                 .isTrue();
     }
+
+    @Test
+    public void testNotifyLowLightChanged() {
+        final DreamOverlayStateController stateController =
+                new DreamOverlayStateController(mExecutor);
+
+        stateController.addCallback(mCallback);
+        mExecutor.runAllReady();
+        assertThat(stateController.isLowLightActive()).isFalse();
+
+        stateController.setLowLightActive(true);
+
+        mExecutor.runAllReady();
+        verify(mCallback, times(1)).onStateChanged();
+        assertThat(stateController.isLowLightActive()).isTrue();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
index 4ebae98..aa02178 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
@@ -24,6 +24,7 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -101,6 +102,8 @@
     DreamOverlayStatusBarItemsProvider.StatusBarItem mStatusBarItem;
     @Mock
     View mStatusBarItemView;
+    @Mock
+    DreamOverlayStateController mDreamOverlayStateController;
 
     private final Executor mMainExecutor = Runnable::run;
 
@@ -126,7 +129,8 @@
                 Optional.of(mDreamOverlayNotificationCountProvider),
                 mZenModeController,
                 mStatusBarWindowStateController,
-                mDreamOverlayStatusBarItemsProvider);
+                mDreamOverlayStatusBarItemsProvider,
+                mDreamOverlayStateController);
     }
 
     @Test
@@ -137,6 +141,7 @@
         verify(mZenModeController).addCallback(any());
         verify(mDreamOverlayNotificationCountProvider).addCallback(any());
         verify(mDreamOverlayStatusBarItemsProvider).addCallback(any());
+        verify(mDreamOverlayStateController).addCallback(any());
     }
 
     @Test
@@ -266,7 +271,8 @@
                 Optional.empty(),
                 mZenModeController,
                 mStatusBarWindowStateController,
-                mDreamOverlayStatusBarItemsProvider);
+                mDreamOverlayStatusBarItemsProvider,
+                mDreamOverlayStateController);
         controller.onViewAttached();
         verify(mView, never()).showIcon(
                 eq(DreamOverlayStatusBarView.STATUS_ICON_NOTIFICATIONS), eq(true), any());
@@ -305,6 +311,7 @@
         verify(mZenModeController).removeCallback(any());
         verify(mDreamOverlayNotificationCountProvider).removeCallback(any());
         verify(mDreamOverlayStatusBarItemsProvider).removeCallback(any());
+        verify(mDreamOverlayStateController).removeCallback(any());
     }
 
     @Test
@@ -458,6 +465,7 @@
     @Test
     public void testStatusBarShownWhenSystemStatusBarHidden() {
         mController.onViewAttached();
+        reset(mView);
 
         final ArgumentCaptor<StatusBarWindowStateListener>
                 callbackCapture = ArgumentCaptor.forClass(StatusBarWindowStateListener.class);
@@ -471,6 +479,7 @@
     public void testUnattachedStatusBarVisibilityUnchangedWhenSystemStatusBarHidden() {
         mController.onViewAttached();
         mController.onViewDetached();
+        reset(mView);
 
         final ArgumentCaptor<StatusBarWindowStateListener>
                 callbackCapture = ArgumentCaptor.forClass(StatusBarWindowStateListener.class);
@@ -493,4 +502,21 @@
 
         verify(mView).setExtraStatusBarItemViews(List.of(mStatusBarItemView));
     }
+
+    @Test
+    public void testLowLightHidesStatusBar() {
+        when(mDreamOverlayStateController.isLowLightActive()).thenReturn(true);
+        mController.onViewAttached();
+
+        verify(mView).setVisibility(View.INVISIBLE);
+        reset(mView);
+
+        when(mDreamOverlayStateController.isLowLightActive()).thenReturn(false);
+        final ArgumentCaptor<DreamOverlayStateController.Callback> callbackCapture =
+                ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class);
+        verify(mDreamOverlayStateController).addCallback(callbackCapture.capture());
+        callbackCapture.getValue().onStateChanged();
+
+        verify(mView).setVisibility(View.VISIBLE);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
index 22ecb4b..5f64336 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
@@ -23,6 +23,8 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.WallpaperColors;
+import android.graphics.Bitmap;
 import android.graphics.drawable.Icon;
 import android.testing.AndroidTestingRunner;
 import android.view.View;
@@ -102,6 +104,18 @@
     }
 
     @Test
+    public void getItemId_validPosition_returnCorrespondingId() {
+        assertThat(mMediaOutputAdapter.getItemId(0)).isEqualTo(mMediaDevices.get(
+                0).getId().hashCode());
+    }
+
+    @Test
+    public void getItemId_invalidPosition_returnPosition() {
+        int invalidPosition = mMediaDevices.size() + 1;
+        assertThat(mMediaOutputAdapter.getItemId(invalidPosition)).isEqualTo(invalidPosition);
+    }
+
+    @Test
     public void onBindViewHolder_bindPairNew_verifyView() {
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 2);
 
@@ -155,6 +169,33 @@
     }
 
     @Test
+    public void onBindViewHolder_bindConnectedDeviceWithMutingExpectedDeviceExist_verifyView() {
+        when(mMediaOutputController.hasMutingExpectedDevice()).thenReturn(true);
+        when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(false);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+        assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_1);
+    }
+
+    @Test
+    public void onBindViewHolder_isMutingExpectedDevice_verifyView() {
+        when(mMediaDevice1.isMutingExpectedDevice()).thenReturn(true);
+        when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(false);
+        when(mMediaOutputController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(false);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+        assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_1);
+    }
+
+    @Test
     public void onBindViewHolder_initSeekbar_setsVolume() {
         when(mMediaDevice1.getMaxVolume()).thenReturn(TEST_MAX_VOLUME);
         when(mMediaDevice1.getCurrentVolume()).thenReturn(TEST_CURRENT_VOLUME);
@@ -165,6 +206,20 @@
     }
 
     @Test
+    public void onBindViewHolder_bindSelectableDevice_verifyView() {
+        List<MediaDevice> selectableDevices = new ArrayList<>();
+        selectableDevices.add(mMediaDevice2);
+        when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(selectableDevices);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
+
+        assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2);
+    }
+
+    @Test
     public void onBindViewHolder_bindNonActiveConnectedDevice_verifyView() {
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
 
@@ -223,6 +278,22 @@
     }
 
     @Test
+    public void onBindViewHolder_bindGroupingDevice_verifyView() {
+        when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(false);
+        when(mMediaDevice1.getState()).thenReturn(
+                LocalMediaManager.MediaDeviceState.STATE_GROUPING);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+        assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_1);
+        assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
+    }
+
+    @Test
     public void onBindViewHolder_inTransferring_bindNonTransferringDevice_verifyView() {
         when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(true);
         when(mMediaDevice2.getState()).thenReturn(
@@ -256,6 +327,31 @@
     }
 
     @Test
+    public void onItemClick_clicksWithMutingExpectedDeviceExist_cancelsMuteAwaitConnection() {
+        when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(false);
+        when(mMediaOutputController.hasMutingExpectedDevice()).thenReturn(true);
+        when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(false);
+        when(mMediaDevice1.isMutingExpectedDevice()).thenReturn(false);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+        mViewHolder.mContainerLayout.performClick();
+
+        verify(mMediaOutputController).cancelMuteAwaitConnection();
+    }
+
+    @Test
+    public void onItemClick_clicksSelectableDevice_triggerGrouping() {
+        List<MediaDevice> selectableDevices = new ArrayList<>();
+        selectableDevices.add(mMediaDevice2);
+        when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(selectableDevices);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
+
+        mViewHolder.mContainerLayout.performClick();
+
+        verify(mMediaOutputController).addDeviceToPlayMedia(mMediaDevice2);
+    }
+
+    @Test
     public void onItemClick_onGroupActionTriggered_verifySeekbarDisabled() {
         when(mMediaOutputController.getSelectedMediaDevice()).thenReturn(mMediaDevices);
         List<MediaDevice> selectableDevices = new ArrayList<>();
@@ -280,4 +376,14 @@
 
         assertThat(mViewHolder.mSeekBar.isEnabled()).isTrue();
     }
+
+    @Test
+    public void updateColorScheme_triggerController() {
+        WallpaperColors wallpaperColors = WallpaperColors.fromBitmap(
+                Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888));
+
+        mMediaOutputAdapter.updateColorScheme(wallpaperColors, true);
+
+        verify(mMediaOutputController).setCurrentColorScheme(wallpaperColors, true);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
index eb8ecae..9be201e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
@@ -241,46 +241,29 @@
     }
 
     @Test
-    public void onStart_isBroadcasting_verifyRegisterLeBroadcastServiceCallBack() {
+    public void whenBroadcasting_verifyLeBroadcastServiceCallBackIsRegisteredAndUnregistered() {
         when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn(
                 mLocalBluetoothLeBroadcast);
         mIsBroadcasting = true;
 
         mMediaOutputBaseDialogImpl.onStart();
-
         verify(mLocalBluetoothLeBroadcast).registerServiceCallBack(any(), any());
-    }
-
-    @Test
-    public void onStart_notBroadcasting_noRegisterLeBroadcastServiceCallBack() {
-        when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn(
-                mLocalBluetoothLeBroadcast);
-        mIsBroadcasting = false;
-
-        mMediaOutputBaseDialogImpl.onStart();
-
-        verify(mLocalBluetoothLeBroadcast, never()).registerServiceCallBack(any(), any());
-    }
-
-    @Test
-    public void onStart_isBroadcasting_verifyUnregisterLeBroadcastServiceCallBack() {
-        when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn(
-                mLocalBluetoothLeBroadcast);
-        mIsBroadcasting = true;
 
         mMediaOutputBaseDialogImpl.onStop();
-
         verify(mLocalBluetoothLeBroadcast).unregisterServiceCallBack(any());
     }
 
     @Test
-    public void onStop_notBroadcasting_noUnregisterLeBroadcastServiceCallBack() {
+    public void
+            whenNotBroadcasting_verifyLeBroadcastServiceCallBackIsNotRegisteredOrUnregistered() {
         when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn(
                 mLocalBluetoothLeBroadcast);
         mIsBroadcasting = false;
 
+        mMediaOutputBaseDialogImpl.onStart();
         mMediaOutputBaseDialogImpl.onStop();
 
+        verify(mLocalBluetoothLeBroadcast, never()).registerServiceCallBack(any(), any());
         verify(mLocalBluetoothLeBroadcast, never()).unregisterServiceCallBack(any());
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
index 6dcf802..cb31fde 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
@@ -35,6 +35,7 @@
 import android.app.Notification;
 import android.content.Context;
 import android.graphics.drawable.Icon;
+import android.media.AudioDeviceAttributes;
 import android.media.AudioManager;
 import android.media.MediaDescription;
 import android.media.MediaMetadata;
@@ -279,6 +280,203 @@
     }
 
     @Test
+    public void onDeviceListUpdate_isRefreshing_updatesNeedRefreshToTrue() {
+        mMediaOutputController.start(mCb);
+        reset(mCb);
+        mMediaOutputController.mIsRefreshing = true;
+
+        mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+
+        assertThat(mMediaOutputController.mNeedRefresh).isTrue();
+    }
+
+    @Test
+    public void cancelMuteAwaitConnection_cancelsWithMediaManager() {
+        when(mAudioManager.getMutingExpectedDevice()).thenReturn(mock(AudioDeviceAttributes.class));
+        mMediaOutputController.start(mCb);
+        reset(mCb);
+
+        mMediaOutputController.cancelMuteAwaitConnection();
+
+        verify(mAudioManager).cancelMuteAwaitConnection(any());
+    }
+
+    @Test
+    public void cancelMuteAwaitConnection_audioManagerIsNull_noAction() {
+        when(mAudioManager.getMutingExpectedDevice()).thenReturn(null);
+        mMediaOutputController.start(mCb);
+        reset(mCb);
+        mMediaOutputController.cancelMuteAwaitConnection();
+
+        verify(mAudioManager, never()).cancelMuteAwaitConnection(any());
+    }
+
+    @Test
+    public void getAppSourceName_packageNameIsNull_returnsNull() {
+        MediaOutputController testMediaOutputController = new MediaOutputController(mSpyContext,
+                "",
+                mMediaSessionManager, mLocalBluetoothManager, mStarter,
+                mNotifCollection, mDialogLaunchAnimator,
+                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+                mKeyguardManager);
+        testMediaOutputController.start(mCb);
+        reset(mCb);
+
+        testMediaOutputController.getAppSourceName();
+
+        assertThat(testMediaOutputController.getAppSourceName()).isNull();
+    }
+
+    @Test
+    public void isActiveItem_deviceNotConnected_returnsFalse() {
+        when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice2);
+
+        assertThat(mMediaOutputController.isActiveItem(mMediaDevice1)).isFalse();
+    }
+
+    @Test
+    public void getNotificationSmallIcon_packageNameIsNull_returnsNull() {
+        MediaOutputController testMediaOutputController = new MediaOutputController(mSpyContext,
+                "",
+                mMediaSessionManager, mLocalBluetoothManager, mStarter,
+                mNotifCollection, mDialogLaunchAnimator,
+                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+                mKeyguardManager);
+        testMediaOutputController.start(mCb);
+        reset(mCb);
+
+        testMediaOutputController.getAppSourceName();
+
+        assertThat(testMediaOutputController.getNotificationSmallIcon()).isNull();
+    }
+
+    @Test
+    public void refreshDataSetIfNeeded_needRefreshIsTrue_setsToFalse() {
+        mMediaOutputController.start(mCb);
+        reset(mCb);
+        mMediaOutputController.mNeedRefresh = true;
+
+        mMediaOutputController.refreshDataSetIfNeeded();
+
+        assertThat(mMediaOutputController.mNeedRefresh).isFalse();
+    }
+
+    @Test
+    public void isCurrentConnectedDeviceRemote_containsFeatures_returnsTrue() {
+        when(mMediaDevice1.getFeatures()).thenReturn(
+                ImmutableList.of(MediaRoute2Info.FEATURE_REMOTE_PLAYBACK));
+        when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice1);
+
+        assertThat(mMediaOutputController.isCurrentConnectedDeviceRemote()).isTrue();
+    }
+
+    @Test
+    public void addDeviceToPlayMedia_triggersFromLocalMediaManager() {
+        MediaOutputController testMediaOutputController = new MediaOutputController(mSpyContext,
+                null,
+                mMediaSessionManager, mLocalBluetoothManager, mStarter,
+                mNotifCollection, mDialogLaunchAnimator,
+                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+                mKeyguardManager);
+
+        LocalMediaManager testLocalMediaManager = spy(testMediaOutputController.mLocalMediaManager);
+        testMediaOutputController.mLocalMediaManager = testLocalMediaManager;
+
+        testMediaOutputController.addDeviceToPlayMedia(mMediaDevice2);
+
+        verify(testLocalMediaManager).addDeviceToPlayMedia(mMediaDevice2);
+    }
+
+    @Test
+    public void removeDeviceFromPlayMedia_triggersFromLocalMediaManager() {
+        MediaOutputController testMediaOutputController = new MediaOutputController(mSpyContext,
+                null,
+                mMediaSessionManager, mLocalBluetoothManager, mStarter,
+                mNotifCollection, mDialogLaunchAnimator,
+                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+                mKeyguardManager);
+
+        LocalMediaManager testLocalMediaManager = spy(testMediaOutputController.mLocalMediaManager);
+        testMediaOutputController.mLocalMediaManager = testLocalMediaManager;
+
+        testMediaOutputController.removeDeviceFromPlayMedia(mMediaDevice2);
+
+        verify(testLocalMediaManager).removeDeviceFromPlayMedia(mMediaDevice2);
+    }
+
+    @Test
+    public void getDeselectableMediaDevice_triggersFromLocalMediaManager() {
+        mMediaOutputController.getDeselectableMediaDevice();
+
+        verify(mLocalMediaManager).getDeselectableMediaDevice();
+    }
+
+    @Test
+    public void adjustSessionVolume_adjustWithoutId_triggersFromLocalMediaManager() {
+        int testVolume = 10;
+        mMediaOutputController.adjustSessionVolume(testVolume);
+
+        verify(mLocalMediaManager).adjustSessionVolume(testVolume);
+    }
+
+    @Test
+    public void getSessionVolumeMax_triggersFromLocalMediaManager() {
+        mMediaOutputController.getSessionVolumeMax();
+
+        verify(mLocalMediaManager).getSessionVolumeMax();
+    }
+
+    @Test
+    public void getSessionVolume_triggersFromLocalMediaManager() {
+        mMediaOutputController.getSessionVolume();
+
+        verify(mLocalMediaManager).getSessionVolume();
+    }
+
+    @Test
+    public void getSessionName_triggersFromLocalMediaManager() {
+        mMediaOutputController.getSessionName();
+
+        verify(mLocalMediaManager).getSessionName();
+    }
+
+    @Test
+    public void releaseSession_triggersFromLocalMediaManager() {
+        mMediaOutputController.releaseSession();
+
+        verify(mLocalMediaManager).releaseSession();
+    }
+
+    @Test
+    public void isAnyDeviceTransferring_noDevicesStateIsConnecting_returnsFalse() {
+        mMediaOutputController.start(mCb);
+        reset(mCb);
+
+        mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+
+        assertThat(mMediaOutputController.isAnyDeviceTransferring()).isFalse();
+    }
+
+    @Test
+    public void isAnyDeviceTransferring_deviceStateIsConnecting_returnsTrue() {
+        when(mMediaDevice1.getState()).thenReturn(
+                LocalMediaManager.MediaDeviceState.STATE_CONNECTING);
+        mMediaOutputController.start(mCb);
+        reset(mCb);
+
+        mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+
+        assertThat(mMediaOutputController.isAnyDeviceTransferring()).isTrue();
+    }
+
+    @Test
+    public void isPlaying_stateIsNull() {
+        when(mMediaController.getPlaybackState()).thenReturn(null);
+
+        assertThat(mMediaOutputController.isPlaying()).isFalse();
+    }
+
+    @Test
     public void onSelectedDeviceStateChanged_verifyCallback() {
         when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice2);
         mMediaOutputController.start(mCb);
@@ -535,6 +733,44 @@
     }
 
     @Test
+    public void getNotificationSmallIcon_withoutSmallIcon_returnsNull() {
+        final List<NotificationEntry> entryList = new ArrayList<>();
+        final NotificationEntry entry = mock(NotificationEntry.class);
+        final StatusBarNotification sbn = mock(StatusBarNotification.class);
+        final Notification notification = mock(Notification.class);
+        entryList.add(entry);
+
+        when(mNotifCollection.getAllNotifs()).thenReturn(entryList);
+        when(entry.getSbn()).thenReturn(sbn);
+        when(sbn.getNotification()).thenReturn(notification);
+        when(sbn.getPackageName()).thenReturn(TEST_PACKAGE_NAME);
+        when(notification.isMediaNotification()).thenReturn(true);
+        when(notification.getSmallIcon()).thenReturn(null);
+
+        assertThat(mMediaOutputController.getNotificationSmallIcon()).isNull();
+    }
+
+    @Test
+    public void getNotificationSmallIcon_withPackageNameAndMediaSession_returnsIconCompat() {
+        final List<NotificationEntry> entryList = new ArrayList<>();
+        final NotificationEntry entry = mock(NotificationEntry.class);
+        final StatusBarNotification sbn = mock(StatusBarNotification.class);
+        final Notification notification = mock(Notification.class);
+        final Icon icon = mock(Icon.class);
+        entryList.add(entry);
+
+        when(mNotifCollection.getAllNotifs()).thenReturn(entryList);
+        when(entry.getSbn()).thenReturn(sbn);
+        when(sbn.getNotification()).thenReturn(notification);
+        when(sbn.getPackageName()).thenReturn(TEST_PACKAGE_NAME);
+        when(notification.isMediaNotification()).thenReturn(true);
+        when(notification.getSmallIcon()).thenReturn(icon);
+
+        assertThat(mMediaOutputController.getNotificationSmallIcon()).isInstanceOf(
+                IconCompat.class);
+    }
+
+    @Test
     public void isVolumeControlEnabled_isCastWithVolumeFixed_returnsFalse() {
         when(mMediaDevice1.getDeviceType()).thenReturn(
                 MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
index 9557513..bae3569 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
@@ -25,7 +25,10 @@
 import static org.mockito.Mockito.when;
 
 import android.app.KeyguardManager;
+import android.graphics.Bitmap;
 import android.media.AudioManager;
+import android.media.MediaDescription;
+import android.media.MediaMetadata;
 import android.media.MediaRoute2Info;
 import android.media.session.MediaController;
 import android.media.session.MediaSessionManager;
@@ -43,6 +46,7 @@
 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
 import com.android.settingslib.media.LocalMediaManager;
 import com.android.settingslib.media.MediaDevice;
+import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.broadcast.BroadcastSender;
@@ -82,6 +86,8 @@
     private final CommonNotifCollection mNotifCollection = mock(CommonNotifCollection.class);
     private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class);
     private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
+    private final MediaMetadata mMediaMetadata = mock(MediaMetadata.class);
+    private final MediaDescription  mMediaDescription = mock(MediaDescription.class);
     private final NearbyMediaDevicesManager mNearbyMediaDevicesManager = mock(
             NearbyMediaDevicesManager.class);
     private final AudioManager mAudioManager = mock(AudioManager.class);
@@ -100,6 +106,8 @@
         when(mMediaController.getPlaybackState()).thenReturn(mPlaybackState);
         when(mPlaybackState.getState()).thenReturn(PlaybackState.STATE_NONE);
         when(mMediaController.getPackageName()).thenReturn(TEST_PACKAGE);
+        when(mMediaController.getMetadata()).thenReturn(mMediaMetadata);
+        when(mMediaMetadata.getDescription()).thenReturn(mMediaDescription);
         mMediaControllers.add(mMediaController);
         when(mMediaSessionManager.getActiveSessions(any())).thenReturn(mMediaControllers);
 
@@ -207,6 +215,80 @@
     }
 
     @Test
+    public void getHeaderIcon_getFromMediaControllerMetaData() {
+        int testWidth = 10;
+        int testHeight = 20;
+        when(mMediaDescription.getIconBitmap())
+                .thenReturn(Bitmap.createBitmap(testWidth, testHeight, Bitmap.Config.ARGB_8888));
+
+        assertThat(mMediaOutputDialog.getHeaderIcon().getBitmap().getHeight()).isEqualTo(
+                testHeight);
+        assertThat(mMediaOutputDialog.getHeaderIcon().getBitmap().getWidth()).isEqualTo(testWidth);
+    }
+
+    @Test
+    public void getHeaderText_getFromMediaControllerMetaData() {
+        String testTitle = "test title";
+        when(mMediaDescription.getTitle())
+                .thenReturn(testTitle);
+        assertThat(mMediaOutputDialog.getHeaderText().toString()).isEqualTo(testTitle);
+    }
+
+    @Test
+    public void getHeaderSubtitle_getFromMediaControllerMetaData() {
+        String testSubtitle = "test title";
+        when(mMediaDescription.getSubtitle())
+                .thenReturn(testSubtitle);
+
+        assertThat(mMediaOutputDialog.getHeaderSubtitle().toString()).isEqualTo(testSubtitle);
+    }
+
+    @Test
+    public void getStopButtonText_notSupportsBroadcast_returnsDefaultText() {
+        String stopText = mContext.getText(R.string.keyboard_key_media_stop).toString();
+        MediaOutputController mockMediaOutputController = mock(MediaOutputController.class);
+        when(mockMediaOutputController.isBroadcastSupported()).thenReturn(false);
+
+        MediaOutputDialog testDialog = new MediaOutputDialog(mContext, false, mBroadcastSender,
+                mockMediaOutputController, mUiEventLogger);
+        testDialog.show();
+
+        assertThat(testDialog.getStopButtonText().toString()).isEqualTo(stopText);
+    }
+
+    @Test
+    public void getStopButtonText_supportsBroadcast_returnsBroadcastText() {
+        String stopText = mContext.getText(R.string.media_output_broadcast).toString();
+        MediaDevice mMediaDevice = mock(MediaDevice.class);
+        MediaOutputController mockMediaOutputController = mock(MediaOutputController.class);
+        when(mockMediaOutputController.isBroadcastSupported()).thenReturn(true);
+        when(mockMediaOutputController.getCurrentConnectedMediaDevice()).thenReturn(mMediaDevice);
+        when(mockMediaOutputController.isBluetoothLeDevice(any())).thenReturn(true);
+        when(mockMediaOutputController.isPlaying()).thenReturn(true);
+        when(mockMediaOutputController.isBluetoothLeBroadcastEnabled()).thenReturn(false);
+        MediaOutputDialog testDialog = new MediaOutputDialog(mContext, false, mBroadcastSender,
+                mockMediaOutputController, mUiEventLogger);
+        testDialog.show();
+
+        assertThat(testDialog.getStopButtonText().toString()).isEqualTo(stopText);
+    }
+
+    @Test
+    public void onStopButtonClick_notPlaying_releaseSession() {
+        MediaOutputController mockMediaOutputController = mock(MediaOutputController.class);
+        when(mockMediaOutputController.isBroadcastSupported()).thenReturn(false);
+        when(mockMediaOutputController.getCurrentConnectedMediaDevice()).thenReturn(null);
+        when(mockMediaOutputController.isPlaying()).thenReturn(false);
+        MediaOutputDialog testDialog = new MediaOutputDialog(mContext, false, mBroadcastSender,
+                mockMediaOutputController, mUiEventLogger);
+        testDialog.show();
+
+        testDialog.onStopButtonClick();
+
+        verify(mockMediaOutputController).releaseSession();
+    }
+
+    @Test
     // Check the visibility metric logging by creating a new MediaOutput dialog,
     // and verify if the calling times increases.
     public void onCreate_ShouldLogVisibility() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
index ecc8457..cbe1186 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
@@ -226,7 +226,9 @@
                 + "  Tile records:\n"
                 + "    " + mockTileString + "\n"
                 + "    " + mockTileViewString + "\n"
-                + "  media bounds: null\n";
+                + "  media bounds: null\n"
+                + "  horizontal layout: false\n"
+                + "  last orientation: 0\n";
         assertEquals(expected, w.getBuffer().toString());
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
index 39f27d4..4af5b90 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
@@ -123,8 +123,7 @@
 
     @Test
     fun mediaExpansion_afterConfigChange_inLandscape_collapsedInLandscapeTrue_updatesToCollapsed() {
-        // times(2) because both controller and base controller are registering their listeners
-        verify(quickQSPanel, times(2)).addOnConfigurationChangedListener(captor.capture())
+        verify(quickQSPanel).addOnConfigurationChangedListener(captor.capture())
 
         // verify that media starts in the expanded state by default
         verify(mediaHost).expansion = MediaHostState.EXPANDED
@@ -139,8 +138,7 @@
 
     @Test
     fun mediaExpansion_afterConfigChange_landscape_collapsedInLandscapeFalse_remainsExpanded() {
-        // times(2) because both controller and base controller are registering their listeners
-        verify(quickQSPanel, times(2)).addOnConfigurationChangedListener(captor.capture())
+        verify(quickQSPanel).addOnConfigurationChangedListener(captor.capture())
         reset(mediaHost)
 
         usingCollapsedLandscapeMedia = false
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
index 945cf7f..9a13e93 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
@@ -635,6 +635,19 @@
     }
 
     @Test
+    public void transientIndication_visibleWhenDozing_ignoresPowerPressed() {
+        createController();
+
+        mController.setVisible(true);
+        reset(mRotateTextViewController);
+        mController.getKeyguardCallback().onBiometricError(
+                FingerprintManager.BIOMETRIC_ERROR_POWER_PRESSED, "foo",
+                BiometricSourceType.FINGERPRINT);
+
+        verifyNoMessage(INDICATION_TYPE_BIOMETRIC_MESSAGE);
+    }
+
+    @Test
     public void transientIndication_swipeUpToRetry() {
         createController();
         String message = mContext.getString(R.string.keyguard_retry);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
index fda80a2..43d0fe9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
@@ -42,6 +42,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.demomode.DemoModeController;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.power.EnhancedEstimates;
 import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
 
@@ -80,6 +81,7 @@
                 mPowerManager,
                 mBroadcastDispatcher,
                 mDemoModeController,
+                mock(DumpManager.class),
                 new Handler(),
                 new Handler());
         // Can throw if updateEstimate is called on the main thread
diff --git a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
index 231d1ff..6bb19ce 100644
--- a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
+++ b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
@@ -133,6 +133,14 @@
         });
     }
 
+    public void maybeSetNoPresentationEventReasonIfNoReasonExists(@NotShownReason int reason) {
+        mEventInternal.ifPresent(event -> {
+            if (event.mCountShown == 0 && event.mNoPresentationReason == NOT_SHOWN_REASON_UNKNOWN) {
+                event.mNoPresentationReason = reason;
+            }
+        });
+    }
+
     public void maybeSetAvailableCount(@Nullable List<Dataset> datasetList,
             AutofillId currentViewId) {
         mEventInternal.ifPresent(event -> {
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 3fa6391..5c11e2c 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -2933,7 +2933,7 @@
             if (sDebug) {
                 Slog.d(TAG, "Set the response has expired.");
             }
-            mPresentationStatsEventLogger.maybeSetNoPresentationEventReason(
+            mPresentationStatsEventLogger.maybeSetNoPresentationEventReasonIfNoReasonExists(
                         NOT_SHOWN_REASON_VIEW_CHANGED);
             mPresentationStatsEventLogger.logAndEndEvent();
             return;
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index a71f51a..e31c952 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -46,6 +46,7 @@
 import static android.app.AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO;
 import static android.app.AppOpsManager.OP_RECORD_AUDIO;
 import static android.app.AppOpsManager.OP_RECORD_AUDIO_HOTWORD;
+import static android.app.AppOpsManager.OP_VIBRATE;
 import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_FAILED;
 import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_RESUMED;
 import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_STARTED;
@@ -266,6 +267,7 @@
             OP_PLAY_AUDIO,
             OP_RECORD_AUDIO,
             OP_CAMERA,
+            OP_VIBRATE,
     };
 
     private static final int MAX_UNFORWARDED_OPS = 10;
diff --git a/services/core/java/com/android/server/logcat/LogcatManagerService.java b/services/core/java/com/android/server/logcat/LogcatManagerService.java
index 1bcc21e..43732d4 100644
--- a/services/core/java/com/android/server/logcat/LogcatManagerService.java
+++ b/services/core/java/com/android/server/logcat/LogcatManagerService.java
@@ -16,6 +16,8 @@
 
 package com.android.server.logcat;
 
+import static android.os.Process.getParentPid;
+
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -38,6 +40,8 @@
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.ILogAccessDialogCallback;
+import com.android.internal.app.LogAccessDialogActivity;
 import com.android.internal.util.ArrayUtils;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
@@ -45,6 +49,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -98,7 +103,7 @@
     private final Injector mInjector;
     private final Supplier<Long> mClock;
     private final BinderService mBinderService;
-    private final LogcatManagerServiceInternal mLocalService;
+    private final LogAccessDialogCallback mDialogCallback;
     private final Handler mHandler;
     private ActivityManagerInternal mActivityManagerInternal;
     private ILogd mLogdService;
@@ -203,7 +208,8 @@
         }
     }
 
-    final class LogcatManagerServiceInternal {
+    final class LogAccessDialogCallback extends ILogAccessDialogCallback.Stub {
+        @Override
         public void approveAccessForClient(int uid, @NonNull String packageName) {
             final LogAccessClient client = new LogAccessClient(uid, packageName);
             if (DEBUG) {
@@ -213,6 +219,7 @@
             mHandler.sendMessageAtTime(msg, mClock.get());
         }
 
+        @Override
         public void declineAccessForClient(int uid, @NonNull String packageName) {
             final LogAccessClient client = new LogAccessClient(uid, packageName);
             if (DEBUG) {
@@ -299,7 +306,7 @@
         mInjector = injector;
         mClock = injector.createClock();
         mBinderService = new BinderService();
-        mLocalService = new LogcatManagerServiceInternal();
+        mDialogCallback = new LogAccessDialogCallback();
         mHandler = new LogAccessRequestHandler(injector.getLooper(), this);
     }
 
@@ -308,15 +315,14 @@
         try {
             mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
             publishBinderService("logcat", mBinderService);
-            publishLocalService(LogcatManagerServiceInternal.class, mLocalService);
         } catch (Throwable t) {
             Slog.e(TAG, "Could not start the LogcatManagerService.", t);
         }
     }
 
     @VisibleForTesting
-    LogcatManagerServiceInternal getLocalService() {
-        return mLocalService;
+    LogAccessDialogCallback getDialogCallback() {
+        return mDialogCallback;
     }
 
     @VisibleForTesting
@@ -340,13 +346,6 @@
      * access
      */
     private String getPackageName(LogAccessRequest request) {
-        if (mActivityManagerInternal != null) {
-            String packageName = mActivityManagerInternal.getPackageNameByPid(request.mPid);
-            if (packageName != null) {
-                return packageName;
-            }
-        }
-
         PackageManager pm = mContext.getPackageManager();
         if (pm == null) {
             // Decline the logd access if PackageManager is null
@@ -355,15 +354,28 @@
         }
 
         String[] packageNames = pm.getPackagesForUid(request.mUid);
-
         if (ArrayUtils.isEmpty(packageNames)) {
             // Decline the logd access if the app name is unknown
             Slog.e(TAG, "Unknown calling package name, declining the logd access");
             return null;
         }
 
-        String firstPackageName = packageNames[0];
+        if (mActivityManagerInternal != null) {
+            int pid = request.mPid;
+            String packageName = mActivityManagerInternal.getPackageNameByPid(pid);
+            while ((packageName == null || !ArrayUtils.contains(packageNames, packageName))
+                    && pid != -1) {
+                pid = getParentPid(pid);
+                packageName = mActivityManagerInternal.getPackageNameByPid(pid);
+            }
 
+            if (packageName != null && ArrayUtils.contains(packageNames, packageName)) {
+                return packageName;
+            }
+        }
+
+        Arrays.sort(packageNames);
+        String firstPackageName = packageNames[0];
         if (firstPackageName == null || firstPackageName.isEmpty()) {
             // Decline the logd access if the package name from uid is unknown
             Slog.e(TAG, "Unknown calling package name, declining the logd access");
@@ -430,6 +442,7 @@
         mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_PENDING_TIMEOUT, client),
                 mClock.get() + PENDING_CONFIRMATION_TIMEOUT_MILLIS);
         final Intent mIntent = createIntent(client);
+        mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         mContext.startActivityAsUser(mIntent, UserHandle.SYSTEM);
     }
 
@@ -530,6 +543,7 @@
 
         intent.putExtra(Intent.EXTRA_PACKAGE_NAME, client.mPackageName);
         intent.putExtra(Intent.EXTRA_UID, client.mUid);
+        intent.putExtra(LogAccessDialogActivity.EXTRA_CALLBACK, mDialogCallback.asBinder());
 
         return intent;
     }
diff --git a/services/core/java/com/android/server/pm/SuspendPackageHelper.java b/services/core/java/com/android/server/pm/SuspendPackageHelper.java
index 7ad4d22..eebde36 100644
--- a/services/core/java/com/android/server/pm/SuspendPackageHelper.java
+++ b/services/core/java/com/android/server/pm/SuspendPackageHelper.java
@@ -110,12 +110,12 @@
         final SuspendParams newSuspendParams =
                 new SuspendParams(dialogInfo, appExtras, launcherExtras);
 
-        final List<String> changedPackagesList = new ArrayList<>(packageNames.length);
-        final IntArray changedUids = new IntArray(packageNames.length);
-        final IntArray modifiedUids = new IntArray(packageNames.length);
         final List<String> unmodifiablePackages = new ArrayList<>(packageNames.length);
 
-        ArraySet<String> modifiedPackages = new ArraySet<>();
+        final List<String> notifyPackagesList = new ArrayList<>(packageNames.length);
+        final IntArray notifyUids = new IntArray(packageNames.length);
+        final ArraySet<String> changedPackagesList = new ArraySet<>(packageNames.length);
+        final IntArray changedUids = new IntArray(packageNames.length);
 
         final boolean[] canSuspend = suspended
                 ? canSuspendPackageForUser(snapshot, packageNames, userId, callingUid) : null;
@@ -143,21 +143,16 @@
 
             final WatchedArrayMap<String, SuspendParams> suspendParamsMap =
                     packageState.getUserStateOrDefault(userId).getSuspendParams();
-            if (suspended) {
-                if (suspendParamsMap != null && suspendParamsMap.containsKey(packageName)) {
-                    final SuspendParams suspendParams = suspendParamsMap.get(packageName);
-                    // Skip if there's no changes
-                    if (suspendParams != null
-                            && Objects.equals(suspendParams.getDialogInfo(), dialogInfo)
-                            && Objects.equals(suspendParams.getAppExtras(), appExtras)
-                            && Objects.equals(suspendParams.getLauncherExtras(),
-                            launcherExtras)) {
-                        // Carried over API behavior, must notify change even if no change
-                        changedPackagesList.add(packageName);
-                        changedUids.add(UserHandle.getUid(userId, packageState.getAppId()));
-                        continue;
-                    }
-                }
+
+            SuspendParams oldSuspendParams = suspendParamsMap == null
+                    ? null : suspendParamsMap.get(packageName);
+            boolean changed = !Objects.equals(oldSuspendParams, newSuspendParams);
+
+            if (suspended && !changed) {
+                // Carried over API behavior, must notify change even if no change
+                notifyPackagesList.add(packageName);
+                notifyUids.add(UserHandle.getUid(userId, packageState.getAppId()));
+                continue;
             }
 
             // If only the callingPackage is suspending this package,
@@ -166,18 +161,21 @@
                     && CollectionUtils.size(suspendParamsMap) == 1
                     && suspendParamsMap.containsKey(callingPackage);
             if (suspended || packageUnsuspended) {
+                // Always notify of a suspend call + notify when fully unsuspended
+                notifyPackagesList.add(packageName);
+                notifyUids.add(UserHandle.getUid(userId, packageState.getAppId()));
+            }
+
+            if (changed) {
                 changedPackagesList.add(packageName);
                 changedUids.add(UserHandle.getUid(userId, packageState.getAppId()));
             }
-
-            modifiedPackages.add(packageName);
-            modifiedUids.add(UserHandle.getUid(userId, packageState.getAppId()));
         }
 
         mPm.commitPackageStateMutation(null, mutator -> {
-            final int size = modifiedPackages.size();
+            final int size = changedPackagesList.size();
             for (int index = 0; index < size; index++) {
-                final String packageName  = modifiedPackages.valueAt(index);
+                final String packageName  = changedPackagesList.valueAt(index);
                 final PackageUserStateWrite userState = mutator.forPackage(packageName)
                         .userState(userId);
                 if (suspended) {
@@ -190,19 +188,19 @@
 
         final Computer newSnapshot = mPm.snapshotComputer();
 
-        if (!changedPackagesList.isEmpty()) {
-            final String[] changedPackages = changedPackagesList.toArray(new String[0]);
+        if (!notifyPackagesList.isEmpty()) {
+            final String[] notifyPackages = notifyPackagesList.toArray(new String[0]);
             sendPackagesSuspendedForUser(newSnapshot,
                     suspended ? Intent.ACTION_PACKAGES_SUSPENDED
                             : Intent.ACTION_PACKAGES_UNSUSPENDED,
-                    changedPackages, changedUids.toArray(), userId);
-            sendMyPackageSuspendedOrUnsuspended(changedPackages, suspended, userId);
+                    notifyPackages, notifyUids.toArray(), userId);
+            sendMyPackageSuspendedOrUnsuspended(notifyPackages, suspended, userId);
             mPm.scheduleWritePackageRestrictions(userId);
         }
         // Send the suspension changed broadcast to ensure suspension state is not stale.
-        if (!modifiedPackages.isEmpty()) {
+        if (!changedPackagesList.isEmpty()) {
             sendPackagesSuspendedForUser(newSnapshot, Intent.ACTION_PACKAGES_SUSPENSION_CHANGED,
-                    modifiedPackages.toArray(new String[0]), modifiedUids.toArray(), userId);
+                    changedPackagesList.toArray(new String[0]), changedUids.toArray(), userId);
         }
         return unmodifiablePackages.toArray(new String[0]);
     }
diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLogging.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLogging.java
index dc4bdaa..ce1157e 100644
--- a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLogging.java
+++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLogging.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.content.Context;
 import android.media.permission.Identity;
 import android.media.permission.IdentityContext;
 import android.media.soundtrigger.ModelParameterRange;
@@ -33,6 +34,8 @@
 import android.os.RemoteException;
 import android.util.Log;
 
+import com.android.internal.util.LatencyTracker;
+
 import java.io.PrintWriter;
 import java.text.SimpleDateFormat;
 import java.util.Date;
@@ -65,9 +68,12 @@
 public class SoundTriggerMiddlewareLogging implements ISoundTriggerMiddlewareInternal, Dumpable {
     private static final String TAG = "SoundTriggerMiddlewareLogging";
     private final @NonNull ISoundTriggerMiddlewareInternal mDelegate;
+    private final @NonNull Context mContext;
 
-    public SoundTriggerMiddlewareLogging(@NonNull ISoundTriggerMiddlewareInternal delegate) {
+    public SoundTriggerMiddlewareLogging(@NonNull Context context,
+            @NonNull ISoundTriggerMiddlewareInternal delegate) {
         mDelegate = delegate;
+        mContext = context;
     }
 
     @Override
@@ -298,6 +304,7 @@
                     int captureSession)
                     throws RemoteException {
                 try {
+                    startKeyphraseEventLatencyTracking(event);
                     mCallbackDelegate.onPhraseRecognition(modelHandle, event, captureSession);
                     logVoidReturn("onPhraseRecognition", modelHandle, event);
                 } catch (Exception e) {
@@ -347,6 +354,26 @@
                 logVoidReturnWithObject(this, mOriginatorIdentity, methodName, args);
             }
 
+            /**
+             * Starts the latency tracking log for keyphrase hotword invocation.
+             * The measurement covers from when the SoundTrigger HAL emits an event to when the
+             * {@link android.service.voice.VoiceInteractionSession} system UI view is shown.
+             */
+            private void startKeyphraseEventLatencyTracking(PhraseRecognitionEvent event) {
+                String latencyTrackerTag = null;
+                if (event.phraseExtras.length > 0) {
+                    latencyTrackerTag = "KeyphraseId=" + event.phraseExtras[0].id;
+                }
+                LatencyTracker latencyTracker = LatencyTracker.getInstance(mContext);
+                // To avoid adding cancel to all of the different failure modes between here and
+                // showing the system UI, we defensively cancel once.
+                // Either we hit the LatencyTracker timeout of 15 seconds or we defensively cancel
+                // here if any error occurs.
+                latencyTracker.onActionCancel(LatencyTracker.ACTION_SHOW_VOICE_INTERACTION);
+                latencyTracker.onActionStart(LatencyTracker.ACTION_SHOW_VOICE_INTERACTION,
+                        latencyTrackerTag);
+            }
+
             @Override
             public IBinder asBinder() {
                 return mCallbackDelegate.asBinder();
diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java
index 1995e54..807ed14 100644
--- a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java
+++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java
@@ -20,14 +20,14 @@
 
 import android.annotation.NonNull;
 import android.content.Context;
-import android.media.soundtrigger.ModelParameterRange;
-import android.media.soundtrigger.PhraseSoundModel;
-import android.media.soundtrigger.RecognitionConfig;
-import android.media.soundtrigger.SoundModel;
 import android.media.permission.ClearCallingIdentityContext;
 import android.media.permission.Identity;
 import android.media.permission.PermissionUtil;
 import android.media.permission.SafeCloseable;
+import android.media.soundtrigger.ModelParameterRange;
+import android.media.soundtrigger.PhraseSoundModel;
+import android.media.soundtrigger.RecognitionConfig;
+import android.media.soundtrigger.SoundModel;
 import android.media.soundtrigger_middleware.ISoundTriggerCallback;
 import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService;
 import android.media.soundtrigger_middleware.ISoundTriggerModule;
@@ -226,12 +226,13 @@
             HalFactory[] factories = new HalFactory[]{new DefaultHalFactory()};
 
             publishBinderService(Context.SOUND_TRIGGER_MIDDLEWARE_SERVICE,
-                    new SoundTriggerMiddlewareService(new SoundTriggerMiddlewareLogging(
-                            new SoundTriggerMiddlewarePermission(
-                                    new SoundTriggerMiddlewareValidation(
-                                            new SoundTriggerMiddlewareImpl(factories,
-                                                    new AudioSessionProviderImpl())),
-                                    getContext())), getContext()));
+                    new SoundTriggerMiddlewareService(
+                            new SoundTriggerMiddlewareLogging(getContext(),
+                                new SoundTriggerMiddlewarePermission(
+                                        new SoundTriggerMiddlewareValidation(
+                                                new SoundTriggerMiddlewareImpl(factories,
+                                                        new AudioSessionProviderImpl())),
+                                        getContext())), getContext()));
         }
     }
 }
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 9456f0f..2e1477d 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -25,6 +25,7 @@
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT;
+import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_FINISH_ACTIVITY;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_LAUNCH_TASK;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_PENDING_INTENT;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_INSETS_PROVIDER;
@@ -1007,6 +1008,20 @@
                         isInLockTaskMode);
                 break;
             }
+            case HIERARCHY_OP_TYPE_FINISH_ACTIVITY: {
+                final ActivityRecord activity = ActivityRecord.forTokenLocked(hop.getContainer());
+                if (activity == null || activity.finishing) {
+                    break;
+                }
+                if (activity.isVisible()) {
+                    // Prevent the transition from being executed too early if the activity is
+                    // visible.
+                    activity.finishIfPossible("finish-activity-op", false /* oomAdj */);
+                } else {
+                    activity.destroyIfPossible("finish-activity-op");
+                }
+                break;
+            }
             case HIERARCHY_OP_TYPE_LAUNCH_TASK: {
                 mService.mAmInternal.enforceCallingPermission(START_TASKS_FROM_RECENTS,
                         "launchTask HierarchyOp");
@@ -1620,6 +1635,9 @@
                                 organizer);
                     }
                     break;
+                case HIERARCHY_OP_TYPE_FINISH_ACTIVITY:
+                    // Allow finish activity if it has the activity token.
+                    break;
                 default:
                     // Other types of hierarchy changes are not allowed.
                     String msg = "Permission Denial: " + func + " from pid="
diff --git a/services/tests/servicestests/src/com/android/server/logcat/LogcatManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/logcat/LogcatManagerServiceTest.java
index f330017..2cd5314 100644
--- a/services/tests/servicestests/src/com/android/server/logcat/LogcatManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/logcat/LogcatManagerServiceTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.logcat;
 
+import static android.os.Process.INVALID_UID;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
@@ -28,6 +30,7 @@
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
 import android.content.ContextWrapper;
+import android.content.pm.PackageManager;
 import android.os.ILogd;
 import android.os.Looper;
 import android.os.UserHandle;
@@ -69,10 +72,12 @@
     @Mock
     private ActivityManagerInternal mActivityManagerInternalMock;
     @Mock
+    private PackageManager mPackageManagerMock;
+    @Mock
     private ILogd mLogdMock;
 
     private LogcatManagerService mService;
-    private LogcatManagerService.LogcatManagerServiceInternal mLocalService;
+    private LogcatManagerService.LogAccessDialogCallback mDialogCallback;
     private ContextWrapper mContextSpy;
     private OffsettableClock mClock;
     private TestLooper mTestLooper;
@@ -81,10 +86,17 @@
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
         addLocalServiceMock(ActivityManagerInternal.class, mActivityManagerInternalMock);
+        when(mActivityManagerInternalMock.getInstrumentationSourceUid(anyInt()))
+                .thenReturn(INVALID_UID);
+
         mContextSpy = spy(new ContextWrapper(ApplicationProvider.getApplicationContext()));
         mClock = new OffsettableClock.Stopped();
         mTestLooper = new TestLooper(mClock::now);
-
+        when(mContextSpy.getPackageManager()).thenReturn(mPackageManagerMock);
+        when(mPackageManagerMock.getPackagesForUid(APP1_UID)).thenReturn(
+                new String[]{APP1_PACKAGE_NAME});
+        when(mPackageManagerMock.getPackagesForUid(APP2_UID)).thenReturn(
+                new String[]{APP2_PACKAGE_NAME});
         when(mActivityManagerInternalMock.getPackageNameByPid(APP1_PID)).thenReturn(
                 APP1_PACKAGE_NAME);
         when(mActivityManagerInternalMock.getPackageNameByPid(APP2_PID)).thenReturn(
@@ -106,7 +118,7 @@
                 return mLogdMock;
             }
         });
-        mLocalService = mService.getLocalService();
+        mDialogCallback = mService.getDialogCallback();
         mService.onStart();
     }
 
@@ -136,6 +148,20 @@
     }
 
     @Test
+    public void test_RequestFromBackground_ApprovedIfInstrumented() throws Exception {
+        when(mActivityManagerInternalMock.getInstrumentationSourceUid(APP1_UID))
+                .thenReturn(APP1_UID);
+        when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn(
+                ActivityManager.PROCESS_STATE_RECEIVER);
+        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
+        mTestLooper.dispatchAll();
+
+        verify(mLogdMock).approve(APP1_UID, APP1_GID, APP1_PID, FD1);
+        verify(mLogdMock, never()).decline(APP1_UID, APP1_GID, APP1_PID, FD1);
+        verify(mContextSpy, never()).startActivityAsUser(any(), any());
+    }
+
+    @Test
     public void test_RequestFromForegroundService_DeclinedWithoutPrompt() throws Exception {
         when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn(
                 ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
@@ -181,7 +207,7 @@
         mTestLooper.dispatchAll();
         verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM));
 
-        mLocalService.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
+        mDialogCallback.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
         mTestLooper.dispatchAll();
 
         verify(mLogdMock, times(1)).approve(APP1_UID, APP1_GID, APP1_PID, FD1);
@@ -196,7 +222,7 @@
         mTestLooper.dispatchAll();
         verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM));
 
-        mLocalService.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
+        mDialogCallback.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
         mTestLooper.dispatchAll();
 
         verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD1);
@@ -214,7 +240,7 @@
         verify(mLogdMock, never()).approve(eq(APP1_UID), eq(APP1_GID), eq(APP1_PID), anyInt());
         verify(mLogdMock, never()).decline(eq(APP1_UID), eq(APP1_GID), eq(APP1_PID), anyInt());
 
-        mLocalService.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
+        mDialogCallback.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
         mTestLooper.dispatchAll();
 
         verify(mLogdMock, times(1)).approve(APP1_UID, APP1_GID, APP1_PID, FD1);
@@ -234,7 +260,7 @@
         verify(mLogdMock, never()).approve(eq(APP1_UID), eq(APP1_GID), eq(APP1_PID), anyInt());
         verify(mLogdMock, never()).decline(eq(APP1_UID), eq(APP1_GID), eq(APP1_PID), anyInt());
 
-        mLocalService.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
+        mDialogCallback.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
         mTestLooper.dispatchAll();
 
         verify(mLogdMock, times(1)).decline(APP1_UID, APP1_GID, APP1_PID, FD1);
@@ -249,7 +275,7 @@
                 ActivityManager.PROCESS_STATE_TOP);
         mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
         mTestLooper.dispatchAll();
-        mLocalService.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
+        mDialogCallback.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
         mTestLooper.dispatchAll();
 
         mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD2);
@@ -267,7 +293,7 @@
                 ActivityManager.PROCESS_STATE_TOP);
         mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
         mTestLooper.dispatchAll();
-        mLocalService.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
+        mDialogCallback.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
         mTestLooper.dispatchAll();
 
         mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD2);
@@ -287,7 +313,7 @@
                 ActivityManager.PROCESS_STATE_TOP);
         mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
         mTestLooper.dispatchAll();
-        mLocalService.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
+        mDialogCallback.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
         mTestLooper.dispatchAll();
 
         mService.getBinderService().startThread(APP2_UID, APP2_GID, APP2_PID, FD2);
@@ -304,7 +330,7 @@
                 ActivityManager.PROCESS_STATE_TOP);
         mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
         mTestLooper.dispatchAll();
-        mLocalService.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
+        mDialogCallback.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
         mTestLooper.dispatchAll();
 
         advanceTime(LogcatManagerService.STATUS_EXPIRATION_TIMEOUT_MILLIS);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index 61cf8cc..1404de2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -730,6 +730,16 @@
     }
 
     @Test
+    public void testApplyTransaction_finishActivity() {
+        final ActivityRecord activity = createActivityRecord(mDisplayContent);
+
+        mTransaction.finishActivity(activity.token);
+        assertApplyTransactionAllowed(mTransaction);
+
+        assertTrue(activity.finishing);
+    }
+
+    @Test
     public void testApplyTransaction_skipTransactionForUnregisterOrganizer() {
         mController.unregisterOrganizer(mIOrganizer);
         final ActivityRecord ownerActivity = createActivityRecord(mDisplayContent);
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
index 0ce0265..3da4711 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
@@ -95,6 +95,7 @@
 import com.android.internal.content.PackageMonitor;
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.DumpUtils;
+import com.android.internal.util.LatencyTracker;
 import com.android.server.FgThread;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
@@ -191,6 +192,8 @@
             mSoundTriggerInternal = LocalServices.getService(SoundTriggerInternal.class);
         } else if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) {
             mServiceStub.systemRunning(isSafeMode());
+        } else if (phase == PHASE_BOOT_COMPLETED) {
+            mServiceStub.registerVoiceInteractionSessionListener(mLatencyLoggingListener);
         }
     }
 
@@ -2334,4 +2337,36 @@
             }
         };
     }
+
+    /**
+     * End the latency tracking log for keyphrase hotword invocation.
+     * The measurement covers from when the SoundTrigger HAL emits an event, captured in
+     * {@link com.android.server.soundtrigger_middleware.SoundTriggerMiddlewareLogging}
+     * to when the {@link android.service.voice.VoiceInteractionSession} system UI view is shown.
+     */
+    private final IVoiceInteractionSessionListener mLatencyLoggingListener =
+            new IVoiceInteractionSessionListener.Stub() {
+                @Override
+                public void onVoiceSessionShown() throws RemoteException {}
+
+                @Override
+                public void onVoiceSessionHidden() throws RemoteException {}
+
+                @Override
+                public void onVoiceSessionWindowVisibilityChanged(boolean visible)
+                        throws RemoteException {
+                    if (visible) {
+                        LatencyTracker.getInstance(mContext)
+                                .onActionEnd(LatencyTracker.ACTION_SHOW_VOICE_INTERACTION);
+                    }
+                }
+
+                @Override
+                public void onSetUiHints(Bundle args) throws RemoteException {}
+
+                @Override
+                public IBinder asBinder() {
+                    return mServiceStub;
+                }
+            };
 }