Merge "Remove pinned tasks when starting locked task w/ intent" into udc-qpr-dev
diff --git a/core/java/android/credentials/CredentialManager.java b/core/java/android/credentials/CredentialManager.java
index c2a0062..eedb25b 100644
--- a/core/java/android/credentials/CredentialManager.java
+++ b/core/java/android/credentials/CredentialManager.java
@@ -123,7 +123,7 @@
      * credential, display a picker when multiple credentials exist, etc.
      * Callers (e.g. browsers) may optionally set origin in {@link GetCredentialRequest} for an
      * app different from their own, to be able to get credentials on behalf of that app. They would
-     * need additional permission {@link CREDENTIAL_MANAGER_SET_ORIGIN}
+     * need additional permission {@code CREDENTIAL_MANAGER_SET_ORIGIN}
      * to use this functionality
      *
      * @param context the context used to launch any UI needed; use an activity context to make sure
@@ -209,9 +209,9 @@
      *
      * <p>This API doesn't invoke any UI. It only performs the preparation work so that you can
      * later launch the remaining get-credential operation (involves UIs) through the {@link
-     * #getCredential(PrepareGetCredentialResponse.PendingGetCredentialHandle, Context,
+     * #getCredential(Context, PrepareGetCredentialResponse.PendingGetCredentialHandle,
      * CancellationSignal, Executor, OutcomeReceiver)} API which incurs less latency compared to
-     * the {@link #getCredential(GetCredentialRequest, Context, CancellationSignal, Executor,
+     * the {@link #getCredential(Context, GetCredentialRequest, CancellationSignal, Executor,
      * OutcomeReceiver)} API that executes the whole operation in one call.
      *
      * @param request            the request specifying type(s) of credentials to get from the user
@@ -261,7 +261,7 @@
      * storing the new credential, etc.
      * Callers (e.g. browsers) may optionally set origin in {@link CreateCredentialRequest} for an
      * app different from their own, to be able to get credentials on behalf of that app. They would
-     * need additional permission {@link CREDENTIAL_MANAGER_SET_ORIGIN}
+     * need additional permission {@code CREDENTIAL_MANAGER_SET_ORIGIN}
      * to use this functionality
      *
      * @param context the context used to launch any UI needed; use an activity context to make sure
diff --git a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
index 6baf91d7..ea951a5 100644
--- a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
@@ -236,9 +236,10 @@
         private static final CameraExtensionManagerGlobal GLOBAL_CAMERA_MANAGER =
                 new CameraExtensionManagerGlobal();
         private final Object mLock = new Object();
-        private final int PROXY_SERVICE_DELAY_MS = 1000;
+        private final int PROXY_SERVICE_DELAY_MS = 2000;
         private InitializerFuture mInitFuture = null;
         private ServiceConnection mConnection = null;
+        private int mConnectionCount = 0;
         private ICameraExtensionsProxyService mProxy = null;
         private boolean mSupportsAdvancedExtensions = false;
 
@@ -249,6 +250,15 @@
             return GLOBAL_CAMERA_MANAGER;
         }
 
+        private void releaseProxyConnectionLocked(Context ctx) {
+            if (mConnection != null ) {
+                ctx.unbindService(mConnection);
+                mConnection = null;
+                mProxy = null;
+                mConnectionCount = 0;
+            }
+        }
+
         private void connectToProxyLocked(Context ctx) {
             if (mConnection == null) {
                 Intent intent = new Intent();
@@ -270,7 +280,6 @@
                 mConnection = new ServiceConnection() {
                     @Override
                     public void onServiceDisconnected(ComponentName component) {
-                        mInitFuture.setStatus(false);
                         mConnection = null;
                         mProxy = null;
                     }
@@ -348,23 +357,32 @@
 
         public boolean registerClient(Context ctx, IBinder token) {
             synchronized (mLock) {
+                boolean ret = false;
                 connectToProxyLocked(ctx);
                 if (mProxy == null) {
                     return false;
                 }
+                mConnectionCount++;
 
                 try {
-                    return mProxy.registerClient(token);
+                    ret = mProxy.registerClient(token);
                 } catch (RemoteException e) {
                     Log.e(TAG, "Failed to initialize extension! Extension service does "
                             + " not respond!");
                 }
+                if (!ret) {
+                    mConnectionCount--;
+                }
 
-                return false;
+                if (mConnectionCount <= 0) {
+                    releaseProxyConnectionLocked(ctx);
+                }
+
+                return ret;
             }
         }
 
-        public void unregisterClient(IBinder token) {
+        public void unregisterClient(Context ctx, IBinder token) {
             synchronized (mLock) {
                 if (mProxy != null) {
                     try {
@@ -372,6 +390,11 @@
                     } catch (RemoteException e) {
                         Log.e(TAG, "Failed to de-initialize extension! Extension service does"
                                 + " not respond!");
+                    } finally {
+                        mConnectionCount--;
+                        if (mConnectionCount <= 0) {
+                            releaseProxyConnectionLocked(ctx);
+                        }
                     }
                 }
             }
@@ -446,8 +469,8 @@
     /**
      * @hide
      */
-    public static void unregisterClient(IBinder token) {
-        CameraExtensionManagerGlobal.get().unregisterClient(token);
+    public static void unregisterClient(Context ctx, IBinder token) {
+        CameraExtensionManagerGlobal.get().unregisterClient(ctx, token);
     }
 
     /**
@@ -578,7 +601,7 @@
                 }
             }
         } finally {
-            unregisterClient(token);
+            unregisterClient(mContext, token);
         }
 
         return Collections.unmodifiableList(ret);
@@ -626,7 +649,7 @@
             Log.e(TAG, "Failed to query the extension for postview availability! Extension "
                     + "service does not respond!");
         } finally {
-            unregisterClient(token);
+            unregisterClient(mContext, token);
         }
 
         return false;
@@ -722,7 +745,7 @@
                     + "service does not respond!");
             return Collections.emptyList();
         } finally {
-            unregisterClient(token);
+            unregisterClient(mContext, token);
         }
     }
 
@@ -791,7 +814,7 @@
                     + " not respond!");
             return new ArrayList<>();
         } finally {
-            unregisterClient(token);
+            unregisterClient(mContext, token);
         }
     }
 
@@ -872,7 +895,7 @@
                     }
                 }
             } finally {
-                unregisterClient(token);
+                unregisterClient(mContext, token);
             }
         } catch (RemoteException e) {
             Log.e(TAG, "Failed to query the extension supported sizes! Extension service does"
@@ -957,7 +980,7 @@
             Log.e(TAG, "Failed to query the extension capture latency! Extension service does"
                     + " not respond!");
         } finally {
-            unregisterClient(token);
+            unregisterClient(mContext, token);
         }
 
         return null;
@@ -998,7 +1021,7 @@
             Log.e(TAG, "Failed to query the extension progress callbacks! Extension service does"
                     + " not respond!");
         } finally {
-            unregisterClient(token);
+            unregisterClient(mContext, token);
         }
 
         return false;
@@ -1075,7 +1098,7 @@
         } catch (RemoteException e) {
             throw new IllegalStateException("Failed to query the available capture request keys!");
         } finally {
-            unregisterClient(token);
+            unregisterClient(mContext, token);
         }
 
         return Collections.unmodifiableSet(ret);
@@ -1155,7 +1178,7 @@
         } catch (RemoteException e) {
             throw new IllegalStateException("Failed to query the available capture result keys!");
         } finally {
-            unregisterClient(token);
+            unregisterClient(mContext, token);
         }
 
         return Collections.unmodifiableSet(ret);
diff --git a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
index ae700a0..f0fb2f9 100644
--- a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
@@ -90,7 +90,7 @@
     private final HashMap<Integer, ImageReader> mReaderMap = new HashMap<>();
     private RequestProcessor mRequestProcessor = new RequestProcessor();
     private final int mSessionId;
-    private final IBinder mToken;
+    private IBinder mToken = null;
 
     private Surface mClientRepeatingRequestSurface;
     private Surface mClientCaptureSurface;
@@ -103,6 +103,8 @@
     private boolean mInitialized;
     private boolean mSessionClosed;
 
+    private final Context mContext;
+
     // Lock to synchronize cross-thread access to device public interface
     final Object mInterfaceLock;
 
@@ -113,14 +115,9 @@
     public static CameraAdvancedExtensionSessionImpl createCameraAdvancedExtensionSession(
             @NonNull android.hardware.camera2.impl.CameraDeviceImpl cameraDevice,
             @NonNull Map<String, CameraCharacteristics> characteristicsMap,
-            @NonNull Context ctx, @NonNull ExtensionSessionConfiguration config, int sessionId)
+            @NonNull Context ctx, @NonNull ExtensionSessionConfiguration config, int sessionId,
+            @NonNull IBinder token)
             throws CameraAccessException, RemoteException {
-        final IBinder token = new Binder(TAG + " : " + sessionId);
-        boolean success = CameraExtensionCharacteristics.registerClient(ctx, token);
-        if (!success) {
-            throw new UnsupportedOperationException("Unsupported extension!");
-        }
-
         String cameraId = cameraDevice.getId();
         CameraExtensionCharacteristics extensionChars = new CameraExtensionCharacteristics(ctx,
                 cameraId, characteristicsMap);
@@ -204,8 +201,9 @@
         IAdvancedExtenderImpl extender = CameraExtensionCharacteristics.initializeAdvancedExtension(
                 config.getExtension());
         extender.init(cameraId, characteristicsMapNative);
-        CameraAdvancedExtensionSessionImpl ret = new CameraAdvancedExtensionSessionImpl(extender,
-                cameraDevice, characteristicsMapNative, repeatingRequestSurface,
+
+        CameraAdvancedExtensionSessionImpl ret = new CameraAdvancedExtensionSessionImpl(ctx,
+                extender, cameraDevice, characteristicsMapNative, repeatingRequestSurface,
                 burstCaptureSurface, postviewSurface, config.getStateCallback(),
                 config.getExecutor(), sessionId, token);
 
@@ -217,13 +215,16 @@
         return ret;
     }
 
-    private CameraAdvancedExtensionSessionImpl(@NonNull IAdvancedExtenderImpl extender,
+    private CameraAdvancedExtensionSessionImpl(Context ctx,
+            @NonNull IAdvancedExtenderImpl extender,
             @NonNull CameraDeviceImpl cameraDevice,
             Map<String, CameraMetadataNative> characteristicsMap,
             @Nullable Surface repeatingRequestSurface, @Nullable Surface burstCaptureSurface,
             @Nullable Surface postviewSurface,
             @NonNull StateCallback callback, @NonNull Executor executor,
-            int sessionId, @NonNull IBinder token) {
+            int sessionId,
+            @NonNull IBinder token) {
+        mContext = ctx;
         mAdvancedExtender = extender;
         mCameraDevice = cameraDevice;
         mCharacteristicsMap = characteristicsMap;
@@ -578,12 +579,16 @@
                 mSessionProcessor = null;
             }
 
-            CameraExtensionCharacteristics.unregisterClient(mToken);
-            if (mInitialized || (mCaptureSession != null)) {
-                notifyClose = true;
-                CameraExtensionCharacteristics.releaseSession();
+
+            if (mToken != null) {
+                if (mInitialized || (mCaptureSession != null)) {
+                    notifyClose = true;
+                    CameraExtensionCharacteristics.releaseSession();
+                }
+                CameraExtensionCharacteristics.unregisterClient(mContext, mToken);
             }
             mInitialized = false;
+            mToken = null;
 
             for (ImageReader reader : mReaderMap.values()) {
                 reader.close();
diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
index d3bde4b..181ab2c 100644
--- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
@@ -2550,19 +2550,32 @@
         HashMap<String, CameraCharacteristics> characteristicsMap = new HashMap<>(
                 mPhysicalIdsToChars);
         characteristicsMap.put(mCameraId, mCharacteristics);
+        boolean initializationFailed = true;
+        IBinder token = new Binder(TAG + " : " + mNextSessionId++);
         try {
+            boolean ret = CameraExtensionCharacteristics.registerClient(mContext, token);
+            if (!ret) {
+                token = null;
+                throw new UnsupportedOperationException("Unsupported extension!");
+            }
+
             if (CameraExtensionCharacteristics.areAdvancedExtensionsSupported()) {
                 mCurrentAdvancedExtensionSession =
                         CameraAdvancedExtensionSessionImpl.createCameraAdvancedExtensionSession(
                                 this, characteristicsMap, mContext, extensionConfiguration,
-                                mNextSessionId++);
+                                mNextSessionId, token);
             } else {
                 mCurrentExtensionSession = CameraExtensionSessionImpl.createCameraExtensionSession(
                         this, characteristicsMap, mContext, extensionConfiguration,
-                        mNextSessionId++);
+                        mNextSessionId, token);
             }
+            initializationFailed = false;
         } catch (RemoteException e) {
             throw new CameraAccessException(CameraAccessException.CAMERA_ERROR);
+        } finally {
+            if (initializationFailed && (token != null)) {
+                CameraExtensionCharacteristics.unregisterClient(mContext, token);
+            }
         }
     }
 }
diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
index 1db4808..7b7d7de 100644
--- a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
@@ -91,7 +91,7 @@
     private final Set<CaptureRequest.Key> mSupportedRequestKeys;
     private final Set<CaptureResult.Key> mSupportedResultKeys;
     private final ExtensionSessionStatsAggregator mStatsAggregator;
-    private final IBinder mToken;
+    private IBinder mToken = null;
     private boolean mCaptureResultsSupported;
 
     private CameraCaptureSession mCaptureSession = null;
@@ -119,6 +119,8 @@
     // will do so internally.
     private boolean mInternalRepeatingRequestEnabled = true;
 
+    private final Context mContext;
+
     // Lock to synchronize cross-thread access to device public interface
     final Object mInterfaceLock;
 
@@ -135,14 +137,9 @@
             @NonNull Map<String, CameraCharacteristics> characteristicsMap,
             @NonNull Context ctx,
             @NonNull ExtensionSessionConfiguration config,
-            int sessionId)
+            int sessionId,
+            @NonNull IBinder token)
             throws CameraAccessException, RemoteException {
-        final IBinder token = new Binder(TAG + " : " + sessionId);
-        boolean success = CameraExtensionCharacteristics.registerClient(ctx, token);
-        if (!success) {
-            throw new UnsupportedOperationException("Unsupported extension!");
-        }
-
         String cameraId = cameraDevice.getId();
         CameraExtensionCharacteristics extensionChars = new CameraExtensionCharacteristics(ctx,
                 cameraId, characteristicsMap);
@@ -234,6 +231,7 @@
                 characteristicsMap.get(cameraId).getNativeMetadata());
 
         CameraExtensionSessionImpl session = new CameraExtensionSessionImpl(
+                ctx,
                 extenders.second,
                 extenders.first,
                 supportedPreviewSizes,
@@ -256,7 +254,7 @@
         return session;
     }
 
-    public CameraExtensionSessionImpl(@NonNull IImageCaptureExtenderImpl imageExtender,
+    public CameraExtensionSessionImpl(Context ctx, @NonNull IImageCaptureExtenderImpl imageExtender,
             @NonNull IPreviewExtenderImpl previewExtender,
             @NonNull List<Size> previewSizes,
             @NonNull android.hardware.camera2.impl.CameraDeviceImpl cameraDevice,
@@ -269,6 +267,7 @@
             @NonNull IBinder token,
             @NonNull Set<CaptureRequest.Key> requestKeys,
             @Nullable Set<CaptureResult.Key> resultKeys) {
+        mContext = ctx;
         mImageExtender = imageExtender;
         mPreviewExtender = previewExtender;
         mCameraDevice = cameraDevice;
@@ -878,12 +877,15 @@
                         + " respond!");
             }
 
-            CameraExtensionCharacteristics.unregisterClient(mToken);
-            if (mInitialized || (mCaptureSession != null)) {
-                notifyClose = true;
-                CameraExtensionCharacteristics.releaseSession();
+            if (mToken != null) {
+                if (mInitialized || (mCaptureSession != null)) {
+                    notifyClose = true;
+                    CameraExtensionCharacteristics.releaseSession();
+                }
+                CameraExtensionCharacteristics.unregisterClient(mContext, mToken);
             }
             mInitialized = false;
+            mToken = null;
 
             if (mRepeatingRequestImageCallback != null) {
                 mRepeatingRequestImageCallback.close();
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index 94bff89..4700720 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -370,8 +370,9 @@
 
     /**
      * Returns the default size of the surface associated with the display, or null if the surface
-     * is not provided for layer mirroring by SurfaceFlinger.
-     * Only used for mirroring started from MediaProjection.
+     * is not provided for layer mirroring by SurfaceFlinger. Size is rotated to reflect the current
+     * display device orientation.
+     * Used for mirroring from MediaProjection, or a physical display based on display flags.
      */
     public abstract Point getDisplaySurfaceDefaultSize(int displayId);
 
diff --git a/core/java/android/service/notification/NotificationRankingUpdate.java b/core/java/android/service/notification/NotificationRankingUpdate.java
index 75640bd..f3b4c6d 100644
--- a/core/java/android/service/notification/NotificationRankingUpdate.java
+++ b/core/java/android/service/notification/NotificationRankingUpdate.java
@@ -92,6 +92,7 @@
                 mapParcel.recycle();
                 if (buffer != null) {
                     mRankingMapFd.unmap(buffer);
+                    mRankingMapFd.close();
                 }
             }
         } else {
diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
index 708ebdf..a4989af 100644
--- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java
+++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
@@ -67,6 +67,7 @@
 import com.android.internal.app.IHotwordRecognitionStatusCallback;
 import com.android.internal.app.IVoiceInteractionManagerService;
 import com.android.internal.app.IVoiceInteractionSoundTriggerSession;
+import com.android.internal.infra.AndroidFuture;
 
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
@@ -1702,6 +1703,11 @@
             Slog.i(TAG, "onProcessRestarted");
             mHandler.sendEmptyMessage(MSG_PROCESS_RESTARTED);
         }
+
+        @Override
+        public void onOpenFile(String filename, AndroidFuture future) throws RemoteException {
+            throw new UnsupportedOperationException("Hotword cannot access files from the disk.");
+        }
     }
 
     void onDetectorRemoteException() {
diff --git a/core/java/android/service/voice/HotwordDetectionService.java b/core/java/android/service/voice/HotwordDetectionService.java
index d9ee859..ccf8b67 100644
--- a/core/java/android/service/voice/HotwordDetectionService.java
+++ b/core/java/android/service/voice/HotwordDetectionService.java
@@ -227,6 +227,12 @@
         public void stopDetection() {
             HotwordDetectionService.this.onStopDetection();
         }
+
+        @Override
+        public void registerRemoteStorageService(IDetectorSessionStorageService
+                detectorSessionStorageService) {
+            throw new UnsupportedOperationException("Hotword cannot access files from the disk.");
+        }
     };
 
     @Override
diff --git a/core/java/android/service/voice/IDetectorSessionStorageService.aidl b/core/java/android/service/voice/IDetectorSessionStorageService.aidl
new file mode 100644
index 0000000..592373e
--- /dev/null
+++ b/core/java/android/service/voice/IDetectorSessionStorageService.aidl
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.voice;
+
+import com.android.internal.infra.AndroidFuture;
+
+/**
+ * @hide
+ */
+oneway interface IDetectorSessionStorageService {
+    /**
+     * Called when a file open request is sent. Only files with the given names under the internal
+     * app storage, i.e., {@link Context#getFilesDir()} can be opened.
+     */
+    void openFile(in String filename, in AndroidFuture future);
+}
diff --git a/core/java/android/service/voice/ISandboxedDetectionService.aidl b/core/java/android/service/voice/ISandboxedDetectionService.aidl
index 098536d..c76ac28 100644
--- a/core/java/android/service/voice/ISandboxedDetectionService.aidl
+++ b/core/java/android/service/voice/ISandboxedDetectionService.aidl
@@ -24,6 +24,7 @@
 import android.os.ParcelFileDescriptor;
 import android.os.PersistableBundle;
 import android.os.SharedMemory;
+import android.service.voice.IDetectorSessionStorageService;
 import android.service.voice.IDetectorSessionVisualQueryDetectionCallback;
 import android.service.voice.IDspHotwordDetectionCallback;
 import android.view.contentcapture.IContentCaptureManager;
@@ -71,4 +72,10 @@
     void ping(in IRemoteCallback callback);
 
     void stopDetection();
+
+    /**
+     * Registers the interface stub to talk to the voice interaction service for initialization/
+     * detection unrelated functionalities.
+     */
+    void registerRemoteStorageService(in IDetectorSessionStorageService detectorSessionStorageService);
 }
diff --git a/core/java/android/service/voice/SoftwareHotwordDetector.java b/core/java/android/service/voice/SoftwareHotwordDetector.java
index 7ab4faf..88c2111 100644
--- a/core/java/android/service/voice/SoftwareHotwordDetector.java
+++ b/core/java/android/service/voice/SoftwareHotwordDetector.java
@@ -36,6 +36,7 @@
 
 import com.android.internal.app.IHotwordRecognitionStatusCallback;
 import com.android.internal.app.IVoiceInteractionManagerService;
+import com.android.internal.infra.AndroidFuture;
 
 import java.io.PrintWriter;
 import java.util.concurrent.Executor;
@@ -299,6 +300,11 @@
             Binder.withCleanCallingIdentity(() -> mExecutor.execute(
                     () -> mCallback.onHotwordDetectionServiceRestarted()));
         }
+
+        @Override
+        public void onOpenFile(String filename, AndroidFuture future) throws RemoteException {
+            throw new UnsupportedOperationException("Hotword cannot access files from the disk.");
+        }
     }
 
     /** @hide */
diff --git a/core/java/android/service/voice/VisualQueryDetectionService.java b/core/java/android/service/voice/VisualQueryDetectionService.java
index cbe7666..d184b1e 100644
--- a/core/java/android/service/voice/VisualQueryDetectionService.java
+++ b/core/java/android/service/voice/VisualQueryDetectionService.java
@@ -40,7 +40,12 @@
 import android.view.contentcapture.ContentCaptureManager;
 import android.view.contentcapture.IContentCaptureManager;
 
+import com.android.internal.infra.AndroidFuture;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
 import java.util.Objects;
+import java.util.concurrent.ExecutionException;
 import java.util.function.IntConsumer;
 
 /**
@@ -86,6 +91,8 @@
     private ContentCaptureManager mContentCaptureManager;
     @Nullable
     private IRecognitionServiceManager mIRecognitionServiceManager;
+    @Nullable
+    private IDetectorSessionStorageService mDetectorSessionStorageService;
 
 
     private final ISandboxedDetectionService mInterface = new ISandboxedDetectionService.Stub() {
@@ -154,6 +161,12 @@
         public void updateRecognitionServiceManager(IRecognitionServiceManager manager) {
             mIRecognitionServiceManager = manager;
         }
+
+        @Override
+        public void registerRemoteStorageService(IDetectorSessionStorageService
+                detectorSessionStorageService) {
+            mDetectorSessionStorageService = detectorSessionStorageService;
+        }
     };
 
     @Override
@@ -323,4 +336,23 @@
         }
     }
 
+    /**
+     * Overrides {@link Context#openFileInput} to read files with the given file names under the
+     * internal app storage of the {@link VoiceInteractionService}, i.e., only files stored in
+     * {@link Context#getFilesDir()} can be opened.
+     */
+    @Override
+    public @Nullable FileInputStream openFileInput(@NonNull String filename) throws
+            FileNotFoundException {
+        try {
+            AndroidFuture<ParcelFileDescriptor> future = new AndroidFuture<>();
+            mDetectorSessionStorageService.openFile(filename, future);
+            ParcelFileDescriptor pfd = future.get();
+            return new FileInputStream(pfd.getFileDescriptor());
+        } catch (RemoteException | ExecutionException | InterruptedException e) {
+            Log.w(TAG, "Cannot open file due to remote service failure");
+            throw new FileNotFoundException(e.getMessage());
+        }
+    }
+
 }
diff --git a/core/java/android/service/voice/VisualQueryDetector.java b/core/java/android/service/voice/VisualQueryDetector.java
index 93b7964..5d7f478 100644
--- a/core/java/android/service/voice/VisualQueryDetector.java
+++ b/core/java/android/service/voice/VisualQueryDetector.java
@@ -25,6 +25,7 @@
 import android.annotation.RequiresPermission;
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
+import android.content.Context;
 import android.hardware.soundtrigger.SoundTrigger;
 import android.media.AudioFormat;
 import android.os.Binder;
@@ -37,7 +38,10 @@
 
 import com.android.internal.app.IHotwordRecognitionStatusCallback;
 import com.android.internal.app.IVoiceInteractionManagerService;
+import com.android.internal.infra.AndroidFuture;
 
+import java.io.File;
+import java.io.FileNotFoundException;
 import java.io.PrintWriter;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
@@ -58,17 +62,18 @@
 
     private final Callback mCallback;
     private final Executor mExecutor;
+    private final Context mContext;
     private final IVoiceInteractionManagerService mManagerService;
     private final VisualQueryDetectorInitializationDelegate mInitializationDelegate;
 
     VisualQueryDetector(
             IVoiceInteractionManagerService managerService,
-            @NonNull @CallbackExecutor Executor executor,
-            Callback callback) {
+            @NonNull @CallbackExecutor Executor executor, Callback callback, Context context) {
         mManagerService = managerService;
         mCallback = callback;
         mExecutor = executor;
         mInitializationDelegate = new VisualQueryDetectorInitializationDelegate();
+        mContext = context;
     }
 
     /**
@@ -245,7 +250,7 @@
         @Override
         void initialize(@Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory) {
             initAndVerifyDetector(options, sharedMemory,
-                    new InitializationStateListener(mExecutor, mCallback),
+                    new InitializationStateListener(mExecutor, mCallback, mContext),
                     DETECTOR_TYPE_VISUAL_QUERY_DETECTOR);
         }
 
@@ -330,9 +335,12 @@
         private final Executor mExecutor;
         private final Callback mCallback;
 
-        InitializationStateListener(Executor executor, Callback callback) {
+        private final Context mContext;
+
+        InitializationStateListener(Executor executor, Callback callback, Context context) {
             this.mExecutor = executor;
             this.mCallback = callback;
+            this.mContext = context;
         }
 
         @Override
@@ -426,5 +434,22 @@
                         !TextUtils.isEmpty(errorMessage) ? errorMessage : "Error data is null");
             }));
         }
+        @Override
+        public void onOpenFile(String filename, AndroidFuture future) throws RemoteException {
+            Slog.v(TAG, "BinderCallback#onOpenFile " + filename);
+            Binder.withCleanCallingIdentity(() -> mExecutor.execute(() -> {
+                Slog.v(TAG, "onOpenFile: " + filename);
+                File f = new File(mContext.getFilesDir(), filename);
+                ParcelFileDescriptor pfd = null;
+                try {
+                    Slog.d(TAG, "opened a file with ParcelFileDescriptor.");
+                    pfd = ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY);
+                } catch (FileNotFoundException e) {
+                    Slog.e(TAG, "Cannot open file. No ParcelFileDescriptor returned.");
+                } finally {
+                    future.complete(pfd);
+                }
+            }));
+        }
     }
 }
diff --git a/core/java/android/service/voice/VoiceInteractionService.java b/core/java/android/service/voice/VoiceInteractionService.java
index ab9ae0a..de2a99b 100644
--- a/core/java/android/service/voice/VoiceInteractionService.java
+++ b/core/java/android/service/voice/VoiceInteractionService.java
@@ -965,7 +965,7 @@
             }
 
             VisualQueryDetector visualQueryDetector =
-                    new VisualQueryDetector(mSystemService, executor, callback);
+                    new VisualQueryDetector(mSystemService, executor, callback, this);
             HotwordDetector visualQueryDetectorInitializationDelegate =
                     visualQueryDetector.getInitializationDelegate();
             mActiveDetectors.add(visualQueryDetectorInitializationDelegate);
diff --git a/core/java/android/speech/RecognitionService.java b/core/java/android/speech/RecognitionService.java
index 9656f36..7f313c1 100644
--- a/core/java/android/speech/RecognitionService.java
+++ b/core/java/android/speech/RecognitionService.java
@@ -38,6 +38,7 @@
 import android.os.RemoteException;
 import android.util.Log;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.function.pooled.PooledLambda;
 
 import java.lang.ref.WeakReference;
@@ -232,39 +233,68 @@
                     intent,
                     attributionSource,
                     new ModelDownloadListener() {
+
+                        private final Object mLock = new Object();
+
+                        @GuardedBy("mLock")
+                        private boolean mIsTerminated = false;
+
                         @Override
                         public void onProgress(int completedPercent) {
-                            try {
-                                listener.onProgress(completedPercent);
-                            } catch (RemoteException e) {
-                                throw e.rethrowFromSystemServer();
+                            synchronized (mLock) {
+                                if (mIsTerminated) {
+                                    return;
+                                }
+                                try {
+                                    listener.onProgress(completedPercent);
+                                } catch (RemoteException e) {
+                                    throw e.rethrowFromSystemServer();
+                                }
                             }
                         }
 
                         @Override
                         public void onSuccess() {
-                            try {
-                                listener.onSuccess();
-                            } catch (RemoteException e) {
-                                throw e.rethrowFromSystemServer();
+                            synchronized (mLock) {
+                                if (mIsTerminated) {
+                                    return;
+                                }
+                                mIsTerminated = true;
+                                try {
+                                    listener.onSuccess();
+                                } catch (RemoteException e) {
+                                    throw e.rethrowFromSystemServer();
+                                }
                             }
                         }
 
                         @Override
                         public void onScheduled() {
-                            try {
-                                listener.onScheduled();
-                            } catch (RemoteException e) {
-                                throw e.rethrowFromSystemServer();
+                            synchronized (mLock) {
+                                if (mIsTerminated) {
+                                    return;
+                                }
+                                mIsTerminated = true;
+                                try {
+                                    listener.onScheduled();
+                                } catch (RemoteException e) {
+                                    throw e.rethrowFromSystemServer();
+                                }
                             }
                         }
 
                         @Override
                         public void onError(int error) {
-                            try {
-                                listener.onError(error);
-                            } catch (RemoteException e) {
-                                throw e.rethrowFromSystemServer();
+                            synchronized (mLock) {
+                                if (mIsTerminated) {
+                                    return;
+                                }
+                                mIsTerminated = true;
+                                try {
+                                    listener.onError(error);
+                                } catch (RemoteException e) {
+                                    throw e.rethrowFromSystemServer();
+                                }
                             }
                         }
                     });
diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java
index 4cfec99..9b34007 100644
--- a/core/java/android/view/HandwritingInitiator.java
+++ b/core/java/android/view/HandwritingInitiator.java
@@ -19,7 +19,10 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
+import android.graphics.Matrix;
 import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.TextView;
 
@@ -78,11 +81,17 @@
     private int mConnectionCount = 0;
     private final InputMethodManager mImm;
 
+    private final RectF mTempRectF = new RectF();
+
+    private final Region mTempRegion = new Region();
+
+    private final Matrix mTempMatrix = new Matrix();
+
     /**
      * The handwrite-able View that is currently the target of a hovering stylus pointer. This is
      * used to help determine whether the handwriting PointerIcon should be shown in
      * {@link #onResolvePointerIcon(Context, MotionEvent)} so that we can reduce the number of calls
-     * to {@link #findBestCandidateView(float, float)}.
+     * to {@link #findBestCandidateView(float, float, boolean)}.
      */
     @Nullable
     private WeakReference<View> mCachedHoverTarget = null;
@@ -189,8 +198,8 @@
                 final float y = motionEvent.getY(pointerIndex);
                 if (largerThanTouchSlop(x, y, mState.mStylusDownX, mState.mStylusDownY)) {
                     mState.mExceedHandwritingSlop = true;
-                    View candidateView =
-                            findBestCandidateView(mState.mStylusDownX, mState.mStylusDownY);
+                    View candidateView = findBestCandidateView(mState.mStylusDownX,
+                            mState.mStylusDownY, /* isHover */ false);
                     if (candidateView != null) {
                         if (candidateView == getConnectedView()) {
                             if (!candidateView.hasFocus()) {
@@ -398,13 +407,14 @@
             final View cachedHoverTarget = getCachedHoverTarget();
             if (cachedHoverTarget != null) {
                 final Rect handwritingArea = getViewHandwritingArea(cachedHoverTarget);
-                if (isInHandwritingArea(handwritingArea, hoverX, hoverY, cachedHoverTarget)
+                if (isInHandwritingArea(handwritingArea, hoverX, hoverY, cachedHoverTarget,
+                        /* isHover */ true)
                         && shouldTriggerStylusHandwritingForView(cachedHoverTarget)) {
                     return cachedHoverTarget;
                 }
             }
 
-            final View candidateView = findBestCandidateView(hoverX, hoverY);
+            final View candidateView = findBestCandidateView(hoverX, hoverY, /* isHover */ true);
 
             if (candidateView != null) {
                 mCachedHoverTarget = new WeakReference<>(candidateView);
@@ -434,14 +444,14 @@
      * @param y the y coordinates of the stylus event, in the coordinates of the window.
      */
     @Nullable
-    private View findBestCandidateView(float x, float y) {
+    private View findBestCandidateView(float x, float y, boolean isHover) {
         // If the connectedView is not null and do not set any handwriting area, it will check
         // whether the connectedView's boundary contains the initial stylus position. If true,
         // directly return the connectedView.
         final View connectedView = getConnectedView();
         if (connectedView != null) {
             Rect handwritingArea = getViewHandwritingArea(connectedView);
-            if (isInHandwritingArea(handwritingArea, x, y, connectedView)
+            if (isInHandwritingArea(handwritingArea, x, y, connectedView, isHover)
                     && shouldTriggerStylusHandwritingForView(connectedView)) {
                 return connectedView;
             }
@@ -455,7 +465,7 @@
         for (HandwritableViewInfo viewInfo : handwritableViewInfos) {
             final View view = viewInfo.getView();
             final Rect handwritingArea = viewInfo.getHandwritingArea();
-            if (!isInHandwritingArea(handwritingArea, x, y, view)
+            if (!isInHandwritingArea(handwritingArea, x, y, view, isHover)
                     || !shouldTriggerStylusHandwritingForView(view)) {
                 continue;
             }
@@ -551,15 +561,48 @@
      * Return true if the (x, y) is inside by the given {@link Rect} with the View's
      * handwriting bounds with offsets applied.
      */
-    private static boolean isInHandwritingArea(@Nullable Rect handwritingArea,
-            float x, float y, View view) {
+    private boolean isInHandwritingArea(@Nullable Rect handwritingArea,
+            float x, float y, View view, boolean isHover) {
         if (handwritingArea == null) return false;
 
-        return contains(handwritingArea, x, y,
+        if (!contains(handwritingArea, x, y,
                 view.getHandwritingBoundsOffsetLeft(),
                 view.getHandwritingBoundsOffsetTop(),
                 view.getHandwritingBoundsOffsetRight(),
-                view.getHandwritingBoundsOffsetBottom());
+                view.getHandwritingBoundsOffsetBottom())) {
+            return false;
+        }
+
+        // The returned handwritingArea computed by ViewParent#getChildVisibleRect didn't consider
+        // the case where a view is stacking on top of the editor. (e.g. DrawerLayout, popup)
+        // We must check the hit region of the editor again, and avoid the case where another
+        // view on top of the editor is handling MotionEvents.
+        ViewParent parent = view.getParent();
+        if (parent == null) {
+            return true;
+        }
+
+        Region region = mTempRegion;
+        mTempRegion.set(0, 0, view.getWidth(), view.getHeight());
+        Matrix matrix = mTempMatrix;
+        matrix.reset();
+        if (!parent.getChildLocalHitRegion(view, region, matrix, isHover)) {
+            return false;
+        }
+
+        // It's not easy to extend the region by the given handwritingBoundsOffset. Instead, we
+        // create a rectangle surrounding the motion event location and check if this rectangle
+        // overlaps with the hit region of the editor.
+        float left = x - view.getHandwritingBoundsOffsetRight();
+        float top = y - view.getHandwritingBoundsOffsetBottom();
+        float right = Math.max(x + view.getHandwritingBoundsOffsetLeft(), left + 1);
+        float bottom =  Math.max(y + view.getHandwritingBoundsOffsetTop(), top + 1);
+        RectF rectF = mTempRectF;
+        rectF.set(left, top, right, bottom);
+        matrix.mapRect(rectF);
+
+        return region.op(Math.round(rectF.left), Math.round(rectF.top),
+                Math.round(rectF.right), Math.round(rectF.bottom), Region.Op.INTERSECT);
     }
 
     /**
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index 1b1098d..7bdff8c 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -7361,6 +7361,90 @@
         }
     }
 
+    /**
+     * @hide
+     */
+    @Override
+    public boolean getChildLocalHitRegion(@NonNull View child, @NonNull Region region,
+            @NonNull Matrix matrix, boolean isHover) {
+        if (!child.hasIdentityMatrix()) {
+            matrix.preConcat(child.getInverseMatrix());
+        }
+
+        final int dx = child.mLeft - mScrollX;
+        final int dy = child.mTop - mScrollY;
+        matrix.preTranslate(-dx, -dy);
+
+        final int width = mRight - mLeft;
+        final int height = mBottom - mTop;
+
+        // Map the bounds of this view into the region's coordinates and clip the region.
+        final RectF rect = mAttachInfo != null ? mAttachInfo.mTmpTransformRect : new RectF();
+        rect.set(0, 0, width, height);
+        matrix.mapRect(rect);
+
+        boolean notEmpty = region.op(Math.round(rect.left), Math.round(rect.top),
+                Math.round(rect.right), Math.round(rect.bottom), Region.Op.INTERSECT);
+
+        if (isHover) {
+            HoverTarget target = mFirstHoverTarget;
+            boolean childIsHit = false;
+            while (target != null) {
+                final HoverTarget next = target.next;
+                if (target.child == child) {
+                    childIsHit = true;
+                    break;
+                }
+                target = next;
+            }
+            if (!childIsHit) {
+                target = mFirstHoverTarget;
+                while (notEmpty && target != null) {
+                    final HoverTarget next = target.next;
+                    final View hoveredView = target.child;
+
+                    rect.set(hoveredView.mLeft, hoveredView.mTop, hoveredView.mRight,
+                            hoveredView.mBottom);
+                    matrix.mapRect(rect);
+                    notEmpty = region.op(Math.round(rect.left), Math.round(rect.top),
+                            Math.round(rect.right), Math.round(rect.bottom), Region.Op.DIFFERENCE);
+                    target = next;
+                }
+            }
+        } else {
+            TouchTarget target = mFirstTouchTarget;
+            boolean childIsHit = false;
+            while (target != null) {
+                final TouchTarget next = target.next;
+                if (target.child == child) {
+                    childIsHit = true;
+                    break;
+                }
+                target = next;
+            }
+            if (!childIsHit) {
+                target = mFirstTouchTarget;
+                while (notEmpty && target != null) {
+                    final TouchTarget next = target.next;
+                    final View touchedView = target.child;
+
+                    rect.set(touchedView.mLeft, touchedView.mTop, touchedView.mRight,
+                            touchedView.mBottom);
+                    matrix.mapRect(rect);
+                    notEmpty = region.op(Math.round(rect.left), Math.round(rect.top),
+                            Math.round(rect.right), Math.round(rect.bottom), Region.Op.DIFFERENCE);
+                    target = next;
+                }
+            }
+        }
+
+        if (notEmpty && mParent != null) {
+            notEmpty = mParent.getChildLocalHitRegion(this, region, matrix, isHover);
+        }
+        return notEmpty;
+    }
+
+
     private static void applyOpToRegionByBounds(Region region, View view, Region.Op op) {
         final int[] locationInWindow = new int[2];
         view.getLocationInWindow(locationInWindow);
diff --git a/core/java/android/view/ViewParent.java b/core/java/android/view/ViewParent.java
index 1020d2e..54bc348 100644
--- a/core/java/android/view/ViewParent.java
+++ b/core/java/android/view/ViewParent.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.os.Bundle;
@@ -686,6 +687,36 @@
     }
 
     /**
+     * Compute the region where the child can receive the {@link MotionEvent}s from the root view.
+     *
+     * <p> Given region where the child will accept {@link MotionEvent}s.
+     * Modify the region to the unblocked region where the child can receive the
+     * {@link MotionEvent}s from the view root.
+     * </p>
+     *
+     * <p> The given region is always clipped by the bounds of the parent views. When there are
+     * on-going {@link MotionEvent}s, this method also makes use of the event dispatching results to
+     * determine whether a sibling view will also block the child's hit region.
+     * </p>
+     *
+     * @param child a child View, whose hit region we want to compute.
+     * @param region the initial hit region where the child view will handle {@link MotionEvent}s,
+     *              defined in the child coordinates. Will be overwritten to the result hit region.
+     * @param matrix the matrix that maps the given child view's coordinates to the region
+     *               coordinates. It will be modified to a matrix that maps window coordinates to
+     *               the result region's coordinates.
+     * @param isHover if true it will return the hover events' hit region, otherwise it will
+     *               return the touch events' hit region.
+     * @return true if the returned region is not empty.
+     * @hide
+     */
+    default boolean getChildLocalHitRegion(@NonNull View child, @NonNull Region region,
+            @NonNull Matrix matrix, boolean isHover) {
+        region.setEmpty();
+        return false;
+    }
+
+    /**
      * Unbuffered dispatch has been requested by a child of this view parent.
      * This method is called by the View hierarchy to signal ancestors that a View needs to
      * request unbuffered dispatch.
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 6edf0e2..c1ce5e0 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -127,6 +127,7 @@
 import android.graphics.PorterDuff;
 import android.graphics.RecordingCanvas;
 import android.graphics.Rect;
+import android.graphics.RectF;
 import android.graphics.Region;
 import android.graphics.RenderNode;
 import android.graphics.drawable.Drawable;
@@ -2393,6 +2394,22 @@
     }
 
     @Override
+    public boolean getChildLocalHitRegion(@NonNull View child, @NonNull Region region,
+            @NonNull Matrix matrix, boolean isHover) {
+        if (child != mView) {
+            throw new IllegalArgumentException("child " + child + " is not the root view "
+                    + mView + " managed by this ViewRootImpl");
+        }
+
+        RectF rectF = new RectF(0, 0, mWidth, mHeight);
+        matrix.mapRect(rectF);
+        // Note: don't apply scroll offset, because we want to know its
+        // visibility in the virtual canvas being given to the view hierarchy.
+        return region.op(Math.round(rectF.left), Math.round(rectF.top),
+                Math.round(rectF.right), Math.round(rectF.bottom), Region.Op.INTERSECT);
+    }
+
+    @Override
     public void bringChildToFront(View child) {
     }
 
diff --git a/core/java/com/android/internal/app/IHotwordRecognitionStatusCallback.aidl b/core/java/com/android/internal/app/IHotwordRecognitionStatusCallback.aidl
index 3801188..ba87caa 100644
--- a/core/java/com/android/internal/app/IHotwordRecognitionStatusCallback.aidl
+++ b/core/java/com/android/internal/app/IHotwordRecognitionStatusCallback.aidl
@@ -22,6 +22,7 @@
 import android.service.voice.HotwordRejectedResult;
 import android.service.voice.SoundTriggerFailure;
 import android.service.voice.VisualQueryDetectionServiceFailure;
+import com.android.internal.infra.AndroidFuture;
 
 /**
  * @hide
@@ -113,4 +114,9 @@
 
     /** Called when the hotword detection process is restarted */
     void onProcessRestarted();
+
+    /**
+     * Called when a file open request is sent.
+     */
+   void onOpenFile(in String filename, in AndroidFuture future);
 }
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 10cf353..7d1253c 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -7770,8 +7770,9 @@
                 android:process=":ui">
         </activity>
         <activity android:name="com.android.internal.app.PlatLogoActivity"
-                android:theme="@style/Theme.Wallpaper.NoTitleBar.Fullscreen"
+                android:theme="@style/Theme.NoTitleBar.Fullscreen"
                 android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
+                android:enableOnBackInvokedCallback="true"
                 android:icon="@drawable/platlogo"
                 android:process=":ui">
         </activity>
diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml
index 31755ef..a358c4f 100644
--- a/core/tests/coretests/AndroidManifest.xml
+++ b/core/tests/coretests/AndroidManifest.xml
@@ -1749,6 +1749,15 @@
                 <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
             </intent-filter>
         </activity>
+
+        <activity android:name="android.view.ViewGroupTestActivity"
+                  android:label="ViewGroup Test"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+            </intent-filter>
+        </activity>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/core/tests/coretests/res/layout/viewgroup_test.xml b/core/tests/coretests/res/layout/viewgroup_test.xml
new file mode 100644
index 0000000..04f4f52
--- /dev/null
+++ b/core/tests/coretests/res/layout/viewgroup_test.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+
+<!-- Demonstrates adding/removing views from ViewGroup. See corresponding Java code. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:id="@+id/linear_layout"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:orientation="vertical">
+    <EditText
+        android:id="@+id/view"
+        android:layout_width="20dp"
+        android:layout_height="10dp"
+        android:text="Hello World!"
+        android:background="#2F00FF00" />
+    <EditText
+        android:id="@+id/view_scale"
+        android:layout_width="20dp"
+        android:layout_height="10dp"
+        android:scaleX="0.5"
+        android:scaleY="2"
+        android:transformPivotX="0dp"
+        android:transformPivotY="0dp"
+        android:text="Hello World!"
+        android:background="#2F00FF00" />
+    <EditText
+        android:id="@+id/view_translate"
+        android:layout_width="20dp"
+        android:layout_height="10dp"
+        android:translationX="10dp"
+        android:translationY="20dp"
+        android:text="Hello World!"
+        android:background="#2F00FF00" />
+    <FrameLayout
+        android:layout_width="20dp"
+        android:layout_height="10dp">
+        <EditText
+            android:id="@+id/view_overlap_bottom"
+            android:layout_width="20dp"
+            android:layout_height="10dp"
+            android:text="Hello World!"/>
+        <Button
+            android:id="@+id/view_overlap_top"
+            android:layout_width="10dp"
+            android:layout_height="10dp"/>
+    </FrameLayout>
+
+    <FrameLayout
+        android:layout_width="20dp"
+        android:layout_height="10dp">
+        <EditText
+            android:id="@+id/view_cover_bottom"
+            android:layout_width="10dp"
+            android:layout_height="10dp"
+            android:text="Hello World!"/>
+        <Button
+            android:id="@+id/view_cover_top"
+            android:layout_width="10dp"
+            android:layout_height="10dp"/>
+    </FrameLayout>
+
+</LinearLayout>
diff --git a/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java b/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java
index a84ac55..55ded9c 100644
--- a/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java
+++ b/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java
@@ -136,7 +136,11 @@
         NotificationListenerService.RankingMap retrievedRankings =
                 retrievedRankingUpdate.getRankingMap();
         assertNotNull(retrievedRankings);
-        assertTrue(retrievedRankingUpdate.isFdNotNullAndClosed());
+        // The rankingUpdate file descriptor is only non-null in the new path.
+        if (SystemUiSystemPropertiesFlags.getResolver().isEnabled(
+                SystemUiSystemPropertiesFlags.NotificationFlags.RANKING_UPDATE_ASHMEM)) {
+            assertTrue(retrievedRankingUpdate.isFdNotNullAndClosed());
+        }
         NotificationListenerService.Ranking retrievedRanking =
                 new NotificationListenerService.Ranking();
         assertTrue(retrievedRankings.getRanking(TEST_KEY, retrievedRanking));
diff --git a/core/tests/coretests/src/android/view/ViewGroupGetChildLocalHitRegionTest.java b/core/tests/coretests/src/android/view/ViewGroupGetChildLocalHitRegionTest.java
new file mode 100644
index 0000000..60a0a2a
--- /dev/null
+++ b/core/tests/coretests/src/android/view/ViewGroupGetChildLocalHitRegionTest.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static com.google.common.truth.Truth.assertThat;
+
+
+import android.graphics.Matrix;
+import android.graphics.Region;
+import android.platform.test.annotations.Presubmit;
+import android.widget.LinearLayout;
+
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.filters.SmallTest;
+
+import com.android.frameworks.coretests.R;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+/**
+ * Test basic functions of ViewGroup.
+ *
+ * Build/Install/Run:
+ *     atest FrameworksCoreTests:ViewGroupTest
+ */
+@Presubmit
+@SmallTest
+public class ViewGroupGetChildLocalHitRegionTest {
+    @Rule
+    public ActivityScenarioRule<ViewGroupTestActivity> mScenarioRule =
+            new ActivityScenarioRule<>(ViewGroupTestActivity.class);
+
+    private LinearLayout mRoot;
+    private final int[] mRootLocation = new int[2];
+
+    @Before
+    public void setup() {
+        mScenarioRule.getScenario().onActivity(activity -> {
+            mRoot = activity.findViewById(R.id.linear_layout);
+            mRoot.getLocationInWindow(mRootLocation);
+        });
+    }
+
+    @Test
+    public void testGetChildLocalHitRegion() {
+        assertGetChildLocalHitRegion(R.id.view);
+    }
+
+    @Test
+    public void testGetChildLocalHitRegion_withScale() {
+        assertGetChildLocalHitRegion(R.id.view_scale);
+    }
+
+    @Test
+    public void testGetChildLocalHitRegion_withTranslate() {
+        assertGetChildLocalHitRegion(R.id.view_translate);
+    }
+
+    @Test
+    public void testGetChildLocalHitRegion_overlap_noMotionEvent() {
+        assertGetChildLocalHitRegion(R.id.view_overlap_bottom);
+    }
+    @Test
+    public void testGetChildLocalHitRegion_overlap_withMotionEvent() {
+        // In this case, view_cover_bottom is partially covered by the view_cover_top.
+        // The returned region is the bounds of the bottom view subtract the bounds of the top view.
+        assertGetChildLocalHitRegion(R.id.view_overlap_top, R.id.view_overlap_bottom);
+    }
+
+    @Test
+    public void testGetChildLocalHitRegion_cover_withMotionEvent() {
+        // In this case, view_cover_bottom is completely covered by the view_cover_top.
+        // The returned region is expected to be empty.
+        assertGetChildLocalHitRegionEmpty(R.id.view_cover_top, R.id.view_cover_bottom);
+    }
+
+    private void injectMotionEvent(View view, boolean isHover) {
+        int[] location = new int[2];
+        view.getLocationInWindow(location);
+
+        float x = location[0] + view.getWidth() / 2f;
+        float y = location[1] + view.getHeight() / 2f;
+
+        int action = isHover ? MotionEvent.ACTION_HOVER_ENTER : MotionEvent.ACTION_DOWN;
+        MotionEvent motionEvent = MotionEvent.obtain(/* downtime= */ 0, /* eventTime= */ 0, action,
+                x, y, /* pressure= */ 0, /* size= */ 0, /* metaState= */ 0,
+                /* xPrecision= */ 1, /* yPrecision= */ 1, /* deviceId= */0, /* edgeFlags= */0);
+
+        View rootView = view.getRootView();
+        rootView.dispatchPointerEvent(motionEvent);
+    }
+
+    private void assertGetChildLocalHitRegion(int viewId) {
+        assertGetChildLocalHitRegion(viewId, /* isHover= */ true);
+        assertGetChildLocalHitRegion(viewId, /* isHover= */ false);
+    }
+
+    /**
+     * Assert ViewParent#getChildLocalHitRegion for a single view.
+     * @param viewId the viewId of the tested view.
+     * @param isHover if true, check the hit region of the hover events. Otherwise, check the hit
+     *                region of the touch events.
+     */
+    private void assertGetChildLocalHitRegion(int viewId, boolean isHover) {
+        mScenarioRule.getScenario().onActivity(activity -> {
+            View view = activity.findViewById(viewId);
+
+            Matrix actualMatrix = new Matrix();
+            Region actualRegion = new Region(0, 0, view.getWidth(), view.getHeight());
+            boolean actualNotEmpty = view.getParent()
+                    .getChildLocalHitRegion(view, actualRegion, actualMatrix, isHover);
+
+            int[] windowLocation = new int[2];
+            view.getLocationInWindow(windowLocation);
+            Matrix expectMatrix = new Matrix();
+            expectMatrix.preScale(1 / view.getScaleX(), 1 / view.getScaleY());
+            expectMatrix.preTranslate(-windowLocation[0], -windowLocation[1]);
+
+            Region expectRegion = new Region(0, 0, view.getWidth(), view.getHeight());
+
+            assertThat(actualNotEmpty).isTrue();
+            assertThat(actualMatrix).isEqualTo(expectMatrix);
+            assertThat(actualRegion).isEqualTo(expectRegion);
+        });
+    }
+
+    private void assertGetChildLocalHitRegion(int viewIdTop, int viewIdBottom) {
+        assertGetChildLocalHitRegion(viewIdTop, viewIdBottom, /* isHover= */ true);
+        assertGetChildLocalHitRegion(viewIdTop, viewIdBottom, /* isHover= */ false);
+    }
+
+    /**
+     * Assert ViewParent#getChildLocalHitRegion of a view that is covered by another view. It will
+     * inject {@link MotionEvent}s to the view on top first and then get the hit region of the
+     * bottom view.
+     *
+     * @param viewIdTop the view id of the test view on top.
+     * @param viewIdBottom the view id of the test view at the bottom.
+     * @param isHover if true, check the hit region of the hover events. Otherwise, check the hit
+     *                region of the touch events.
+     */
+    private void assertGetChildLocalHitRegion(int viewIdTop, int viewIdBottom, boolean isHover) {
+        mScenarioRule.getScenario().onActivity(activity -> {
+            View viewTop = activity.findViewById(viewIdTop);
+            View viewBottom = activity.findViewById(viewIdBottom);
+
+            injectMotionEvent(viewTop, isHover);
+
+            Matrix actualMatrix = new Matrix();
+            Region actualRegion = new Region(0, 0, viewBottom.getWidth(), viewBottom.getHeight());
+            boolean actualNotEmpty = viewBottom.getParent()
+                    .getChildLocalHitRegion(viewBottom, actualRegion, actualMatrix, isHover);
+
+            int[] windowLocation = new int[2];
+            viewBottom.getLocationInWindow(windowLocation);
+            Matrix expectMatrix = new Matrix();
+            expectMatrix.preTranslate(-windowLocation[0], -windowLocation[1]);
+
+            Region expectRegion = new Region(0, 0, viewBottom.getWidth(), viewBottom.getHeight());
+            expectRegion.op(0, 0, viewTop.getWidth(), viewTop.getHeight(), Region.Op.DIFFERENCE);
+
+            assertThat(actualNotEmpty).isTrue();
+            assertThat(actualMatrix).isEqualTo(expectMatrix);
+            assertThat(actualRegion).isEqualTo(expectRegion);
+        });
+    }
+
+    private void assertGetChildLocalHitRegionEmpty(int viewIdTop, int viewIdBottom) {
+        assertGetChildLocalHitRegionEmpty(viewIdTop, viewIdBottom, /* isHover= */ true);
+        assertGetChildLocalHitRegionEmpty(viewIdTop, viewIdBottom, /* isHover= */ false);
+    }
+
+    /**
+     * Assert ViewParent#getChildLocalHitRegion returns an empty region for a view that is
+     * completely covered by another view. It will inject {@link MotionEvent}s to the view on top
+     * first and then get the hit region of the
+     * bottom view.
+     *
+     * @param viewIdTop the view id of the test view on top.
+     * @param viewIdBottom the view id of the test view at the bottom.
+     * @param isHover if true, check the hit region of the hover events. Otherwise, check the hit
+     *                region of the touch events.
+     */
+    private void assertGetChildLocalHitRegionEmpty(int viewIdTop, int viewIdBottom,
+            boolean isHover) {
+        mScenarioRule.getScenario().onActivity(activity -> {
+            View viewTop = activity.findViewById(viewIdTop);
+            View viewBottom = activity.findViewById(viewIdBottom);
+
+            injectMotionEvent(viewTop, isHover);
+
+            Region actualRegion = new Region(0, 0, viewBottom.getWidth(), viewBottom.getHeight());
+            boolean actualNotEmpty = viewBottom.getParent()
+                    .getChildLocalHitRegion(viewBottom, actualRegion, new Matrix(), isHover);
+
+            assertThat(actualNotEmpty).isFalse();
+            assertThat(actualRegion.isEmpty()).isTrue();
+        });
+    }
+}
diff --git a/core/tests/coretests/src/android/view/ViewGroupTestActivity.java b/core/tests/coretests/src/android/view/ViewGroupTestActivity.java
new file mode 100644
index 0000000..b94bda5
--- /dev/null
+++ b/core/tests/coretests/src/android/view/ViewGroupTestActivity.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.os.Bundle;
+
+import com.android.frameworks.coretests.R;
+
+public class ViewGroupTestActivity extends Activity {
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.viewgroup_test);
+    }
+}
diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java b/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java
index 388a996..b4c72ca 100644
--- a/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java
+++ b/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java
@@ -21,7 +21,9 @@
 
 import android.app.Instrumentation;
 import android.content.Context;
+import android.graphics.Matrix;
 import android.graphics.Rect;
+import android.graphics.Region;
 import android.view.View;
 import android.view.ViewGroup;
 
@@ -45,7 +47,7 @@
             float handwritingBoundsOffsetRight, float handwritingBoundsOffsetBottom) {
         final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
         final Context context = instrumentation.getTargetContext();
-        // mock a parent so that HandwritingInitiator can get
+        // mock a parent so that HandwritingInitiator can get visible rect and hit region.
         final ViewGroup parent = new ViewGroup(context) {
             @Override
             protected void onLayout(boolean changed, int l, int t, int r, int b) {
@@ -56,6 +58,14 @@
                 r.set(handwritingArea);
                 return true;
             }
+
+            @Override
+            public boolean getChildLocalHitRegion(View child, Region region, Matrix matrix,
+                    boolean isHover) {
+                matrix.reset();
+                region.set(handwritingArea);
+                return true;
+            }
         };
 
         View view = spy(new View(context));
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java
index ef93a33..be1b9b1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java
@@ -16,6 +16,7 @@
 package com.android.wm.shell.common.split;
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
@@ -60,7 +61,8 @@
     public static final int[] CONTROLLED_WINDOWING_MODES =
             {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED};
     public static final int[] CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE =
-            {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED, WINDOWING_MODE_MULTI_WINDOW};
+            {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED, WINDOWING_MODE_MULTI_WINDOW,
+            WINDOWING_MODE_FREEFORM};
 
     /** Flag applied to a transition change to identify it as a divider bar for animation. */
     public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 1d46e75..633f627 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -18,6 +18,7 @@
 
 import android.R
 import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration
 import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
 import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
@@ -301,6 +302,24 @@
         }
     }
 
+    /** Move a desktop app to split screen. */
+    fun moveToSplit(task: RunningTaskInfo) {
+        KtProtoLog.v(
+            WM_SHELL_DESKTOP_MODE,
+            "DesktopTasksController: moveToSplit taskId=%d",
+            task.taskId
+        )
+        val wct = WindowContainerTransaction()
+        wct.setWindowingMode(task.token, WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW)
+        wct.setBounds(task.token, null)
+        wct.setDensityDpi(task.token, getDefaultDensityDpi())
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+            transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
+        } else {
+            shellTaskOrganizer.applyTransaction(wct)
+        }
+    }
+
     /**
      * The second part of the animated move to desktop transition, called after
      * {@link startMoveToDesktop}. Move a task to fullscreen after being dragged from fullscreen
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 2be7a49..29fff03 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -71,6 +71,7 @@
 import com.android.wm.shell.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.desktopmode.DesktopTasksController;
 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
+import com.android.wm.shell.splitscreen.SplitScreen;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.sysui.KeyguardChangeListener;
 import com.android.wm.shell.sysui.ShellController;
@@ -200,6 +201,19 @@
     @Override
     public void setSplitScreenController(SplitScreenController splitScreenController) {
         mSplitScreenController = splitScreenController;
+        mSplitScreenController.registerSplitScreenListener(new SplitScreen.SplitScreenListener() {
+            @Override
+            public void onTaskStageChanged(int taskId, int stage, boolean visible) {
+                if (visible) {
+                    DesktopModeWindowDecoration decor = mWindowDecorByTaskId.get(taskId);
+                    if (decor != null && DesktopModeStatus.isActive(mContext)
+                            && decor.mTaskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
+                        mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(false));
+                        mDesktopTasksController.ifPresent(c -> c.moveToSplit(decor.mTaskInfo));
+                    }
+                }
+            }
+        });
     }
 
     @Override
diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java
index 9ab84d2..f90a17a 100644
--- a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java
+++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java
@@ -45,6 +45,7 @@
     private static final int EXTREME_LOW_BATTERY_THRESHOLD = 3;
     private static final int DEFAULT_CHARGING_VOLTAGE_MICRO_VOLT = 5000000;
 
+    public static final int BATTERY_LEVEL_UNKNOWN = -1;
     public static final int CHARGING_UNKNOWN = -1;
     public static final int CHARGING_SLOWLY = 0;
     public static final int CHARGING_REGULAR = 1;
@@ -186,12 +187,13 @@
     /** Gets the battery level from the intent. */
     public static int getBatteryLevel(Intent batteryChangedIntent) {
         if (batteryChangedIntent == null) {
-            return -1; /*invalid battery level*/
+            return BATTERY_LEVEL_UNKNOWN;
         }
-        final int level = batteryChangedIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
+        final int level =
+                batteryChangedIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, BATTERY_LEVEL_UNKNOWN);
         final int scale = batteryChangedIntent.getIntExtra(BatteryManager.EXTRA_SCALE, 0);
         return scale == 0
-                ? -1 /*invalid battery level*/
+                ? BATTERY_LEVEL_UNKNOWN
                 : Math.round((level / (float) scale) * 100f);
     }
 
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
index d65edae..0fa4ebd 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
@@ -409,6 +409,18 @@
             scope.launch(bgDispatcher) { mutateSetting { it.copy(seedColor = value) } }
         }
 
+    // Returns currentClockId if clock is connected, otherwise DEFAULT_CLOCK_ID. Since this
+    // is dependent on which clocks are connected, it may change when a clock is installed or
+    // removed from the device (unlike currentClockId).
+    // TODO: Merge w/ CurrentClockId when we convert to a flow. We shouldn't need both behaviors.
+    val activeClockId: String
+        get() {
+            if (!availableClocks.containsKey(currentClockId)) {
+                return DEFAULT_CLOCK_ID
+            }
+            return currentClockId
+        }
+
     init {
         // Register default clock designs
         for (clock in defaultClockProvider.getClocks()) {
diff --git a/packages/SystemUI/res/drawable/stat_sys_ringer_silent.xml b/packages/SystemUI/res/drawable/stat_sys_ringer_silent.xml
index 4a9d41f..b83f15a 100644
--- a/packages/SystemUI/res/drawable/stat_sys_ringer_silent.xml
+++ b/packages/SystemUI/res/drawable/stat_sys_ringer_silent.xml
@@ -14,6 +14,4 @@
     limitations under the License.
 -->
 <inset xmlns:android="http://schemas.android.com/apk/res/android"
-    android:insetLeft="3dp"
-    android:insetRight="3dp"
     android:drawable="@drawable/ic_speaker_mute" />
diff --git a/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml b/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml
index 78cd718..39ec09b 100644
--- a/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml
+++ b/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml
@@ -34,8 +34,8 @@
         android:layout_height="@dimen/overlay_dismiss_button_tappable_size"
         android:contentDescription="@string/screenshot_dismiss_work_profile">
         <ImageView
-            android:layout_width="16dp"
-            android:layout_height="16dp"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
             android:layout_gravity="center"
             android:background="@drawable/circular_background"
             android:backgroundTint="?androidprv:attr/materialColorSurfaceContainerHigh"
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 04692c4..9f3908a 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -27,6 +27,7 @@
 import static com.android.keyguard.KeyguardSecurityContainer.USER_TYPE_SECONDARY_USER;
 import static com.android.keyguard.KeyguardSecurityContainer.USER_TYPE_WORK_PROFILE;
 import static com.android.systemui.DejankUtils.whitelistIpcs;
+import static com.android.systemui.flags.Flags.LOCKSCREEN_ENABLE_LANDSCAPE;
 import static com.android.systemui.flags.Flags.REVAMPED_BOUNCER_MESSAGES;
 
 import android.app.ActivityManager;
@@ -370,8 +371,12 @@
 
                 @Override
                 public void onOrientationChanged(int orientation) {
-                    KeyguardSecurityContainerController.this
+                    if (mFeatureFlags.isEnabled(LOCKSCREEN_ENABLE_LANDSCAPE)) {
+                        // TODO(b/295603468)
+                        // Fix reinflation of views when flag is enabled.
+                        KeyguardSecurityContainerController.this
                             .onDensityOrFontScaleOrOrientationChanged();
+                    }
                 }
             };
     private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback =
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index f73a602..64c4eec 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -107,6 +107,7 @@
 import com.android.systemui.statusbar.notification.row.dagger.NotificationShelfComponent;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.LetterboxModule;
+import com.android.systemui.statusbar.phone.NotificationIconAreaControllerModule;
 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent;
 import com.android.systemui.statusbar.pipeline.dagger.StatusBarPipelineModule;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
@@ -184,6 +185,7 @@
             MediaProjectionModule.class,
             MediaProjectionTaskSwitcherModule.class,
             MotionToolModule.class,
+            NotificationIconAreaControllerModule.class,
             PeopleHubModule.class,
             PeopleModule.class,
             PluginModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index a6ada33..8485fe2 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -533,13 +533,6 @@
             teamfood = false
         )
 
-    // TODO(b/198643358): Tracking bug
-    @Keep
-    @JvmField
-    val ENABLE_PIP_SIZE_LARGE_SCREEN =
-        sysPropBooleanFlag("persist.wm.debug.enable_pip_size_large_screen", default = true)
-
-
     // TODO(b/293252410) : Tracking Bug
     @JvmField
     val LOCKSCREEN_ENABLE_LANDSCAPE =
@@ -623,6 +616,10 @@
     // TODO(b/251205791): Tracking Bug
     @JvmField val SCREENSHOT_APP_CLIPS = releasedFlag("screenshot_app_clips")
 
+    /** TODO(b/295143676): Tracking bug. When enable, captures a screenshot for each display. */
+    @JvmField
+    val MULTI_DISPLAY_SCREENSHOT = unreleasedFlag("multi_display_screenshot")
+
     // 1400 - columbus
     // TODO(b/254512756): Tracking Bug
     val QUICK_TAP_IN_PCC = releasedFlag("quick_tap_in_pcc")
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt
index 635961b..e501ece 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt
@@ -54,7 +54,11 @@
         if (event.handleAction()) {
             when (event.keyCode) {
                 KeyEvent.KEYCODE_MENU -> return dispatchMenuKeyEvent()
-                KeyEvent.KEYCODE_SPACE -> return dispatchSpaceEvent()
+                KeyEvent.KEYCODE_SPACE,
+                KeyEvent.KEYCODE_ENTER ->
+                    if (isDeviceInteractive()) {
+                        return collapseShadeLockedOrShowPrimaryBouncer()
+                    }
             }
         }
         return false
@@ -90,16 +94,22 @@
                 (statusBarStateController.state != StatusBarState.SHADE) &&
                 statusBarKeyguardViewManager.shouldDismissOnMenuPressed()
         if (shouldUnlockOnMenuPressed) {
-            shadeController.animateCollapseShadeForced()
-            return true
+            return collapseShadeLockedOrShowPrimaryBouncer()
         }
         return false
     }
 
-    private fun dispatchSpaceEvent(): Boolean {
-        if (isDeviceInteractive() && statusBarStateController.state != StatusBarState.SHADE) {
-            shadeController.animateCollapseShadeForced()
-            return true
+    private fun collapseShadeLockedOrShowPrimaryBouncer(): Boolean {
+        when (statusBarStateController.state) {
+            StatusBarState.SHADE -> return false
+            StatusBarState.SHADE_LOCKED -> {
+                shadeController.animateCollapseShadeForced()
+                return true
+            }
+            StatusBarState.KEYGUARD -> {
+                statusBarKeyguardViewManager.showPrimaryBouncer(true)
+                return true
+            }
         }
         return false
     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
index a3d1d8c..d8824983 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -515,11 +515,13 @@
             mSeekBar.setOnTouchListener((v, event) -> false);
             updateIconAreaClickListener((v) -> {
                 if (device.getCurrentVolume() == 0) {
+                    mController.logInteractionUnmuteDevice(device);
                     mSeekBar.setVolume(UNMUTE_DEFAULT_VOLUME);
                     mController.adjustVolume(device, UNMUTE_DEFAULT_VOLUME);
                     updateUnmutedVolumeIcon();
                     mIconAreaLayout.setOnTouchListener(((iconV, event) -> false));
                 } else {
+                    mController.logInteractionMuteDevice(device);
                     mSeekBar.resetVolume();
                     mController.adjustVolume(device, 0);
                     updateMutedVolumeIcon();
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 b431bab..13cd8e3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -843,6 +843,14 @@
         mMetricLogger.logInteractionAdjustVolume(device);
     }
 
+    void logInteractionMuteDevice(MediaDevice device) {
+        mMetricLogger.logInteractionMute(device);
+    }
+
+    void logInteractionUnmuteDevice(MediaDevice device) {
+        mMetricLogger.logInteractionUnmute(device);
+    }
+
     String getPackageName() {
         return mPackageName;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java
index 412d1a3..ffd626a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java
@@ -154,6 +154,38 @@
     }
 
     /**
+     * Do the metric logging of muting device.
+     */
+    public void logInteractionMute(MediaDevice source) {
+        if (DEBUG) {
+            Log.d(TAG, "logInteraction - Mute");
+        }
+
+        SysUiStatsLog.write(
+                SysUiStatsLog.MEDIAOUTPUT_OP_INTERACTION_REPORT,
+                SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__INTERACTION_TYPE__MUTE,
+                getInteractionDeviceType(source),
+                getLoggingPackageName(),
+                source.isSuggestedDevice());
+    }
+
+    /**
+     * Do the metric logging of unmuting device.
+     */
+    public void logInteractionUnmute(MediaDevice source) {
+        if (DEBUG) {
+            Log.d(TAG, "logInteraction - Unmute");
+        }
+
+        SysUiStatsLog.write(
+                SysUiStatsLog.MEDIAOUTPUT_OP_INTERACTION_REPORT,
+                SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__INTERACTION_TYPE__UNMUTE,
+                getInteractionDeviceType(source),
+                getLoggingPackageName(),
+                source.isSuggestedDevice());
+    }
+
+    /**
      * Do the metric logging of content switching failure.
      *
      * @param deviceItemList media item list for device count updating
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
index e8683fb..fb99775 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
@@ -63,6 +63,7 @@
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setDialogTitle(R.string.screenrecord_permission_dialog_title)
+        setTitle(R.string.screenrecord_title)
         setStartButtonText(R.string.screenrecord_permission_dialog_continue)
         setStartButtonOnClickListener { v: View? ->
             onStartRecordingClicked?.run()
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index cabbeb1..014093d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -985,6 +985,7 @@
         // Make sure the clock is in the correct position after the unlock animation
         // so that it's not in the wrong place when we show the keyguard again.
         positionClockAndNotifications(true /* forceClockUpdate */);
+        mScrimController.onUnlockAnimationFinished();
     }
 
     private void unlockAnimationStarted(
@@ -1243,6 +1244,13 @@
             mKeyguardStatusViewController.init();
         }
         mKeyguardStatusViewController.setSplitShadeEnabled(mSplitShadeEnabled);
+        mKeyguardStatusViewController.getView().addOnLayoutChangeListener(
+                (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+                    int oldHeight = oldBottom - oldTop;
+                    if (v.getHeight() != oldHeight) {
+                        mNotificationStackScrollLayoutController.animateNextTopPaddingChange();
+                    }
+                });
 
         updateClockAppearance();
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index 832a25b..802ed80 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -21,8 +21,6 @@
 import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
 
 import android.app.StatusBarManager;
-import android.media.AudioManager;
-import android.media.session.MediaSessionLegacyHelper;
 import android.os.PowerManager;
 import android.util.Log;
 import android.view.GestureDetector;
@@ -47,6 +45,7 @@
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
+import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.keyguard.shared.model.TransitionState;
@@ -101,6 +100,7 @@
     private final NotificationInsetsController mNotificationInsetsController;
     private final boolean mIsTrackpadCommonEnabled;
     private final FeatureFlags mFeatureFlags;
+    private final KeyEventInteractor mKeyEventInteractor;
     private GestureDetector mPulsingWakeupGestureHandler;
     private GestureDetector mDreamingWakeupGestureHandler;
     private View mBrightnessMirror;
@@ -164,7 +164,8 @@
             FeatureFlags featureFlags,
             SystemClock clock,
             BouncerMessageInteractor bouncerMessageInteractor,
-            BouncerLogger bouncerLogger) {
+            BouncerLogger bouncerLogger,
+            KeyEventInteractor keyEventInteractor) {
         mLockscreenShadeTransitionController = transitionController;
         mFalsingCollector = falsingCollector;
         mStatusBarStateController = statusBarStateController;
@@ -190,6 +191,7 @@
         mNotificationInsetsController = notificationInsetsController;
         mIsTrackpadCommonEnabled = featureFlags.isEnabled(TRACKPAD_GESTURE_COMMON);
         mFeatureFlags = featureFlags;
+        mKeyEventInteractor = keyEventInteractor;
 
         // This view is not part of the newly inflated expanded status bar.
         mBrightnessMirror = mView.findViewById(R.id.brightness_mirror_container);
@@ -457,44 +459,17 @@
 
             @Override
             public boolean interceptMediaKey(KeyEvent event) {
-                return mService.interceptMediaKey(event);
+                return mKeyEventInteractor.interceptMediaKey(event);
             }
 
             @Override
             public boolean dispatchKeyEventPreIme(KeyEvent event) {
-                return mService.dispatchKeyEventPreIme(event);
+                return mKeyEventInteractor.dispatchKeyEventPreIme(event);
             }
 
             @Override
             public boolean dispatchKeyEvent(KeyEvent event) {
-                boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
-                switch (event.getKeyCode()) {
-                    case KeyEvent.KEYCODE_BACK:
-                        if (!down) {
-                            mBackActionInteractor.onBackRequested();
-                        }
-                        return true;
-                    case KeyEvent.KEYCODE_MENU:
-                        if (!down) {
-                            return mService.onMenuPressed();
-                        }
-                        break;
-                    case KeyEvent.KEYCODE_SPACE:
-                        if (!down) {
-                            return mService.onSpacePressed();
-                        }
-                        break;
-                    case KeyEvent.KEYCODE_VOLUME_DOWN:
-                    case KeyEvent.KEYCODE_VOLUME_UP:
-                        if (mStatusBarStateController.isDozing()) {
-                            MediaSessionLegacyHelper.getHelper(mView.getContext())
-                                    .sendVolumeKeyEvent(
-                                            event, AudioManager.USE_DEFAULT_STREAM_TYPE, true);
-                            return true;
-                        }
-                        break;
-                }
-                return false;
+                return mKeyEventInteractor.dispatchKeyEvent(event);
             }
         });
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt
index d3c19b7..5042f1b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt
@@ -31,56 +31,68 @@
 ) {
     fun logInitialClick(
         entry: NotificationEntry?,
+        index: Integer?,
         pendingIntent: PendingIntent
     ) {
         buffer.log(TAG, LogLevel.DEBUG, {
             str1 = entry?.key
             str2 = entry?.ranking?.channel?.id
-            str3 = pendingIntent.intent.toString()
+            str3 = pendingIntent.toString()
+            int1 = index?.toInt() ?: Int.MIN_VALUE
         }, {
-            "ACTION CLICK $str1 (channel=$str2) for pending intent $str3"
+            "ACTION CLICK $str1 (channel=$str2) for pending intent $str3 at index $int1"
         })
     }
 
     fun logRemoteInputWasHandled(
-        entry: NotificationEntry?
+        entry: NotificationEntry?,
+        index: Int?
     ) {
         buffer.log(TAG, LogLevel.DEBUG, {
             str1 = entry?.key
+            int1 = index ?: Int.MIN_VALUE
         }, {
-            "  [Action click] Triggered remote input (for $str1))"
+            "  [Action click] Triggered remote input (for $str1) at index $int1"
         })
     }
 
     fun logStartingIntentWithDefaultHandler(
         entry: NotificationEntry?,
-        pendingIntent: PendingIntent
+        pendingIntent: PendingIntent,
+        index: Int?
     ) {
         buffer.log(TAG, LogLevel.DEBUG, {
             str1 = entry?.key
-            str2 = pendingIntent.intent.toString()
+            str2 = pendingIntent.toString()
+            int1 = index ?: Int.MIN_VALUE
         }, {
-            "  [Action click] Launching intent $str2 via default handler (for $str1)"
+            "  [Action click] Launching intent $str2 via default handler (for $str1 at index $int1)"
         })
     }
 
     fun logWaitingToCloseKeyguard(
-        pendingIntent: PendingIntent
+        pendingIntent: PendingIntent,
+        index: Int?
     ) {
         buffer.log(TAG, LogLevel.DEBUG, {
-            str1 = pendingIntent.intent.toString()
+            str1 = pendingIntent.toString()
+            int1 = index ?: Int.MIN_VALUE
         }, {
-            "  [Action click] Intent $str1 launches an activity, dismissing keyguard first..."
+            "  [Action click] Intent $str1 at index $int1 launches an activity, dismissing " +
+                    "keyguard first..."
         })
     }
 
     fun logKeyguardGone(
-        pendingIntent: PendingIntent
+        pendingIntent: PendingIntent,
+        index: Int?
     ) {
         buffer.log(TAG, LogLevel.DEBUG, {
-            str1 = pendingIntent.intent.toString()
+            str1 = pendingIntent.toString()
+            int1 = index ?: Int.MIN_VALUE
         }, {
-            "  [Action click] Keyguard dismissed, calling default handler for intent $str1"
+            "  [Action click] Keyguard dismissed, calling default handler for intent $str1 at " +
+                    "index $int1"
         })
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationClickNotifier.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationClickNotifier.kt
index abf81c5..692a997 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationClickNotifier.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationClickNotifier.kt
@@ -2,10 +2,12 @@
 
 import android.app.Notification
 import android.os.RemoteException
+import android.util.Log
 import com.android.internal.statusbar.IStatusBarService
 import com.android.internal.statusbar.NotificationVisibility
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dagger.qualifiers.UiBackground
 import com.android.systemui.util.Assert
 import java.util.concurrent.Executor
 import javax.inject.Inject
@@ -21,7 +23,8 @@
 @SysUISingleton
 public class NotificationClickNotifier @Inject constructor(
     val barService: IStatusBarService,
-    @Main val mainExecutor: Executor
+    @Main val mainExecutor: Executor,
+    @UiBackground val backgroundExecutor: Executor
 ) {
     val listeners = mutableListOf<NotificationInteractionListener>()
 
@@ -48,13 +51,14 @@
         visibility: NotificationVisibility,
         generatedByAssistant: Boolean
     ) {
-        try {
-            barService.onNotificationActionClick(
-                    key, actionIndex, action, visibility, generatedByAssistant)
-        } catch (e: RemoteException) {
-            // nothing
+        backgroundExecutor.execute {
+            try {
+                barService.onNotificationActionClick(
+                        key, actionIndex, action, visibility, generatedByAssistant)
+            } catch (e: RemoteException) {
+                // nothing
+            }
         }
-
         mainExecutor.execute {
             notifyListenersAboutInteraction(key)
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
index da84afe..8089fd9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
@@ -119,11 +119,14 @@
             mPowerInteractor.wakeUpIfDozing(
                     "NOTIFICATION_CLICK", PowerManager.WAKE_REASON_GESTURE);
 
+            Integer actionIndex = (Integer)
+                    view.getTag(com.android.internal.R.id.notification_action_index_tag);
+
             final NotificationEntry entry = getNotificationForParent(view.getParent());
-            mLogger.logInitialClick(entry, pendingIntent);
+            mLogger.logInitialClick(entry, actionIndex, pendingIntent);
 
             if (handleRemoteInput(view, pendingIntent)) {
-                mLogger.logRemoteInputWasHandled(entry);
+                mLogger.logRemoteInputWasHandled(entry, actionIndex);
                 return true;
             }
 
@@ -141,9 +144,9 @@
             }
             Notification.Action action = getActionFromView(view, entry, pendingIntent);
             return mCallback.handleRemoteViewClick(view, pendingIntent,
-                    action == null ? false : action.isAuthenticationRequired(), () -> {
+                    action == null ? false : action.isAuthenticationRequired(), actionIndex, () -> {
                     Pair<Intent, ActivityOptions> options = response.getLaunchOptions(view);
-                    mLogger.logStartingIntentWithDefaultHandler(entry, pendingIntent);
+                    mLogger.logStartingIntentWithDefaultHandler(entry, pendingIntent, actionIndex);
                     boolean started = RemoteViews.startPendingIntent(view, pendingIntent, options);
                     if (started) releaseNotificationIfKeptForRemoteInputHistory(entry);
                     return started;
@@ -692,11 +695,13 @@
          * @param view
          * @param pendingIntent
          * @param appRequestedAuth
+         * @param actionIndex
          * @param defaultHandler
          * @return  true iff the click was handled
          */
         boolean handleRemoteViewClick(View view, PendingIntent pendingIntent,
-                boolean appRequestedAuth, ClickHandler defaultHandler);
+                boolean appRequestedAuth, @Nullable Integer actionIndex,
+                ClickHandler defaultHandler);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImpl.kt
new file mode 100644
index 0000000..26dfe3e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImpl.kt
@@ -0,0 +1,684 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.icon.ui.viewbinder
+
+import android.content.Context
+import android.graphics.Color
+import android.graphics.Rect
+import android.os.Bundle
+import android.os.Trace
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.FrameLayout
+import androidx.annotation.ColorInt
+import androidx.annotation.VisibleForTesting
+import androidx.collection.ArrayMap
+import com.android.app.animation.Interpolators
+import com.android.internal.statusbar.StatusBarIcon
+import com.android.internal.util.ContrastColorUtil
+import com.android.settingslib.Utils
+import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.demomode.DemoMode
+import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.flags.ViewRefactorFlag
+import com.android.systemui.plugins.DarkIconDispatcher
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.CrossFadeHelper
+import com.android.systemui.statusbar.NotificationListener
+import com.android.systemui.statusbar.NotificationMediaManager
+import com.android.systemui.statusbar.NotificationShelfController
+import com.android.systemui.statusbar.StatusBarIconView
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.notification.NotificationUtils
+import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
+import com.android.systemui.statusbar.notification.collection.ListEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerShelfViewModel
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel
+import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinderWrapperControllerImpl
+import com.android.systemui.statusbar.phone.DozeParameters
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.phone.NotificationIconAreaController
+import com.android.systemui.statusbar.phone.NotificationIconContainer
+import com.android.systemui.statusbar.phone.ScreenOffAnimationController
+import com.android.systemui.statusbar.window.StatusBarWindowController
+import com.android.wm.shell.bubbles.Bubbles
+import java.util.Optional
+import java.util.function.Function
+import javax.inject.Inject
+import kotlinx.coroutines.DisposableHandle
+
+/**
+ * Controller class for [NotificationIconContainer]. This implementation serves as a temporary
+ * wrapper around [NotificationIconContainerViewBinder], so that external code can continue to
+ * depend on the [NotificationIconAreaController] interface. Once
+ * [LegacyNotificationIconAreaControllerImpl] is removed, this class can go away and the ViewBinder
+ * can be used directly.
+ */
+@SysUISingleton
+class NotificationIconAreaControllerViewBinderWrapperImpl
+@Inject
+constructor(
+    private val context: Context,
+    private val statusBarStateController: StatusBarStateController,
+    private val wakeUpCoordinator: NotificationWakeUpCoordinator,
+    private val bypassController: KeyguardBypassController,
+    private val mediaManager: NotificationMediaManager,
+    notificationListener: NotificationListener,
+    private val dozeParameters: DozeParameters,
+    private val sectionStyleProvider: SectionStyleProvider,
+    private val bubblesOptional: Optional<Bubbles>,
+    demoModeController: DemoModeController,
+    darkIconDispatcher: DarkIconDispatcher,
+    featureFlags: FeatureFlags,
+    private val statusBarWindowController: StatusBarWindowController,
+    private val screenOffAnimationController: ScreenOffAnimationController,
+    private val shelfIconsViewModel: NotificationIconContainerShelfViewModel,
+    private val statusBarIconsViewModel: NotificationIconContainerStatusBarViewModel,
+    private val aodIconsViewModel: NotificationIconContainerAlwaysOnDisplayViewModel,
+) :
+    NotificationIconAreaController,
+    DarkIconDispatcher.DarkReceiver,
+    StatusBarStateController.StateListener,
+    NotificationWakeUpCoordinator.WakeUpListener,
+    DemoMode {
+
+    private val contrastColorUtil: ContrastColorUtil = ContrastColorUtil.getInstance(context)
+    private val updateStatusBarIcons = Runnable { updateStatusBarIcons() }
+    private val shelfRefactor = ViewRefactorFlag(featureFlags, Flags.NOTIFICATION_SHELF_REFACTOR)
+    private val tintAreas = ArrayList<Rect>()
+
+    private var iconSize = 0
+    private var iconHPadding = 0
+    private var iconTint = Color.WHITE
+    private var notificationEntries = listOf<ListEntry>()
+    private var notificationIconArea: View? = null
+    private var notificationIcons: NotificationIconContainer? = null
+    private var shelfIcons: NotificationIconContainer? = null
+    private var aodIcons: NotificationIconContainer? = null
+    private var aodBindJob: DisposableHandle? = null
+    private var aodIconAppearTranslation = 0
+    private var animationsEnabled = false
+    private var aodIconTint = 0
+    private var aodIconsVisible = false
+    private var showLowPriority = true
+
+    @VisibleForTesting
+    val settingsListener: NotificationListener.NotificationSettingsListener =
+        object : NotificationListener.NotificationSettingsListener {
+            override fun onStatusBarIconsBehaviorChanged(hideSilentStatusIcons: Boolean) {
+                showLowPriority = !hideSilentStatusIcons
+                updateStatusBarIcons()
+            }
+        }
+
+    init {
+        statusBarStateController.addCallback(this)
+        wakeUpCoordinator.addListener(this)
+        demoModeController.addCallback(this)
+        notificationListener.addNotificationSettingsListener(settingsListener)
+        initializeNotificationAreaViews(context)
+        reloadAodColor()
+        darkIconDispatcher.addDarkReceiver(this)
+    }
+
+    @VisibleForTesting
+    fun shouldShowLowPriorityIcons(): Boolean {
+        return showLowPriority
+    }
+
+    /** Called by the Keyguard*ViewController whose view contains the aod icons. */
+    override fun setupAodIcons(aodIcons: NotificationIconContainer) {
+        val changed = this.aodIcons != null && aodIcons !== this.aodIcons
+        if (changed) {
+            this.aodIcons!!.setAnimationsEnabled(false)
+            this.aodIcons!!.removeAllViews()
+            aodBindJob?.dispose()
+        }
+        this.aodIcons = aodIcons
+        this.aodIcons!!.setOnLockScreen(true)
+        aodBindJob = NotificationIconContainerViewBinder.bind(aodIcons, aodIconsViewModel)
+        updateAodIconsVisibility(animate = false, forceUpdate = changed)
+        updateAnimations()
+        if (changed) {
+            updateAodNotificationIcons()
+        }
+        updateIconLayoutParams(context)
+    }
+
+    override fun setupShelf(notificationShelfController: NotificationShelfController) =
+        NotificationShelfViewBinderWrapperControllerImpl.unsupported
+
+    override fun setShelfIcons(icons: NotificationIconContainer) {
+        if (shelfRefactor.expectEnabled()) {
+            NotificationIconContainerViewBinder.bind(icons, shelfIconsViewModel)
+            shelfIcons = icons
+        }
+    }
+
+    override fun onDensityOrFontScaleChanged(context: Context) {
+        updateIconLayoutParams(context)
+    }
+
+    /** Returns the view that represents the notification area. */
+    override fun getNotificationInnerAreaView(): View? {
+        return notificationIconArea
+    }
+
+    /**
+     * See [com.android.systemui.statusbar.policy.DarkIconDispatcher.setIconsDarkArea]. Sets the
+     * color that should be used to tint any icons in the notification area.
+     *
+     * @param tintAreas the areas in which to tint the icons, specified in screen coordinates
+     * @param darkIntensity
+     */
+    override fun onDarkChanged(tintAreas: ArrayList<Rect>, darkIntensity: Float, iconTint: Int) {
+        this.tintAreas.clear()
+        this.tintAreas.addAll(tintAreas)
+        if (DarkIconDispatcher.isInAreas(tintAreas, notificationIconArea)) {
+            this.iconTint = iconTint
+        }
+        applyNotificationIconsTint()
+    }
+
+    /** Updates the notifications with the given list of notifications to display. */
+    override fun updateNotificationIcons(entries: List<ListEntry>) {
+        notificationEntries = entries
+        updateNotificationIcons()
+    }
+
+    private fun updateStatusBarIcons() {
+        updateIconsForLayout(
+            { entry: NotificationEntry -> entry.icons.statusBarIcon },
+            notificationIcons,
+            showAmbient = false /* showAmbient */,
+            showLowPriority = showLowPriority,
+            hideDismissed = true /* hideDismissed */,
+            hideRepliedMessages = true /* hideRepliedMessages */,
+            hideCurrentMedia = false /* hideCurrentMedia */,
+            hidePulsing = false /* hidePulsing */
+        )
+    }
+
+    override fun updateAodNotificationIcons() {
+        if (aodIcons == null) {
+            return
+        }
+        updateIconsForLayout(
+            { entry: NotificationEntry -> entry.icons.aodIcon },
+            aodIcons,
+            showAmbient = false /* showAmbient */,
+            showLowPriority = true /* showLowPriority */,
+            hideDismissed = true /* hideDismissed */,
+            hideRepliedMessages = true /* hideRepliedMessages */,
+            hideCurrentMedia = true /* hideCurrentMedia */,
+            hidePulsing = bypassController.bypassEnabled /* hidePulsing */
+        )
+    }
+
+    override fun showIconIsolated(icon: StatusBarIconView?, animated: Boolean) {
+        notificationIcons!!.showIconIsolated(icon, animated)
+    }
+
+    override fun setIsolatedIconLocation(iconDrawingRect: Rect, requireStateUpdate: Boolean) {
+        notificationIcons!!.setIsolatedIconLocation(iconDrawingRect, requireStateUpdate)
+    }
+
+    override fun onDozingChanged(isDozing: Boolean) {
+        if (aodIcons == null) {
+            return
+        }
+        val animate = (dozeParameters.alwaysOn && !dozeParameters.displayNeedsBlanking)
+        aodIcons!!.setDozing(isDozing, animate, 0)
+    }
+
+    override fun setAnimationsEnabled(enabled: Boolean) {
+        animationsEnabled = enabled
+        updateAnimations()
+    }
+
+    override fun onStateChanged(newState: Int) {
+        updateAodIconsVisibility(animate = false, forceUpdate = false)
+        updateAnimations()
+    }
+
+    override fun onThemeChanged() {
+        reloadAodColor()
+        updateAodIconColors()
+    }
+
+    override fun getHeight(): Int {
+        return if (aodIcons == null) 0 else aodIcons!!.height
+    }
+
+    @VisibleForTesting
+    fun appearAodIcons() {
+        if (aodIcons == null) {
+            return
+        }
+        if (screenOffAnimationController.shouldAnimateAodIcons()) {
+            aodIcons!!.translationY = -aodIconAppearTranslation.toFloat()
+            aodIcons!!.alpha = 0f
+            animateInAodIconTranslation()
+            aodIcons!!
+                .animate()
+                .alpha(1f)
+                .setInterpolator(Interpolators.LINEAR)
+                .setDuration(AOD_ICONS_APPEAR_DURATION)
+                .start()
+        } else {
+            aodIcons!!.alpha = 1.0f
+            aodIcons!!.translationY = 0f
+        }
+    }
+
+    override fun onFullyHiddenChanged(isFullyHidden: Boolean) {
+        var animate = true
+        if (!bypassController.bypassEnabled) {
+            animate = dozeParameters.alwaysOn && !dozeParameters.displayNeedsBlanking
+            // We only want the appear animations to happen when the notifications get fully hidden,
+            // since otherwise the unhide animation overlaps
+            animate = animate and isFullyHidden
+        }
+        updateAodIconsVisibility(animate, false /* force */)
+        updateAodNotificationIcons()
+        updateAodIconColors()
+    }
+
+    override fun onPulseExpansionChanged(expandingChanged: Boolean) {
+        if (expandingChanged) {
+            updateAodIconsVisibility(animate = true, forceUpdate = false)
+        }
+    }
+
+    override fun demoCommands(): List<String> {
+        val commands = ArrayList<String>()
+        commands.add(DemoMode.COMMAND_NOTIFICATIONS)
+        return commands
+    }
+
+    override fun dispatchDemoCommand(command: String, args: Bundle) {
+        if (notificationIconArea != null) {
+            val visible = args.getString("visible")
+            val vis = if ("false" == visible) View.INVISIBLE else View.VISIBLE
+            notificationIconArea?.visibility = vis
+        }
+    }
+
+    override fun onDemoModeFinished() {
+        if (notificationIconArea != null) {
+            notificationIconArea?.visibility = View.VISIBLE
+        }
+    }
+
+    private fun inflateIconArea(inflater: LayoutInflater): View {
+        return inflater.inflate(R.layout.notification_icon_area, null)
+    }
+
+    /** Initializes the views that will represent the notification area. */
+    private fun initializeNotificationAreaViews(context: Context) {
+        reloadDimens(context)
+        val layoutInflater = LayoutInflater.from(context)
+        notificationIconArea = inflateIconArea(layoutInflater)
+        notificationIcons = notificationIconArea?.findViewById(R.id.notificationIcons)
+        NotificationIconContainerViewBinder.bind(notificationIcons!!, statusBarIconsViewModel)
+    }
+
+    private fun updateIconLayoutParams(context: Context) {
+        reloadDimens(context)
+        val params = generateIconLayoutParams()
+        for (i in 0 until notificationIcons!!.childCount) {
+            val child = notificationIcons!!.getChildAt(i)
+            child.layoutParams = params
+        }
+        if (shelfIcons != null) {
+            for (i in 0 until shelfIcons!!.childCount) {
+                val child = shelfIcons!!.getChildAt(i)
+                child.layoutParams = params
+            }
+        }
+        if (aodIcons != null) {
+            for (i in 0 until aodIcons!!.childCount) {
+                val child = aodIcons!!.getChildAt(i)
+                child.layoutParams = params
+            }
+        }
+    }
+
+    private fun generateIconLayoutParams(): FrameLayout.LayoutParams {
+        return FrameLayout.LayoutParams(
+            iconSize + 2 * iconHPadding,
+            statusBarWindowController.statusBarHeight
+        )
+    }
+
+    private fun reloadDimens(context: Context) {
+        val res = context.resources
+        iconSize = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_icon_size_sp)
+        iconHPadding = res.getDimensionPixelSize(R.dimen.status_bar_icon_horizontal_margin)
+        aodIconAppearTranslation = res.getDimensionPixelSize(R.dimen.shelf_appear_translation)
+    }
+
+    private fun shouldShowNotificationIcon(
+        entry: NotificationEntry,
+        showAmbient: Boolean,
+        showLowPriority: Boolean,
+        hideDismissed: Boolean,
+        hideRepliedMessages: Boolean,
+        hideCurrentMedia: Boolean,
+        hidePulsing: Boolean
+    ): Boolean {
+        if (!showAmbient && sectionStyleProvider.isMinimized(entry)) {
+            return false
+        }
+        if (hideCurrentMedia && entry.key == mediaManager.mediaNotificationKey) {
+            return false
+        }
+        if (!showLowPriority && sectionStyleProvider.isSilent(entry)) {
+            return false
+        }
+        if (entry.isRowDismissed && hideDismissed) {
+            return false
+        }
+        if (hideRepliedMessages && entry.isLastMessageFromReply) {
+            return false
+        }
+        // showAmbient == show in shade but not shelf
+        if (!showAmbient && entry.shouldSuppressStatusBar()) {
+            return false
+        }
+        if (
+            hidePulsing &&
+                entry.showingPulsing() &&
+                (!wakeUpCoordinator.notificationsFullyHidden || !entry.isPulseSuppressed)
+        ) {
+            return false
+        }
+        return if (bubblesOptional.isPresent && bubblesOptional.get().isBubbleExpanded(entry.key)) {
+            false
+        } else true
+    }
+
+    private fun updateNotificationIcons() {
+        Trace.beginSection("NotificationIconAreaController.updateNotificationIcons")
+        updateStatusBarIcons()
+        updateShelfIcons()
+        updateAodNotificationIcons()
+        applyNotificationIconsTint()
+        Trace.endSection()
+    }
+
+    private fun updateShelfIcons() {
+        if (shelfIcons == null) {
+            return
+        }
+        updateIconsForLayout(
+            { entry: NotificationEntry -> entry.icons.shelfIcon },
+            shelfIcons,
+            showAmbient = true,
+            showLowPriority = true,
+            hideDismissed = false,
+            hideRepliedMessages = false,
+            hideCurrentMedia = false,
+            hidePulsing = false
+        )
+    }
+
+    /**
+     * Updates the notification icons for a host layout. This will ensure that the notification host
+     * layout will have the same icons like the ones in here.
+     *
+     * @param function A function to look up an icon view based on an entry
+     * @param hostLayout which layout should be updated
+     * @param showAmbient should ambient notification icons be shown
+     * @param showLowPriority should icons from silent notifications be shown
+     * @param hideDismissed should dismissed icons be hidden
+     * @param hideRepliedMessages should messages that have been replied to be hidden
+     * @param hidePulsing should pulsing notifications be hidden
+     */
+    private fun updateIconsForLayout(
+        function: Function<NotificationEntry, StatusBarIconView?>,
+        hostLayout: NotificationIconContainer?,
+        showAmbient: Boolean,
+        showLowPriority: Boolean,
+        hideDismissed: Boolean,
+        hideRepliedMessages: Boolean,
+        hideCurrentMedia: Boolean,
+        hidePulsing: Boolean,
+    ) {
+        val toShow = ArrayList<StatusBarIconView>(notificationEntries.size)
+        // Filter out ambient notifications and notification children.
+        for (i in notificationEntries.indices) {
+            val entry = notificationEntries[i].representativeEntry
+            if (entry != null && entry.row != null) {
+                if (
+                    shouldShowNotificationIcon(
+                        entry,
+                        showAmbient,
+                        showLowPriority,
+                        hideDismissed,
+                        hideRepliedMessages,
+                        hideCurrentMedia,
+                        hidePulsing
+                    )
+                ) {
+                    val iconView = function.apply(entry)
+                    if (iconView != null) {
+                        toShow.add(iconView)
+                    }
+                }
+            }
+        }
+
+        // In case we are changing the suppression of a group, the replacement shouldn't flicker
+        // and it should just be replaced instead. We therefore look for notifications that were
+        // just replaced by the child or vice-versa to suppress this.
+        val replacingIcons = ArrayMap<String, ArrayList<StatusBarIcon>>()
+        val toRemove = ArrayList<View>()
+        for (i in 0 until hostLayout!!.childCount) {
+            val child = hostLayout.getChildAt(i) as? StatusBarIconView ?: continue
+            if (!toShow.contains(child)) {
+                var iconWasReplaced = false
+                val removedGroupKey = child.notification.groupKey
+                for (j in toShow.indices) {
+                    val candidate = toShow[j]
+                    if (
+                        candidate.sourceIcon.sameAs(child.sourceIcon) &&
+                            candidate.notification.groupKey == removedGroupKey
+                    ) {
+                        if (!iconWasReplaced) {
+                            iconWasReplaced = true
+                        } else {
+                            iconWasReplaced = false
+                            break
+                        }
+                    }
+                }
+                if (iconWasReplaced) {
+                    var statusBarIcons = replacingIcons[removedGroupKey]
+                    if (statusBarIcons == null) {
+                        statusBarIcons = ArrayList()
+                        replacingIcons[removedGroupKey] = statusBarIcons
+                    }
+                    statusBarIcons.add(child.statusBarIcon)
+                }
+                toRemove.add(child)
+            }
+        }
+        // removing all duplicates
+        val duplicates = ArrayList<String?>()
+        for (key in replacingIcons.keys) {
+            val statusBarIcons = replacingIcons[key]!!
+            if (statusBarIcons.size != 1) {
+                duplicates.add(key)
+            }
+        }
+        replacingIcons.removeAll(duplicates)
+        hostLayout.setReplacingIcons(replacingIcons)
+        val toRemoveCount = toRemove.size
+        for (i in 0 until toRemoveCount) {
+            hostLayout.removeView(toRemove[i])
+        }
+        val params = generateIconLayoutParams()
+        for (i in toShow.indices) {
+            val v = toShow[i]
+            // The view might still be transiently added if it was just removed and added again
+            hostLayout.removeTransientView(v)
+            if (v.parent == null) {
+                if (hideDismissed) {
+                    v.setOnDismissListener(updateStatusBarIcons)
+                }
+                hostLayout.addView(v, i, params)
+            }
+        }
+        hostLayout.setChangingViewPositions(true)
+        // Re-sort notification icons
+        val childCount = hostLayout.childCount
+        for (i in 0 until childCount) {
+            val actual = hostLayout.getChildAt(i)
+            val expected = toShow[i]
+            if (actual === expected) {
+                continue
+            }
+            hostLayout.removeView(expected)
+            hostLayout.addView(expected, i)
+        }
+        hostLayout.setChangingViewPositions(false)
+        hostLayout.setReplacingIcons(null)
+    }
+
+    /** Applies [.mIconTint] to the notification icons. */
+    private fun applyNotificationIconsTint() {
+        for (i in 0 until notificationIcons!!.childCount) {
+            val iv = notificationIcons!!.getChildAt(i) as StatusBarIconView
+            if (iv.width != 0) {
+                updateTintForIcon(iv, iconTint)
+            } else {
+                iv.executeOnLayout { updateTintForIcon(iv, iconTint) }
+            }
+        }
+        updateAodIconColors()
+    }
+
+    private fun updateTintForIcon(v: StatusBarIconView, tint: Int) {
+        val isPreL = java.lang.Boolean.TRUE == v.getTag(R.id.icon_is_pre_L)
+        var color = StatusBarIconView.NO_COLOR
+        val colorize = !isPreL || NotificationUtils.isGrayscale(v, contrastColorUtil)
+        if (colorize) {
+            color = DarkIconDispatcher.getTint(tintAreas, v, tint)
+        }
+        v.staticDrawableColor = color
+        v.setDecorColor(tint)
+    }
+
+    private fun updateAnimations() {
+        val inShade = statusBarStateController.state == StatusBarState.SHADE
+        if (aodIcons != null) {
+            aodIcons!!.setAnimationsEnabled(animationsEnabled && !inShade)
+        }
+        notificationIcons!!.setAnimationsEnabled(animationsEnabled && inShade)
+    }
+
+    private fun animateInAodIconTranslation() {
+        aodIcons!!
+            .animate()
+            .setInterpolator(Interpolators.DECELERATE_QUINT)
+            .translationY(0f)
+            .setDuration(AOD_ICONS_APPEAR_DURATION)
+            .start()
+    }
+
+    private fun reloadAodColor() {
+        aodIconTint =
+            Utils.getColorAttrDefaultColor(
+                context,
+                R.attr.wallpaperTextColor,
+                DEFAULT_AOD_ICON_COLOR
+            )
+    }
+
+    private fun updateAodIconColors() {
+        if (aodIcons != null) {
+            for (i in 0 until aodIcons!!.childCount) {
+                val iv = aodIcons!!.getChildAt(i) as StatusBarIconView
+                if (iv.width != 0) {
+                    updateTintForIcon(iv, aodIconTint)
+                } else {
+                    iv.executeOnLayout { updateTintForIcon(iv, aodIconTint) }
+                }
+            }
+        }
+    }
+
+    private fun updateAodIconsVisibility(animate: Boolean, forceUpdate: Boolean) {
+        if (aodIcons == null) {
+            return
+        }
+        var visible = (bypassController.bypassEnabled || wakeUpCoordinator.notificationsFullyHidden)
+
+        // Hide the AOD icons if we're not in the KEYGUARD state unless the screen off animation is
+        // playing, in which case we want them to be visible since we're animating in the AOD UI and
+        // will be switching to KEYGUARD shortly.
+        if (
+            statusBarStateController.state != StatusBarState.KEYGUARD &&
+                !screenOffAnimationController.shouldShowAodIconsWhenShade()
+        ) {
+            visible = false
+        }
+        if (visible && wakeUpCoordinator.isPulseExpanding() && !bypassController.bypassEnabled) {
+            visible = false
+        }
+        if (aodIconsVisible != visible || forceUpdate) {
+            aodIconsVisible = visible
+            aodIcons!!.animate().cancel()
+            if (animate) {
+                val wasFullyInvisible = aodIcons!!.visibility != View.VISIBLE
+                if (aodIconsVisible) {
+                    if (wasFullyInvisible) {
+                        // No fading here, let's just appear the icons instead!
+                        aodIcons!!.visibility = View.VISIBLE
+                        aodIcons!!.alpha = 1.0f
+                        appearAodIcons()
+                    } else {
+                        // Let's make sure the icon are translated to 0, since we cancelled it above
+                        animateInAodIconTranslation()
+                        // We were fading out, let's fade in instead
+                        CrossFadeHelper.fadeIn(aodIcons)
+                    }
+                } else {
+                    // Let's make sure the icon are translated to 0, since we cancelled it above
+                    animateInAodIconTranslation()
+                    CrossFadeHelper.fadeOut(aodIcons)
+                }
+            } else {
+                aodIcons!!.alpha = 1.0f
+                aodIcons!!.translationY = 0f
+                aodIcons!!.visibility = if (visible) View.VISIBLE else View.INVISIBLE
+            }
+        }
+    }
+
+    companion object {
+        private const val AOD_ICONS_APPEAR_DURATION: Long = 200
+
+        @ColorInt private val DEFAULT_AOD_ICON_COLOR = -0x1
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
new file mode 100644
index 0000000..8293bb3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.icon.ui.viewbinder
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel
+import com.android.systemui.statusbar.phone.NotificationIconContainer
+import kotlinx.coroutines.DisposableHandle
+
+/** Binds a [NotificationIconContainer] to its [view model][NotificationIconContainerViewModel]. */
+object NotificationIconContainerViewBinder {
+    fun bind(
+        view: NotificationIconContainer,
+        viewModel: NotificationIconContainerViewModel,
+    ): DisposableHandle {
+        return view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) {} }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt
new file mode 100644
index 0000000..f68b0ef
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.icon.ui.viewmodel
+
+import javax.inject.Inject
+
+/** View-model for the row of notification icons displayed on the always-on display. */
+class NotificationIconContainerAlwaysOnDisplayViewModel @Inject constructor() :
+    NotificationIconContainerViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt
new file mode 100644
index 0000000..933c76f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.icon.ui.viewmodel
+
+import javax.inject.Inject
+
+/** View-model for the overflow row of notification icons displayed in the notification shade. */
+class NotificationIconContainerShelfViewModel @Inject constructor() :
+    NotificationIconContainerViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
new file mode 100644
index 0000000..2217646
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.icon.ui.viewmodel
+
+import javax.inject.Inject
+
+/** View-model for the row of notification icons displayed in the status bar, */
+class NotificationIconContainerStatusBarViewModel @Inject constructor() :
+    NotificationIconContainerViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerViewModel.kt
new file mode 100644
index 0000000..892b2be
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerViewModel.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.icon.ui.viewmodel
+
+/**
+ * View-model for the row of notification icons displayed in the NotificationShelf, StatusBar, and
+ * AOD.
+ */
+interface NotificationIconContainerViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
index 22a87a7..b92c51f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
@@ -64,8 +64,10 @@
 
     override fun setOnClickListener(listener: View.OnClickListener) = unsupported
 
-    private val unsupported: Nothing
-        get() = error("Code path not supported when NOTIFICATION_SHELF_REFACTOR is disabled")
+    companion object {
+        val unsupported: Nothing
+            get() = error("Code path not supported when NOTIFICATION_SHELF_REFACTOR is disabled")
+    }
 }
 
 /** Binds a [NotificationShelf] to its [view model][NotificationShelfViewModel]. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 4668aa4..6db8df9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -28,7 +28,6 @@
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_GENTLE;
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_HIGH_PRIORITY;
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.SelectedRows;
-import static com.android.systemui.statusbar.phone.NotificationIconAreaController.HIGH_PRIORITY;
 import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
 
 import android.content.res.Configuration;
@@ -150,6 +149,7 @@
 public class NotificationStackScrollLayoutController {
     private static final String TAG = "StackScrollerController";
     private static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG);
+    private static final String HIGH_PRIORITY = "high_priority";
 
     private final boolean mAllowLongPress;
     private final NotificationGutsManager mNotificationGutsManager;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
index 5c28be3..af09bf2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
@@ -25,7 +25,6 @@
 import android.content.pm.PackageManager;
 import android.os.Bundle;
 import android.os.UserHandle;
-import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.RemoteAnimationAdapter;
 import android.view.View;
@@ -276,19 +275,11 @@
 
     void userActivity();
 
-    boolean interceptMediaKey(KeyEvent event);
-
-    boolean dispatchKeyEventPreIme(KeyEvent event);
-
-    boolean onMenuPressed();
-
     void endAffordanceLaunch();
 
     /** Should the keyguard be hidden immediately in response to a back press/gesture. */
     boolean shouldKeyguardHideImmediately();
 
-    boolean onSpacePressed();
-
     void showBouncerWithDimissAndCancelIfKeyguard(OnDismissAction performAction,
             Runnable cancelAction);
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 8ffd43a..ccb87bf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -89,7 +89,6 @@
 import android.view.Display;
 import android.view.IRemoteAnimationRunner;
 import android.view.IWindowManager;
-import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.ThreadedRenderer;
 import android.view.View;
@@ -2636,44 +2635,6 @@
     }
 
     @Override
-    public boolean interceptMediaKey(KeyEvent event) {
-        return mState == StatusBarState.KEYGUARD
-                && mStatusBarKeyguardViewManager.interceptMediaKey(event);
-    }
-
-    /**
-     * While IME is active and a BACK event is detected, check with
-     * {@link StatusBarKeyguardViewManager#dispatchBackKeyEventPreIme()} to see if the event
-     * should be handled before routing to IME, in order to prevent the user having to hit back
-     * twice to exit bouncer.
-     */
-    @Override
-    public boolean dispatchKeyEventPreIme(KeyEvent event) {
-        switch (event.getKeyCode()) {
-            case KeyEvent.KEYCODE_BACK:
-                if (mState == StatusBarState.KEYGUARD
-                        && mStatusBarKeyguardViewManager.dispatchBackKeyEventPreIme()) {
-                    return mBackActionInteractor.onBackRequested();
-                }
-        }
-        return false;
-    }
-
-    protected boolean shouldUnlockOnMenuPressed() {
-        return mDeviceInteractive && mState != StatusBarState.SHADE
-            && mStatusBarKeyguardViewManager.shouldDismissOnMenuPressed();
-    }
-
-    @Override
-    public boolean onMenuPressed() {
-        if (shouldUnlockOnMenuPressed()) {
-            mShadeController.animateCollapseShadeForced();
-            return true;
-        }
-        return false;
-    }
-
-    @Override
     public void endAffordanceLaunch() {
         releaseGestureWakeLock();
         mCameraLauncherLazy.get().setLaunchingAffordance(false);
@@ -2692,15 +2653,6 @@
         return (isScrimmedBouncer || isBouncerOverDream);
     }
 
-    @Override
-    public boolean onSpacePressed() {
-        if (mDeviceInteractive && mState != StatusBarState.SHADE) {
-            mShadeController.animateCollapseShadeForced();
-            return true;
-        }
-        return false;
-    }
-
     private void showBouncerOrLockScreenIfKeyguard() {
         // If the keyguard is animating away, we aren't really the keyguard anymore and should not
         // show the bouncer/lockscreen.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
similarity index 96%
rename from packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
rename to packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
index 0bf0f4b..d22ed38 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package com.android.systemui.statusbar.phone;
 
 import android.content.Context;
@@ -43,6 +58,8 @@
 import com.android.systemui.statusbar.window.StatusBarWindowController;
 import com.android.wm.shell.bubbles.Bubbles;
 
+import org.jetbrains.annotations.NotNull;
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
@@ -55,13 +72,13 @@
  * normally reserved for notifications.
  */
 @SysUISingleton
-public class NotificationIconAreaController implements
+public class LegacyNotificationIconAreaControllerImpl implements
+        NotificationIconAreaController,
         DarkReceiver,
         StatusBarStateController.StateListener,
         NotificationWakeUpCoordinator.WakeUpListener,
         DemoMode {
 
-    public static final String HIGH_PRIORITY = "high_priority";
     private static final long AOD_ICONS_APPEAR_DURATION = 200;
     @ColorInt
     private static final int DEFAULT_AOD_ICON_COLOR = 0xffffffff;
@@ -110,7 +127,7 @@
             };
 
     @Inject
-    public NotificationIconAreaController(
+    public LegacyNotificationIconAreaControllerImpl(
             Context context,
             StatusBarStateController statusBarStateController,
             NotificationWakeUpCoordinator wakeUpCoordinator,
@@ -192,7 +209,7 @@
         }
     }
 
-    public void onDensityOrFontScaleChanged(Context context) {
+    public void onDensityOrFontScaleChanged(@NotNull Context context) {
         updateIconLayoutParams(context);
     }
 
@@ -493,7 +510,7 @@
         mNotificationIcons.showIconIsolated(icon, animated);
     }
 
-    public void setIsolatedIconLocation(Rect iconDrawingRect, boolean requireStateUpdate) {
+    public void setIsolatedIconLocation(@NotNull Rect iconDrawingRect, boolean requireStateUpdate) {
         mNotificationIcons.setIsolatedIconLocation(iconDrawingRect, requireStateUpdate);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.kt
new file mode 100644
index 0000000..0079f7c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.phone
+
+import android.content.Context
+import android.graphics.Rect
+import android.view.View
+import com.android.systemui.statusbar.NotificationShelfController
+import com.android.systemui.statusbar.StatusBarIconView
+import com.android.systemui.statusbar.notification.collection.ListEntry
+
+/**
+ * A controller for the space in the status bar to the left of the system icons. This area is
+ * normally reserved for notifications.
+ */
+interface NotificationIconAreaController {
+    /** Called by the Keyguard*ViewController whose view contains the aod icons. */
+    fun setupAodIcons(aodIcons: NotificationIconContainer)
+    fun setupShelf(notificationShelfController: NotificationShelfController)
+    fun setShelfIcons(icons: NotificationIconContainer)
+    fun onDensityOrFontScaleChanged(context: Context)
+
+    /** Returns the view that represents the notification area. */
+    fun getNotificationInnerAreaView(): View?
+
+    /** Updates the notifications with the given list of notifications to display. */
+    fun updateNotificationIcons(entries: List<@JvmSuppressWildcards ListEntry>)
+    fun updateAodNotificationIcons()
+    fun showIconIsolated(icon: StatusBarIconView?, animated: Boolean)
+    fun setIsolatedIconLocation(iconDrawingRect: Rect, requireStateUpdate: Boolean)
+    fun setAnimationsEnabled(enabled: Boolean)
+    fun onThemeChanged()
+    fun getHeight(): Int
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaControllerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaControllerModule.kt
new file mode 100644
index 0000000..d1ddd51
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaControllerModule.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.phone
+
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconAreaControllerViewBinderWrapperImpl
+import dagger.Module
+import dagger.Provides
+import javax.inject.Provider
+
+@Module
+object NotificationIconAreaControllerModule {
+    @Provides
+    fun provideNotificationIconAreaControllerImpl(
+        featureFlags: FeatureFlags,
+        legacyProvider: Provider<LegacyNotificationIconAreaControllerImpl>,
+        newProvider: Provider<NotificationIconAreaControllerViewBinderWrapperImpl>,
+    ): NotificationIconAreaController =
+        if (featureFlags.isEnabled(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR)) {
+            newProvider.get()
+        } else {
+            legacyProvider.get()
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index fc66138..62a8cfd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -709,6 +709,11 @@
         }
     }
 
+    public void onUnlockAnimationFinished() {
+        mAnimatingPanelExpansionOnUnlock = false;
+        applyAndDispatchState();
+    }
+
     /**
      * Set the amount of progress we are currently in if we're transitioning to the full shade.
      * 0.0f means we're not transitioning yet, while 1 means we're all the way in the full
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
index 7fe01825..a6284e3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
@@ -32,6 +32,8 @@
 import android.view.View;
 import android.view.ViewParent;
 
+import androidx.annotation.Nullable;
+
 import com.android.systemui.ActivityIntentHelper;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -254,16 +256,16 @@
 
     @Override
     public boolean handleRemoteViewClick(View view, PendingIntent pendingIntent,
-            boolean appRequestedAuth,
+            boolean appRequestedAuth, @Nullable Integer actionIndex,
             NotificationRemoteInputManager.ClickHandler defaultHandler) {
         final boolean isActivity = pendingIntent.isActivity();
         if (isActivity || appRequestedAuth) {
-            mActionClickLogger.logWaitingToCloseKeyguard(pendingIntent);
+            mActionClickLogger.logWaitingToCloseKeyguard(pendingIntent, actionIndex);
             final boolean afterKeyguardGone = mActivityIntentHelper
                     .wouldPendingLaunchResolverActivity(pendingIntent,
                             mLockscreenUserManager.getCurrentUserId());
             mActivityStarter.dismissKeyguardThenExecute(() -> {
-                mActionClickLogger.logKeyguardGone(pendingIntent);
+                mActionClickLogger.logKeyguardGone(pendingIntent, actionIndex);
 
                 try {
                     ActivityManager.getService().resumeAppSwitches();
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
index a9aed2f..b5317fa 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
@@ -620,6 +620,51 @@
         configurationListenerArgumentCaptor.value.onUiModeChanged()
         verify(view).reloadColors()
     }
+    @Test
+    fun onOrientationChanged_landscapeKeyguardFlagDisabled_blockReinflate() {
+        featureFlags.set(Flags.LOCKSCREEN_ENABLE_LANDSCAPE, false)
+
+        // Run onOrientationChanged
+        val configurationListenerArgumentCaptor =
+            ArgumentCaptor.forClass(ConfigurationController.ConfigurationListener::class.java)
+        underTest.onViewAttached()
+        verify(configurationController).addCallback(configurationListenerArgumentCaptor.capture())
+        clearInvocations(viewFlipperController)
+        configurationListenerArgumentCaptor.value.onOrientationChanged(
+            Configuration.ORIENTATION_LANDSCAPE
+        )
+        // Verify view is reinflated when flag is on
+        verify(viewFlipperController, never()).clearViews()
+        verify(viewFlipperController, never())
+            .asynchronouslyInflateView(
+                eq(SecurityMode.PIN),
+                any(),
+                onViewInflatedCallbackArgumentCaptor.capture()
+            )
+    }
+
+    @Test
+    fun onOrientationChanged_landscapeKeyguardFlagEnabled_doesReinflate() {
+        featureFlags.set(Flags.LOCKSCREEN_ENABLE_LANDSCAPE, true)
+
+        // Run onOrientationChanged
+        val configurationListenerArgumentCaptor =
+            ArgumentCaptor.forClass(ConfigurationController.ConfigurationListener::class.java)
+        underTest.onViewAttached()
+        verify(configurationController).addCallback(configurationListenerArgumentCaptor.capture())
+        clearInvocations(viewFlipperController)
+        configurationListenerArgumentCaptor.value.onOrientationChanged(
+            Configuration.ORIENTATION_LANDSCAPE
+        )
+        // Verify view is reinflated when flag is on
+        verify(viewFlipperController).clearViews()
+        verify(viewFlipperController)
+            .asynchronouslyInflateView(
+                eq(SecurityMode.PIN),
+                any(),
+                onViewInflatedCallbackArgumentCaptor.capture()
+            )
+    }
 
     @Test
     fun hasDismissActions() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt
index a3f7fc5..e0ae0c3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt
@@ -131,14 +131,12 @@
     }
 
     @Test
-    fun dispatchKeyEvent_menuActionUp_interactiveKeyguard_collapsesShade() {
+    fun dispatchKeyEvent_menuActionUp_interactiveKeyguard_showsPrimaryBouncer() {
         keyguardInteractorWithDependencies.repository.setWakefulnessModel(awakeWakefulnessMode)
         whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
         whenever(statusBarKeyguardViewManager.shouldDismissOnMenuPressed()).thenReturn(true)
 
-        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MENU)
-        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue()
-        verify(shadeController).animateCollapseShadeForced()
+        verifyActionUpShowsPrimaryBouncer(KeyEvent.KEYCODE_MENU)
     }
 
     @Test
@@ -147,42 +145,48 @@
         whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED)
         whenever(statusBarKeyguardViewManager.shouldDismissOnMenuPressed()).thenReturn(true)
 
-        // action down: does NOT collapse the shade
-        val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MENU)
-        assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse()
-        verify(shadeController, never()).animateCollapseShadeForced()
-
-        // action up: collapses the shade
-        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MENU)
-        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue()
-        verify(shadeController).animateCollapseShadeForced()
+        verifyActionUpCollapsesTheShade(KeyEvent.KEYCODE_MENU)
     }
 
     @Test
-    fun dispatchKeyEvent_menuActionUp_nonInteractiveKeyguard_neverCollapsesShade() {
+    fun dispatchKeyEvent_menuActionUp_nonInteractiveKeyguard_doNothing() {
         keyguardInteractorWithDependencies.repository.setWakefulnessModel(asleepWakefulnessMode)
         whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
         whenever(statusBarKeyguardViewManager.shouldDismissOnMenuPressed()).thenReturn(true)
 
-        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MENU)
-        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isFalse()
-        verify(shadeController, never()).animateCollapseShadeForced()
+        verifyActionsDoNothing(KeyEvent.KEYCODE_MENU)
     }
 
     @Test
-    fun dispatchKeyEvent_spaceActionUp_interactiveKeyguard_collapsesShade() {
+    fun dispatchKeyEvent_spaceActionUp_interactiveKeyguard_showsPrimaryBouncer() {
         keyguardInteractorWithDependencies.repository.setWakefulnessModel(awakeWakefulnessMode)
         whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
 
-        // action down: does NOT collapse the shade
-        val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SPACE)
-        assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse()
-        verify(shadeController, never()).animateCollapseShadeForced()
+        verifyActionUpShowsPrimaryBouncer(KeyEvent.KEYCODE_SPACE)
+    }
 
-        // action up: collapses the shade
-        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_SPACE)
-        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue()
-        verify(shadeController).animateCollapseShadeForced()
+    @Test
+    fun dispatchKeyEvent_spaceActionUp_shadeLocked_collapsesShade() {
+        keyguardInteractorWithDependencies.repository.setWakefulnessModel(awakeWakefulnessMode)
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED)
+
+        verifyActionUpCollapsesTheShade(KeyEvent.KEYCODE_SPACE)
+    }
+
+    @Test
+    fun dispatchKeyEvent_enterActionUp_interactiveKeyguard_showsPrimaryBouncer() {
+        keyguardInteractorWithDependencies.repository.setWakefulnessModel(awakeWakefulnessMode)
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+
+        verifyActionUpShowsPrimaryBouncer(KeyEvent.KEYCODE_ENTER)
+    }
+
+    @Test
+    fun dispatchKeyEvent_enterActionUp_shadeLocked_collapsesShade() {
+        keyguardInteractorWithDependencies.repository.setWakefulnessModel(awakeWakefulnessMode)
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED)
+
+        verifyActionUpCollapsesTheShade(KeyEvent.KEYCODE_ENTER)
     }
 
     @Test
@@ -252,4 +256,42 @@
             .isFalse()
         verify(statusBarKeyguardViewManager, never()).interceptMediaKey(any())
     }
+
+    private fun verifyActionUpCollapsesTheShade(keycode: Int) {
+        // action down: does NOT collapse the shade
+        val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, keycode)
+        assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse()
+        verify(shadeController, never()).animateCollapseShadeForced()
+
+        // action up: collapses the shade
+        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, keycode)
+        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue()
+        verify(shadeController).animateCollapseShadeForced()
+    }
+
+    private fun verifyActionUpShowsPrimaryBouncer(keycode: Int) {
+        // action down: does NOT collapse the shade
+        val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, keycode)
+        assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse()
+        verify(statusBarKeyguardViewManager, never()).showPrimaryBouncer(any())
+
+        // action up: collapses the shade
+        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, keycode)
+        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue()
+        verify(statusBarKeyguardViewManager).showPrimaryBouncer(eq(true))
+    }
+
+    private fun verifyActionsDoNothing(keycode: Int) {
+        // action down: does nothing
+        val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, keycode)
+        assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse()
+        verify(shadeController, never()).animateCollapseShadeForced()
+        verify(statusBarKeyguardViewManager, never()).showPrimaryBouncer(any())
+
+        // action up: doesNothing
+        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, keycode)
+        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isFalse()
+        verify(shadeController, never()).animateCollapseShadeForced()
+        verify(statusBarKeyguardViewManager, never()).showPrimaryBouncer(any())
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index eb4ae1a..7aeafeb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -539,6 +539,23 @@
     }
 
     @Test
+    public void onKeyguardStatusViewHeightChange_animatesNextTopPaddingChangeForNSSL() {
+        ArgumentCaptor<View.OnLayoutChangeListener> captor =
+                ArgumentCaptor.forClass(View.OnLayoutChangeListener.class);
+        verify(mKeyguardStatusView).addOnLayoutChangeListener(captor.capture());
+        View.OnLayoutChangeListener listener = captor.getValue();
+
+        clearInvocations(mNotificationStackScrollLayoutController);
+
+        when(mKeyguardStatusView.getHeight()).thenReturn(0);
+        listener.onLayoutChange(mKeyguardStatusView, /* left= */ 0, /* top= */ 0, /* right= */
+                0, /* bottom= */ 0, /* oldLeft= */ 0, /* oldTop= */ 0, /* oldRight= */
+                0, /* oldBottom = */ 200);
+
+        verify(mNotificationStackScrollLayoutController).animateNextTopPaddingChange();
+    }
+
+    @Test
     public void testCanCollapsePanelOnTouch_trueForKeyGuard() {
         mStatusBarStateController.setState(KEYGUARD);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index 1edeeff..dc506a5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -18,6 +18,7 @@
 
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
+import android.view.KeyEvent
 import android.view.MotionEvent
 import android.view.ViewGroup
 import androidx.test.filters.SmallTest
@@ -38,6 +39,7 @@
 import com.android.systemui.dump.logcatLogBuffer
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
+import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.TransitionStep
@@ -118,6 +120,7 @@
     @Mock lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
     @Mock
     lateinit var primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel
+    @Mock lateinit var keyEventInteractor: KeyEventInteractor
     private val notificationExpansionRepository = NotificationExpansionRepository()
 
     private lateinit var interactionEventHandlerCaptor: ArgumentCaptor<InteractionEventHandler>
@@ -188,7 +191,8 @@
                         CountDownTimerUtil(),
                         featureFlags
                     ),
-                    BouncerLogger(logcatLogBuffer("BouncerLog"))
+                    BouncerLogger(logcatLogBuffer("BouncerLog")),
+                    keyEventInteractor,
             )
         underTest.setupExpandedStatusBar()
 
@@ -345,6 +349,27 @@
             verify(view).findViewById<ViewGroup>(R.id.keyguard_message_area)
         }
 
+    @Test
+    fun forwardsDispatchKeyEvent() {
+        val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_B)
+        interactionEventHandler.dispatchKeyEvent(keyEvent)
+        verify(keyEventInteractor).dispatchKeyEvent(keyEvent)
+    }
+
+    @Test
+    fun forwardsDispatchKeyEventPreIme() {
+        val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_B)
+        interactionEventHandler.dispatchKeyEventPreIme(keyEvent)
+        verify(keyEventInteractor).dispatchKeyEventPreIme(keyEvent)
+    }
+
+    @Test
+    fun forwardsInterceptMediaKey() {
+        val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_VOLUME_UP)
+        interactionEventHandler.interceptMediaKey(keyEvent)
+        verify(keyEventInteractor).interceptMediaKey(keyEvent)
+    }
+
     companion object {
         private val DOWN_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
         private const val VIEW_BOTTOM = 100
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
index 829184c..66d48d6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
@@ -38,6 +38,7 @@
 import com.android.systemui.dump.logcatLogBuffer
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
+import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
@@ -198,7 +199,8 @@
                     CountDownTimerUtil(),
                     featureFlags
                 ),
-                BouncerLogger(logcatLogBuffer("BouncerLog"))
+                BouncerLogger(logcatLogBuffer("BouncerLog")),
+                Mockito.mock(KeyEventInteractor::class.java),
             )
 
         controller.setupExpandedStatusBar()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
index 9495fdd..ecaf137 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
@@ -227,6 +227,25 @@
     }
 
     @Test
+    fun activeClockId_changeAfterPluginConnected() {
+        val plugin1 = FakeClockPlugin()
+            .addClock("clock_1", "clock 1")
+            .addClock("clock_2", "clock 2")
+
+        val plugin2 = FakeClockPlugin()
+            .addClock("clock_3", "clock 3", { mockClock })
+            .addClock("clock_4", "clock 4")
+
+        registry.applySettings(ClockSettings("clock_3", null))
+
+        pluginListener.onPluginLoaded(plugin1, mockContext, mockPluginLifecycle)
+        assertEquals(DEFAULT_CLOCK_ID, registry.activeClockId)
+
+        pluginListener.onPluginLoaded(plugin2, mockContext, mockPluginLifecycle)
+        assertEquals("clock_3", registry.activeClockId)
+    }
+
+    @Test
     fun createDefaultClock_pluginDisconnected() {
         val plugin1 = FakeClockPlugin()
             .addClock("clock_1", "clock 1")
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImplTest.kt
new file mode 100644
index 0000000..b8792a8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImplTest.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.icon.ui.viewbinder
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.plugins.DarkIconDispatcher
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.NotificationListener
+import com.android.systemui.statusbar.NotificationMediaManager
+import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
+import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerShelfViewModel
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel
+import com.android.systemui.statusbar.phone.DozeParameters
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.phone.NotificationIconContainer
+import com.android.systemui.statusbar.phone.ScreenOffAnimationController
+import com.android.systemui.statusbar.window.StatusBarWindowController
+import com.android.systemui.util.mockito.whenever
+import com.android.wm.shell.bubbles.Bubbles
+import java.util.Optional
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper(setAsMainLooper = true)
+class NotificationIconAreaControllerViewBinderWrapperImplTest : SysuiTestCase() {
+    @Mock private lateinit var notifListener: NotificationListener
+    @Mock private lateinit var statusBarStateController: StatusBarStateController
+    @Mock private lateinit var wakeUpCoordinator: NotificationWakeUpCoordinator
+    @Mock private lateinit var keyguardBypassController: KeyguardBypassController
+    @Mock private lateinit var notifMediaManager: NotificationMediaManager
+    @Mock private lateinit var dozeParams: DozeParameters
+    @Mock private lateinit var sectionStyleProvider: SectionStyleProvider
+    @Mock private lateinit var darkIconDispatcher: DarkIconDispatcher
+    @Mock private lateinit var statusBarWindowController: StatusBarWindowController
+    @Mock private lateinit var screenOffAnimController: ScreenOffAnimationController
+    @Mock private lateinit var bubbles: Bubbles
+    @Mock private lateinit var demoModeController: DemoModeController
+    @Mock private lateinit var aodIcons: NotificationIconContainer
+    @Mock private lateinit var featureFlags: FeatureFlags
+
+    private val shelfViewModel = NotificationIconContainerShelfViewModel()
+    private val statusBarViewModel = NotificationIconContainerStatusBarViewModel()
+    private val aodViewModel = NotificationIconContainerAlwaysOnDisplayViewModel()
+
+    private lateinit var underTest: NotificationIconAreaControllerViewBinderWrapperImpl
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        underTest =
+            NotificationIconAreaControllerViewBinderWrapperImpl(
+                mContext,
+                statusBarStateController,
+                wakeUpCoordinator,
+                keyguardBypassController,
+                notifMediaManager,
+                notifListener,
+                dozeParams,
+                sectionStyleProvider,
+                Optional.of(bubbles),
+                demoModeController,
+                darkIconDispatcher,
+                featureFlags,
+                statusBarWindowController,
+                screenOffAnimController,
+                shelfViewModel,
+                statusBarViewModel,
+                aodViewModel,
+            )
+    }
+
+    @Test
+    fun testNotificationIcons_settingHideIcons() {
+        underTest.settingsListener.onStatusBarIconsBehaviorChanged(true)
+        assertFalse(underTest.shouldShowLowPriorityIcons())
+    }
+
+    @Test
+    fun testNotificationIcons_settingShowIcons() {
+        underTest.settingsListener.onStatusBarIconsBehaviorChanged(false)
+        assertTrue(underTest.shouldShowLowPriorityIcons())
+    }
+
+    @Test
+    fun testAppearResetsTranslation() {
+        underTest.setupAodIcons(aodIcons)
+        whenever(dozeParams.shouldControlScreenOff()).thenReturn(false)
+        underTest.appearAodIcons()
+        verify(aodIcons).translationY = 0f
+        verify(aodIcons).alpha = 1.0f
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationIconAreaControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java
similarity index 94%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationIconAreaControllerTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java
index 8e1dcf0..1b8cfd4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationIconAreaControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java
@@ -48,7 +48,7 @@
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
-public class NotificationIconAreaControllerTest extends SysuiTestCase {
+public class LegacyNotificationIconAreaControllerImplTest extends SysuiTestCase {
 
     @Mock
     private NotificationListener mListener;
@@ -70,7 +70,7 @@
     StatusBarWindowController mStatusBarWindowController;
     @Mock
     ScreenOffAnimationController mScreenOffAnimationController;
-    private NotificationIconAreaController mController;
+    private LegacyNotificationIconAreaControllerImpl mController;
     @Mock
     private Bubbles mBubbles;
     @Mock private DemoModeController mDemoModeController;
@@ -82,7 +82,7 @@
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
-        mController = new NotificationIconAreaController(
+        mController = new LegacyNotificationIconAreaControllerImpl(
                 mContext,
                 mStatusBarStateController,
                 mWakeUpCoordinator,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
index 0dc1d9a..6b3bd22 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
@@ -1802,6 +1802,15 @@
         assertFalse(ScrimState.UNLOCKED.mAnimateChange);
     }
 
+    @Test
+    public void testNotifScrimAlpha_1f_afterUnlockFinishedAndExpanded() {
+        mScrimController.transitionTo(ScrimState.KEYGUARD);
+        when(mKeyguardUnlockAnimationController.isPlayingCannedUnlockAnimation()).thenReturn(true);
+        mScrimController.transitionTo(ScrimState.UNLOCKED);
+        mScrimController.onUnlockAnimationFinished();
+        assertAlphaAfterExpansion(mNotificationsScrim, 1f, 1f);
+    }
+
     private void assertAlphaAfterExpansion(ScrimView scrim, float expectedAlpha, float expansion) {
         mScrimController.setRawPanelExpansionFraction(expansion);
         finishAnimationsImmediately();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index 21e4f5a..a6a2761 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -32,7 +32,6 @@
 import static org.junit.Assume.assumeNotNull;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
@@ -480,123 +479,45 @@
 
     @Test
     public void ifPortraitHalfOpen_drawVerticallyTop() {
-        DevicePostureController devicePostureController = mock(DevicePostureController.class);
-        when(devicePostureController.getDevicePosture())
-                .thenReturn(DevicePostureController.DEVICE_POSTURE_CLOSED);
-
-        VolumeDialogImpl dialog = new VolumeDialogImpl(
-                getContext(),
-                mVolumeDialogController,
-                mAccessibilityMgr,
-                mDeviceProvisionedController,
-                mConfigurationController,
-                mMediaOutputDialogFactory,
-                mVolumePanelFactory,
-                mActivityStarter,
-                mInteractionJankMonitor,
-                false,
-                mCsdWarningDialogFactory,
-                devicePostureController,
-                mTestableLooper.getLooper(),
-                mDumpManager,
-                mFeatureFlags
-        );
-        dialog.init(0 , null);
-
-        verify(devicePostureController).addCallback(any());
-        dialog.onPostureChanged(DevicePostureController.DEVICE_POSTURE_HALF_OPENED);
+        mDialog.onPostureChanged(DevicePostureController.DEVICE_POSTURE_HALF_OPENED);
         mTestableLooper.processAllMessages(); // let dismiss() finish
 
         setOrientation(Configuration.ORIENTATION_PORTRAIT);
 
         // Call show() to trigger layout updates before verifying position
-        dialog.show(SHOW_REASON_UNKNOWN);
+        mDialog.show(SHOW_REASON_UNKNOWN);
         mTestableLooper.processAllMessages(); // let show() finish before assessing its side-effect
 
-        int gravity = dialog.getWindowGravity();
+        int gravity = mDialog.getWindowGravity();
         assertEquals(Gravity.TOP, gravity & Gravity.VERTICAL_GRAVITY_MASK);
-
-        cleanUp(dialog);
     }
 
     @Test
     public void ifPortraitAndOpen_drawCenterVertically() {
-        DevicePostureController devicePostureController = mock(DevicePostureController.class);
-        when(devicePostureController.getDevicePosture())
-                .thenReturn(DevicePostureController.DEVICE_POSTURE_CLOSED);
-
-        VolumeDialogImpl dialog = new VolumeDialogImpl(
-                getContext(),
-                mVolumeDialogController,
-                mAccessibilityMgr,
-                mDeviceProvisionedController,
-                mConfigurationController,
-                mMediaOutputDialogFactory,
-                mVolumePanelFactory,
-                mActivityStarter,
-                mInteractionJankMonitor,
-                false,
-                mCsdWarningDialogFactory,
-                devicePostureController,
-                mTestableLooper.getLooper(),
-                mDumpManager,
-                mFeatureFlags
-        );
-        dialog.init(0, null);
-
-        verify(devicePostureController).addCallback(any());
-        dialog.onPostureChanged(DevicePostureController.DEVICE_POSTURE_OPENED);
+        mDialog.onPostureChanged(DevicePostureController.DEVICE_POSTURE_OPENED);
         mTestableLooper.processAllMessages(); // let dismiss() finish
 
         setOrientation(Configuration.ORIENTATION_PORTRAIT);
 
-        dialog.show(SHOW_REASON_UNKNOWN);
+        mDialog.show(SHOW_REASON_UNKNOWN);
         mTestableLooper.processAllMessages(); // let show() finish before assessing its side-effect
 
-        int gravity = dialog.getWindowGravity();
+        int gravity = mDialog.getWindowGravity();
         assertEquals(Gravity.CENTER_VERTICAL, gravity & Gravity.VERTICAL_GRAVITY_MASK);
-
-        cleanUp(dialog);
     }
 
     @Test
     public void ifLandscapeAndHalfOpen_drawCenterVertically() {
-        DevicePostureController devicePostureController = mock(DevicePostureController.class);
-        when(devicePostureController.getDevicePosture())
-                .thenReturn(DevicePostureController.DEVICE_POSTURE_CLOSED);
-
-        VolumeDialogImpl dialog = new VolumeDialogImpl(
-                getContext(),
-                mVolumeDialogController,
-                mAccessibilityMgr,
-                mDeviceProvisionedController,
-                mConfigurationController,
-                mMediaOutputDialogFactory,
-                mVolumePanelFactory,
-                mActivityStarter,
-                mInteractionJankMonitor,
-                false,
-                mCsdWarningDialogFactory,
-                devicePostureController,
-                mTestableLooper.getLooper(),
-                mDumpManager,
-                mFeatureFlags
-        );
-        dialog.init(0, null);
-
-        verify(devicePostureController).addCallback(any());
-        dialog.onPostureChanged(DevicePostureController.DEVICE_POSTURE_HALF_OPENED);
+        mDialog.onPostureChanged(DevicePostureController.DEVICE_POSTURE_HALF_OPENED);
         mTestableLooper.processAllMessages(); // let dismiss() finish
 
         setOrientation(Configuration.ORIENTATION_LANDSCAPE);
 
-        dialog.show(SHOW_REASON_UNKNOWN);
+        mDialog.show(SHOW_REASON_UNKNOWN);
         mTestableLooper.processAllMessages(); // let show() finish before assessing its side-effect
 
-        int gravity = dialog.getWindowGravity();
+        int gravity = mDialog.getWindowGravity();
         assertEquals(Gravity.CENTER_VERTICAL, gravity & Gravity.VERTICAL_GRAVITY_MASK);
-
-        cleanUp(dialog);
     }
 
     @Test
@@ -607,31 +528,9 @@
 
     @Test
     public void dialogDestroy_removesPostureControllerCallback() {
-        VolumeDialogImpl dialog = new VolumeDialogImpl(
-                getContext(),
-                mVolumeDialogController,
-                mAccessibilityMgr,
-                mDeviceProvisionedController,
-                mConfigurationController,
-                mMediaOutputDialogFactory,
-                mVolumePanelFactory,
-                mActivityStarter,
-                mInteractionJankMonitor,
-                false,
-                mCsdWarningDialogFactory,
-                mPostureController,
-                mTestableLooper.getLooper(),
-                mDumpManager,
-                mFeatureFlags
-        );
-        dialog.init(0, null);
-
         verify(mPostureController, never()).removeCallback(any());
-        dialog.destroy();
-
+        mDialog.destroy();
         verify(mPostureController).removeCallback(any());
-
-        cleanUp(dialog);
     }
 
     private void setOrientation(int orientation) {
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
index 315972c..f594170 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
@@ -1225,15 +1225,13 @@
         public ContentCaptureOptions getOptions(@UserIdInt int userId,
                 @NonNull String packageName) {
             boolean isContentCaptureReceiverEnabled;
-            boolean isContentProtectionReceiverEnabled;
+            boolean isContentProtectionReceiverEnabled =
+                    isContentProtectionReceiverEnabled(userId, packageName);
             ArraySet<ComponentName> whitelistedComponents = null;
 
             synchronized (mGlobalWhitelistStateLock) {
                 isContentCaptureReceiverEnabled =
                         isContentCaptureReceiverEnabled(userId, packageName);
-                isContentProtectionReceiverEnabled =
-                        isContentProtectionReceiverEnabled(userId, packageName);
-
                 if (!isContentCaptureReceiverEnabled) {
                     // Full package is not allowlisted: check individual components next
                     whitelistedComponents = getWhitelistedComponents(userId, packageName);
diff --git a/services/core/java/com/android/server/EventLogTags.logtags b/services/core/java/com/android/server/EventLogTags.logtags
index b67e627..d47a399 100644
--- a/services/core/java/com/android/server/EventLogTags.logtags
+++ b/services/core/java/com/android/server/EventLogTags.logtags
@@ -81,7 +81,7 @@
 # when a notification has been clicked
 27520 notification_clicked (key|3),(lifespan|1),(freshness|1),(exposure|1),(rank|1),(count|1)
 # when a notification action button has been clicked
-27521 notification_action_clicked (key|3),(action_index|1),(lifespan|1),(freshness|1),(exposure|1),(rank|1),(count|1)
+27521 notification_action_clicked (key|3),(piIdentifier|3),(pendingIntent|3),(action_index|1),(lifespan|1),(freshness|1),(exposure|1),(rank|1),(count|1)
 # when a notification has been canceled
 27530 notification_canceled (key|3),(reason|1),(lifespan|1),(freshness|1),(exposure|1),(rank|1),(count|1),(listener|3)
 # replaces 27510 with a row per notification
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 9f9e2eb..533ecf1 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -391,7 +391,6 @@
         final boolean wasBtScoRequested = isBluetoothScoRequested();
         CommunicationRouteClient client;
 
-
         // Save previous client route in case of failure to start BT SCO audio
         AudioDeviceAttributes prevClientDevice = null;
         boolean prevPrivileged = false;
@@ -1043,7 +1042,7 @@
         synchronized (mBluetoothAudioStateLock) {
             mBluetoothScoOn = on;
             updateAudioHalBluetoothState();
-            postUpdateCommunicationRouteClient(eventSource);
+            postUpdateCommunicationRouteClient(isBluetoothScoRequested(), eventSource);
         }
     }
 
@@ -1395,8 +1394,10 @@
                 MSG_I_SAVE_CLEAR_PREF_DEVICES_FOR_CAPTURE_PRESET, SENDMSG_QUEUE, capturePreset);
     }
 
-    /*package*/ void postUpdateCommunicationRouteClient(String eventSource) {
-        sendLMsgNoDelay(MSG_L_UPDATE_COMMUNICATION_ROUTE_CLIENT, SENDMSG_QUEUE, eventSource);
+    /*package*/ void postUpdateCommunicationRouteClient(
+            boolean wasBtScoRequested, String eventSource) {
+        sendILMsgNoDelay(MSG_IL_UPDATE_COMMUNICATION_ROUTE_CLIENT, SENDMSG_QUEUE,
+                wasBtScoRequested ? 1 : 0, eventSource);
     }
 
     /*package*/ void postSetCommunicationDeviceForClient(CommunicationDeviceInfo info) {
@@ -1708,7 +1709,8 @@
                                             : AudioSystem.STREAM_DEFAULT);
                             if (btInfo.mProfile == BluetoothProfile.LE_AUDIO
                                     || btInfo.mProfile == BluetoothProfile.HEARING_AID) {
-                                onUpdateCommunicationRouteClient("setBluetoothActiveDevice");
+                                onUpdateCommunicationRouteClient(isBluetoothScoRequested(),
+                                        "setBluetoothActiveDevice");
                             }
                         }
                     }
@@ -1762,9 +1764,11 @@
                 case MSG_I_SET_MODE_OWNER:
                     synchronized (mSetModeLock) {
                         synchronized (mDeviceStateLock) {
+                            boolean wasBtScoRequested = isBluetoothScoRequested();
                             mAudioModeOwner = (AudioModeInfo) msg.obj;
                             if (mAudioModeOwner.mMode != AudioSystem.MODE_RINGTONE) {
-                                onUpdateCommunicationRouteClient("setNewModeOwner");
+                                onUpdateCommunicationRouteClient(
+                                        wasBtScoRequested, "setNewModeOwner");
                             }
                         }
                     }
@@ -1787,10 +1791,10 @@
                     }
                     break;
 
-                case MSG_L_UPDATE_COMMUNICATION_ROUTE_CLIENT:
+                case MSG_IL_UPDATE_COMMUNICATION_ROUTE_CLIENT:
                     synchronized (mSetModeLock) {
                         synchronized (mDeviceStateLock) {
-                            onUpdateCommunicationRouteClient((String) msg.obj);
+                            onUpdateCommunicationRouteClient(msg.arg1 == 1, (String) msg.obj);
                         }
                     }
                     break;
@@ -1973,7 +1977,7 @@
     private static final int MSG_I_SAVE_CLEAR_PREF_DEVICES_FOR_CAPTURE_PRESET = 38;
 
     private static final int MSG_L_SET_COMMUNICATION_DEVICE_FOR_CLIENT = 42;
-    private static final int MSG_L_UPDATE_COMMUNICATION_ROUTE_CLIENT = 43;
+    private static final int MSG_IL_UPDATE_COMMUNICATION_ROUTE_CLIENT = 43;
     private static final int MSG_I_SCO_AUDIO_STATE_CHANGED = 44;
 
     private static final int MSG_L_BT_ACTIVE_DEVICE_CHANGE_EXT = 45;
@@ -2330,16 +2334,20 @@
      */
     // @GuardedBy("mSetModeLock")
     @GuardedBy("mDeviceStateLock")
-    private void onUpdateCommunicationRouteClient(String eventSource) {
-        updateCommunicationRoute(eventSource);
+    private void onUpdateCommunicationRouteClient(boolean wasBtScoRequested, String eventSource) {
         CommunicationRouteClient crc = topCommunicationRouteClient();
         if (AudioService.DEBUG_COMM_RTE) {
-            Log.v(TAG, "onUpdateCommunicationRouteClient, crc: "
-                    + crc + " eventSource: " + eventSource);
+            Log.v(TAG, "onUpdateCommunicationRouteClient, crc: " + crc
+                    + " wasBtScoRequested: " + wasBtScoRequested + " eventSource: " + eventSource);
         }
         if (crc != null) {
             setCommunicationRouteForClient(crc.getBinder(), crc.getUid(), crc.getDevice(),
                     BtHelper.SCO_MODE_UNDEFINED, crc.isPrivileged(), eventSource);
+        } else {
+            if (!isBluetoothScoRequested() && wasBtScoRequested) {
+                mBtHelper.stopBluetoothSco(eventSource);
+            }
+            updateCommunicationRoute(eventSource);
         }
     }
 
@@ -2433,6 +2441,7 @@
             List<AudioRecordingConfiguration> recordConfigs) {
         synchronized (mSetModeLock) {
             synchronized (mDeviceStateLock) {
+                final boolean wasBtScoRequested = isBluetoothScoRequested();
                 boolean updateCommunicationRoute = false;
                 for (CommunicationRouteClient crc : mCommunicationRouteClients) {
                     boolean wasActive = crc.isActive();
@@ -2461,7 +2470,8 @@
                     }
                 }
                 if (updateCommunicationRoute) {
-                    postUpdateCommunicationRouteClient("updateCommunicationRouteClientsActivity");
+                    postUpdateCommunicationRouteClient(
+                            wasBtScoRequested, "updateCommunicationRouteClientsActivity");
                 }
             }
         }
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index 3560797..a4d26d3 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -329,7 +329,7 @@
             default:
                 break;
         }
-        if(broadcast) {
+        if (broadcast) {
             broadcastScoConnectionState(scoAudioState);
             //FIXME: this is to maintain compatibility with deprecated intent
             // AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED. Remove when appropriate.
@@ -718,8 +718,10 @@
         checkScoAudioState();
         if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) {
             // Make sure that the state transitions to CONNECTING even if we cannot initiate
-            // the connection.
-            broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_CONNECTING);
+            // the connection except if already connected internally
+            if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL) {
+                broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_CONNECTING);
+            }
             switch (mScoAudioState) {
                 case SCO_STATE_INACTIVE:
                     mScoAudioMode = scoAudioMode;
@@ -775,7 +777,7 @@
                     broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_CONNECTED);
                     break;
                 case SCO_STATE_ACTIVE_INTERNAL:
-                    Log.w(TAG, "requestScoState: already in ACTIVE mode, simply return");
+                    // Already in ACTIVE mode, simply return
                     break;
                 case SCO_STATE_ACTIVE_EXTERNAL:
                     /* Confirm SCO Audio connection to requesting app as it is already connected
diff --git a/services/core/java/com/android/server/display/DisplayDevice.java b/services/core/java/com/android/server/display/DisplayDevice.java
index d57dc47..8642fb8 100644
--- a/services/core/java/com/android/server/display/DisplayDevice.java
+++ b/services/core/java/com/android/server/display/DisplayDevice.java
@@ -16,6 +16,9 @@
 
 package com.android.server.display;
 
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
+
 import android.annotation.Nullable;
 import android.content.Context;
 import android.graphics.Point;
@@ -132,12 +135,15 @@
     /**
      * Returns the default size of the surface associated with the display, or null if the surface
      * is not provided for layer mirroring by SurfaceFlinger. For non virtual displays, this will
-     * be the actual display device's size.
+     * be the actual display device's size, reflecting the current rotation.
      */
     @Nullable
     public Point getDisplaySurfaceDefaultSizeLocked() {
         DisplayDeviceInfo displayDeviceInfo = getDisplayDeviceInfoLocked();
-        return new Point(displayDeviceInfo.width, displayDeviceInfo.height);
+        final boolean isRotated = mCurrentOrientation == ROTATION_90
+                || mCurrentOrientation == ROTATION_270;
+        return isRotated ? new Point(displayDeviceInfo.height, displayDeviceInfo.width)
+                : new Point(displayDeviceInfo.width, displayDeviceInfo.height);
     }
 
     /**
@@ -358,7 +364,7 @@
         }
 
         boolean isRotated = (mCurrentOrientation == Surface.ROTATION_90
-                || mCurrentOrientation == Surface.ROTATION_270);
+                || mCurrentOrientation == ROTATION_270);
         DisplayDeviceInfo info = getDisplayDeviceInfoLocked();
         viewport.deviceWidth = isRotated ? info.height : info.width;
         viewport.deviceHeight = isRotated ? info.width : info.height;
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index e5965ef..486cd28 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -2080,7 +2080,7 @@
 
     /** Loads the refresh rate profiles. */
     private void loadRefreshRateZoneProfiles(RefreshRateConfigs refreshRateConfigs) {
-        if (refreshRateConfigs == null) {
+        if (refreshRateConfigs == null || refreshRateConfigs.getRefreshRateZoneProfiles() == null) {
             return;
         }
         for (RefreshRateZone zone :
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 15a8c0f..c24e729 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -1194,7 +1194,9 @@
                 mNotificationRecordLogger.log(
                         NotificationRecordLogger.NotificationEvent.fromAction(actionIndex,
                                 generatedByAssistant, action.isContextual()), r);
-                EventLogTags.writeNotificationActionClicked(key, actionIndex,
+                EventLogTags.writeNotificationActionClicked(key,
+                        action.actionIntent.getTarget().toString(),
+                        action.actionIntent.getIntent().toString(), actionIndex,
                         r.getLifespanMs(now), r.getFreshnessMs(now), r.getExposureMs(now),
                         nv.rank, nv.count);
                 nv.recycle();
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 48cca32..5b466a0 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -1124,14 +1124,15 @@
                         + "track #%d", transition.getSyncId(), track);
             }
         }
-        if (sync) {
+        transition.mAnimationTrack = track;
+        info.setTrack(track);
+        mTrackCount = Math.max(mTrackCount, track + 1);
+        if (sync && mTrackCount > 1) {
+            // If there are >1 tracks, mark as sync so that all tracks finish.
             info.setFlags(info.getFlags() | TransitionInfo.FLAG_SYNC);
             ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Marking #%d animation as SYNC.",
                     transition.getSyncId());
         }
-        transition.mAnimationTrack = track;
-        info.setTrack(track);
-        mTrackCount = Math.max(mTrackCount, track + 1);
     }
 
     void updateAnimatingState(SurfaceControl.Transaction t) {
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceTest.java
new file mode 100644
index 0000000..4fd8f26
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceTest.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display;
+
+import static android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_180;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.view.SurfaceControl;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests for the {@link DisplayDevice} class.
+ *
+ * Build/Install/Run:
+ * atest DisplayServicesTests:DisplayDeviceTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class DisplayDeviceTest {
+    private final DisplayDeviceInfo mDisplayDeviceInfo = new DisplayDeviceInfo();
+    private static final int WIDTH = 500;
+    private static final int HEIGHT = 900;
+    private static final Point PORTRAIT_SIZE = new Point(WIDTH, HEIGHT);
+    private static final Point LANDSCAPE_SIZE = new Point(HEIGHT, WIDTH);
+
+    @Mock
+    private SurfaceControl.Transaction mMockTransaction;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mDisplayDeviceInfo.width = WIDTH;
+        mDisplayDeviceInfo.height = HEIGHT;
+        mDisplayDeviceInfo.rotation = ROTATION_0;
+    }
+
+    @Test
+    public void testGetDisplaySurfaceDefaultSizeLocked_notRotated() {
+        DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo);
+        assertThat(displayDevice.getDisplaySurfaceDefaultSizeLocked()).isEqualTo(PORTRAIT_SIZE);
+    }
+
+    @Test
+    public void testGetDisplaySurfaceDefaultSizeLocked_rotation0() {
+        DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo);
+        displayDevice.setProjectionLocked(mMockTransaction, ROTATION_0, new Rect(), new Rect());
+        assertThat(displayDevice.getDisplaySurfaceDefaultSizeLocked()).isEqualTo(PORTRAIT_SIZE);
+    }
+
+    @Test
+    public void testGetDisplaySurfaceDefaultSizeLocked_rotation90() {
+        DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo);
+        displayDevice.setProjectionLocked(mMockTransaction, ROTATION_90, new Rect(), new Rect());
+        assertThat(displayDevice.getDisplaySurfaceDefaultSizeLocked()).isEqualTo(LANDSCAPE_SIZE);
+    }
+
+    @Test
+    public void testGetDisplaySurfaceDefaultSizeLocked_rotation180() {
+        DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo);
+        displayDevice.setProjectionLocked(mMockTransaction, ROTATION_180, new Rect(), new Rect());
+        assertThat(displayDevice.getDisplaySurfaceDefaultSizeLocked()).isEqualTo(PORTRAIT_SIZE);
+    }
+
+    @Test
+    public void testGetDisplaySurfaceDefaultSizeLocked_rotation270() {
+        DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo);
+        displayDevice.setProjectionLocked(mMockTransaction, ROTATION_270, new Rect(), new Rect());
+        assertThat(displayDevice.getDisplaySurfaceDefaultSizeLocked()).isEqualTo(LANDSCAPE_SIZE);
+    }
+
+    private static class FakeDisplayDevice extends DisplayDevice {
+        private final DisplayDeviceInfo mDisplayDeviceInfo;
+
+        FakeDisplayDevice(DisplayDeviceInfo displayDeviceInfo) {
+            super(null, null, "", InstrumentationRegistry.getInstrumentation().getContext());
+            mDisplayDeviceInfo = displayDeviceInfo;
+        }
+
+        @Override
+        public boolean hasStableUniqueId() {
+            return false;
+        }
+
+        @Override
+        public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
+            return mDisplayDeviceInfo;
+        }
+    }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index bdee99b..5c35b05 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -7752,7 +7752,8 @@
     public void testOnNotificationActionClick() {
         final int actionIndex = 2;
         final Notification.Action action =
-                new Notification.Action.Builder(null, "text", null).build();
+                new Notification.Action.Builder(null, "text", PendingIntent.getActivity(
+                        mContext, 0, new Intent(), PendingIntent.FLAG_IMMUTABLE)).build();
         final boolean generatedByAssistant = false;
 
         NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
@@ -7776,7 +7777,8 @@
     public void testOnAssistantNotificationActionClick() {
         final int actionIndex = 1;
         final Notification.Action action =
-                new Notification.Action.Builder(null, "text", null).build();
+                new Notification.Action.Builder(null, "text", PendingIntent.getActivity(
+                        mContext, 0, new Intent(), PendingIntent.FLAG_IMMUTABLE)).build();
         final boolean generatedByAssistant = true;
 
         NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index b4f1176..d7cd9f2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -39,6 +39,7 @@
 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
 import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER;
+import static android.window.TransitionInfo.FLAG_SYNC;
 import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
 import static android.window.TransitionInfo.isIndependent;
 
@@ -2384,6 +2385,37 @@
         assertFalse(controller.isCollecting());
     }
 
+    @Test
+    public void testNoSyncFlagIfOneTrack() {
+        final TransitionController controller = mAtm.getTransitionController();
+        final TestTransitionPlayer player = registerTestTransitionPlayer();
+
+        mSyncEngine = createTestBLASTSyncEngine();
+        controller.setSyncEngine(mSyncEngine);
+
+        final Transition transitA = createTestTransition(TRANSIT_OPEN, controller);
+        final Transition transitB = createTestTransition(TRANSIT_OPEN, controller);
+        final Transition transitC = createTestTransition(TRANSIT_OPEN, controller);
+
+        controller.startCollectOrQueue(transitA, (deferred) -> {});
+        controller.startCollectOrQueue(transitB, (deferred) -> {});
+        controller.startCollectOrQueue(transitC, (deferred) -> {});
+
+        // Verify that, as-long as there is <= 1 track, we won't get a SYNC flag
+        transitA.start();
+        transitA.setAllReady();
+        mSyncEngine.tryFinishForTest(transitA.getSyncId());
+        assertTrue((player.mLastReady.getFlags() & FLAG_SYNC) == 0);
+        transitB.start();
+        transitB.setAllReady();
+        mSyncEngine.tryFinishForTest(transitB.getSyncId());
+        assertTrue((player.mLastReady.getFlags() & FLAG_SYNC) == 0);
+        transitC.start();
+        transitC.setAllReady();
+        mSyncEngine.tryFinishForTest(transitC.getSyncId());
+        assertTrue((player.mLastReady.getFlags() & FLAG_SYNC) == 0);
+    }
+
     private static void makeTaskOrganized(Task... tasks) {
         final ITaskOrganizer organizer = mock(ITaskOrganizer.class);
         for (Task t : tasks) {
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
index 58da4b43..3d78a1d 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
@@ -54,6 +54,7 @@
 import android.service.voice.HotwordDetectionService;
 import android.service.voice.HotwordDetectionServiceFailure;
 import android.service.voice.HotwordDetector;
+import android.service.voice.IDetectorSessionStorageService;
 import android.service.voice.IMicrophoneHotwordDetectionVoiceInteractionCallback;
 import android.service.voice.ISandboxedDetectionService;
 import android.service.voice.IVisualQueryDetectionVoiceInteractionCallback;
@@ -69,6 +70,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.app.IHotwordRecognitionStatusCallback;
 import com.android.internal.app.IVisualQueryDetectionAttentionListener;
+import com.android.internal.infra.AndroidFuture;
 import com.android.internal.infra.ServiceConnector;
 import com.android.server.LocalServices;
 import com.android.server.pm.permission.PermissionManagerServiceInternal;
@@ -157,6 +159,8 @@
     @NonNull private ServiceConnection mRemoteVisualQueryDetectionService;
     @GuardedBy("mLock")
     @Nullable private IBinder mAudioFlinger;
+
+    @Nullable private IHotwordRecognitionStatusCallback mHotwordRecognitionCallback;
     @GuardedBy("mLock")
     private boolean mDebugHotwordLogging = false;
 
@@ -694,6 +698,7 @@
             updateContentCaptureManager(connection);
             updateSpeechService(connection);
             updateServiceIdentity(connection);
+            updateStorageService(connection);
             return connection;
         }
     }
@@ -910,6 +915,7 @@
                     mVoiceInteractionServiceUid, mVoiceInteractorIdentity,
                     mScheduledExecutorService, mDebugHotwordLogging, mRemoteExceptionListener);
         }
+        mHotwordRecognitionCallback = callback;
         mDetectorSessions.put(detectorType, session);
         session.initialize(options, sharedMemory);
     }
@@ -1035,6 +1041,23 @@
         }));
     }
 
+    private void updateStorageService(ServiceConnection connection) {
+        connection.run(service -> {
+            service.registerRemoteStorageService(new IDetectorSessionStorageService.Stub() {
+                @Override
+                public void openFile(String filename, AndroidFuture future)
+                        throws RemoteException {
+                    Slog.v(TAG, "BinderCallback#onFileOpen");
+                    try {
+                        mHotwordRecognitionCallback.onOpenFile(filename, future);
+                    } catch (RemoteException e) {
+                        e.rethrowFromSystemServer();
+                    }
+                }
+            });
+        });
+    }
+
     private void addServiceUidForAudioPolicy(int uid) {
         mScheduledExecutorService.execute(() -> {
             AudioManagerInternal audioManager =