Merge "[pm] do not stop system apps with disabled a launcher activity" into main
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
index 13903ac..f429966 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
@@ -56,6 +56,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.FrameworkStatsLog;
+import com.android.modules.expresslog.Counter;
 import com.android.server.LocalServices;
 import com.android.server.job.GrantedUriPermissions;
 import com.android.server.job.JobSchedulerInternal;
@@ -161,6 +162,9 @@
     /** If the job is going to be passed an unmetered network. */
     private boolean mHasAccessToUnmetered;
 
+    /** If the effective bucket has been downgraded once due to being buggy. */
+    private boolean mIsDowngradedDueToBuggyApp;
+
     /**
      * The additional set of dynamic constraints that must be met if this is an expedited job that
      * had a long enough run while the device was Dozing or in battery saver.
@@ -1173,18 +1177,32 @@
             // like other ACTIVE apps.
             return ACTIVE_INDEX;
         }
+
+        final int bucketWithMediaExemption;
+        if (actualBucket != RESTRICTED_INDEX && actualBucket != NEVER_INDEX
+                && mHasMediaBackupExemption) {
+            // Treat it as if it's at most WORKING_INDEX (lower index grants higher quota) since
+            // media backup jobs are important to the user, and the source package may not have
+            // been used directly in a while.
+            bucketWithMediaExemption = Math.min(WORKING_INDEX, actualBucket);
+        } else {
+            bucketWithMediaExemption = actualBucket;
+        }
+
         // If the app is considered buggy, but hasn't yet been put in the RESTRICTED bucket
         // (potentially because it's used frequently by the user), limit its effective bucket
         // so that it doesn't get to run as much as a normal ACTIVE app.
-        final int highestBucket = isBuggy ? WORKING_INDEX : ACTIVE_INDEX;
-        if (actualBucket != RESTRICTED_INDEX && actualBucket != NEVER_INDEX
-                && mHasMediaBackupExemption) {
-            // Treat it as if it's at least WORKING_INDEX since media backup jobs are important
-            // to the user, and the
-            // source package may not have been used directly in a while.
-            return Math.max(highestBucket, Math.min(WORKING_INDEX, actualBucket));
+        if (isBuggy && bucketWithMediaExemption < WORKING_INDEX) {
+            if (!mIsDowngradedDueToBuggyApp) {
+                // Safety check to avoid logging multiple times for the same job.
+                Counter.logIncrementWithUid(
+                        "job_scheduler.value_job_quota_reduced_due_to_buggy_uid",
+                        getTimeoutBlameUid());
+                mIsDowngradedDueToBuggyApp = true;
+            }
+            return WORKING_INDEX;
         }
-        return Math.max(highestBucket, actualBucket);
+        return bucketWithMediaExemption;
     }
 
     /** Returns the real standby bucket of the job. */
diff --git a/cmds/appops/OWNERS b/cmds/appops/OWNERS
index 999ea0e..2fe78c5 100644
--- a/cmds/appops/OWNERS
+++ b/cmds/appops/OWNERS
@@ -1 +1,2 @@
+#Bug component: 137825
 include /core/java/android/permission/OWNERS
diff --git a/core/api/current.txt b/core/api/current.txt
index ff011cf..eaefe84 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -9694,6 +9694,7 @@
     method public int describeContents();
     method public void enforceCallingUid();
     method @Nullable public String getAttributionTag();
+    method public int getDeviceId();
     method @Nullable public android.content.AttributionSource getNext();
     method @Nullable public String getPackageName();
     method public int getPid();
@@ -9709,6 +9710,7 @@
     ctor public AttributionSource.Builder(@NonNull android.content.AttributionSource);
     method @NonNull public android.content.AttributionSource build();
     method @NonNull public android.content.AttributionSource.Builder setAttributionTag(@Nullable String);
+    method @NonNull public android.content.AttributionSource.Builder setDeviceId(int);
     method @Deprecated @NonNull public android.content.AttributionSource.Builder setNext(@Nullable android.content.AttributionSource);
     method @NonNull public android.content.AttributionSource.Builder setNextAttributionSource(@NonNull android.content.AttributionSource);
     method @NonNull public android.content.AttributionSource.Builder setPackageName(@Nullable String);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index cf26e12..083c445 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -850,6 +850,7 @@
     ctor public AttributionSource(int, @Nullable String, @Nullable String, @NonNull android.os.IBinder);
     ctor public AttributionSource(int, @Nullable String, @Nullable String, @Nullable java.util.Set<java.lang.String>, @Nullable android.content.AttributionSource);
     ctor public AttributionSource(int, int, @Nullable String, @Nullable String, @NonNull android.os.IBinder, @Nullable String[], @Nullable android.content.AttributionSource);
+    ctor public AttributionSource(int, int, @Nullable String, @Nullable String, @NonNull android.os.IBinder, @Nullable String[], int, @Nullable android.content.AttributionSource);
     method public void enforceCallingPid();
   }
 
@@ -4235,6 +4236,7 @@
     field public final boolean isTrustedOverlay;
     field public final boolean isVisible;
     field @NonNull public final String name;
+    field @NonNull public final android.graphics.Matrix transform;
     field @NonNull public final android.os.IBinder windowToken;
   }
 
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index 4eaebe0..59b0dac 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -27,7 +27,6 @@
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.annotation.UiContext;
-import android.app.servertransaction.WindowTokenClientController;
 import android.companion.virtual.VirtualDeviceManager;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.AttributionSource;
@@ -3277,8 +3276,7 @@
         // if this Context is not a WindowContext. WindowContext finalization is handled in
         // WindowContext class.
         if (mToken instanceof WindowTokenClient && mOwnsToken) {
-            WindowTokenClientController.getInstance().detachIfNeeded(
-                    (WindowTokenClient) mToken);
+            ((WindowTokenClient) mToken).detachFromWindowContainerIfNeeded();
         }
         super.finalize();
     }
@@ -3306,7 +3304,7 @@
         final WindowTokenClient token = new WindowTokenClient();
         final ContextImpl context = systemContext.createWindowContextBase(token, displayId);
         token.attachContext(context);
-        WindowTokenClientController.getInstance().attachToDisplayContent(token, displayId);
+        token.attachToDisplayContent(displayId);
         context.mContextType = CONTEXT_TYPE_SYSTEM_OR_SYSTEM_UI;
         context.mOwnsToken = true;
 
@@ -3465,7 +3463,7 @@
         AttributionSource attributionSource = new AttributionSource(Process.myUid(),
                 Process.myPid(), mOpPackageName, attributionTag,
                 (renouncedPermissions != null) ? renouncedPermissions.toArray(new String[0]) : null,
-                nextAttributionSource);
+                getDeviceId(), nextAttributionSource);
         // If we want to access protected data on behalf of another app we need to
         // tell the OS that we opt in to participate in the attribution chain.
         if (nextAttributionSource != null) {
diff --git a/core/java/android/app/servertransaction/WindowTokenClientController.java b/core/java/android/app/servertransaction/WindowTokenClientController.java
deleted file mode 100644
index 28e2040..0000000
--- a/core/java/android/app/servertransaction/WindowTokenClientController.java
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * 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.app.servertransaction;
-
-import static android.view.WindowManager.LayoutParams.WindowType;
-import static android.view.WindowManagerGlobal.getWindowManagerService;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.content.Context;
-import android.content.res.Configuration;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.util.ArrayMap;
-import android.view.IWindowManager;
-import android.window.WindowContext;
-import android.window.WindowTokenClient;
-
-import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.VisibleForTesting;
-
-/**
- * Singleton controller to manage the attached {@link WindowTokenClient}s, and to dispatch
- * corresponding window configuration change from server side.
- * @hide
- */
-public class WindowTokenClientController {
-
-    private static WindowTokenClientController sController;
-
-    private final Object mLock = new Object();
-
-    /** Mapping from a client defined token to the {@link WindowTokenClient} it represents. */
-    @GuardedBy("mLock")
-    private final ArrayMap<IBinder, WindowTokenClient> mWindowTokenClientMap = new ArrayMap<>();
-
-    /** Gets the singleton controller. */
-    public static WindowTokenClientController getInstance() {
-        synchronized (WindowTokenClientController.class) {
-            if (sController == null) {
-                sController = new WindowTokenClientController();
-            }
-            return sController;
-        }
-    }
-
-    /** Overrides the {@link #getInstance()} for test only. */
-    @VisibleForTesting
-    public static void overrideInstance(@NonNull WindowTokenClientController controller) {
-        synchronized (WindowTokenClientController.class) {
-            sController = controller;
-        }
-    }
-
-    private WindowTokenClientController() {}
-
-    /**
-     * Attaches a {@link WindowTokenClient} to a {@link com.android.server.wm.DisplayArea}.
-     *
-     * @param client The {@link WindowTokenClient} to attach.
-     * @param type The window type of the {@link WindowContext}
-     * @param displayId The {@link Context#getDisplayId() ID of display} to associate with
-     * @param options The window context launched option
-     * @return {@code true} if attaching successfully.
-     */
-    public boolean attachToDisplayArea(@NonNull WindowTokenClient client,
-            @WindowType int type, int displayId, @Nullable Bundle options) {
-        final Configuration configuration;
-        try {
-            configuration = getWindowManagerService()
-                    .attachWindowContextToDisplayArea(client, type, displayId, options);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-        if (configuration == null) {
-            return false;
-        }
-        onWindowContainerTokenAttached(client, displayId, configuration);
-        return true;
-    }
-
-    /**
-     * Attaches a {@link WindowTokenClient} to a {@code DisplayContent}.
-     *
-     * @param client The {@link WindowTokenClient} to attach.
-     * @param displayId The {@link Context#getDisplayId() ID of display} to associate with
-     * @return {@code true} if attaching successfully.
-     */
-    public boolean attachToDisplayContent(@NonNull WindowTokenClient client, int displayId) {
-        final IWindowManager wms = getWindowManagerService();
-        // #createSystemUiContext may call this method before WindowManagerService is initialized.
-        if (wms == null) {
-            return false;
-        }
-        final Configuration configuration;
-        try {
-            configuration = wms.attachToDisplayContent(client, displayId);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-        if (configuration == null) {
-            return false;
-        }
-        onWindowContainerTokenAttached(client, displayId, configuration);
-        return true;
-    }
-
-    /**
-     * Attaches this {@link WindowTokenClient} to a {@code windowToken}.
-     *
-     * @param client The {@link WindowTokenClient} to attach.
-     * @param windowToken the window token to associated with
-     */
-    public void attachToWindowToken(@NonNull WindowTokenClient client,
-            @NonNull IBinder windowToken) {
-        try {
-            getWindowManagerService().attachWindowContextToWindowToken(client, windowToken);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-        // We don't report configuration change for now.
-        synchronized (mLock) {
-            mWindowTokenClientMap.put(client.asBinder(), client);
-        }
-    }
-
-    /** Detaches a {@link WindowTokenClient} from associated WindowContainer if there's one. */
-    public void detachIfNeeded(@NonNull WindowTokenClient client) {
-        synchronized (mLock) {
-            if (mWindowTokenClientMap.remove(client.asBinder()) == null) {
-                return;
-            }
-        }
-        try {
-            getWindowManagerService().detachWindowContextFromWindowContainer(client);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    private void onWindowContainerTokenAttached(@NonNull WindowTokenClient client, int displayId,
-            @NonNull Configuration configuration) {
-        synchronized (mLock) {
-            mWindowTokenClientMap.put(client.asBinder(), client);
-        }
-        client.onConfigurationChanged(configuration, displayId,
-                false /* shouldReportConfigChange */);
-    }
-}
diff --git a/core/java/android/content/AttributionSource.java b/core/java/android/content/AttributionSource.java
index 3dddbc0..d0bb2b9 100644
--- a/core/java/android/content/AttributionSource.java
+++ b/core/java/android/content/AttributionSource.java
@@ -115,14 +115,14 @@
     public AttributionSource(int uid, @Nullable String packageName,
             @Nullable String attributionTag, @NonNull IBinder token) {
         this(uid, Process.INVALID_PID, packageName, attributionTag, token,
-                /*renouncedPermissions*/ null, /*next*/ null);
+                /*renouncedPermissions*/ null, Context.DEVICE_ID_DEFAULT, /*next*/ null);
     }
 
     /** @hide */
     public AttributionSource(int uid, int pid, @Nullable String packageName,
             @Nullable String attributionTag, @NonNull IBinder token) {
         this(uid, pid, packageName, attributionTag, token, /*renouncedPermissions*/ null,
-                /*next*/ null);
+                Context.DEVICE_ID_DEFAULT, /*next*/ null);
     }
 
     /** @hide */
@@ -132,21 +132,23 @@
             @Nullable AttributionSource next) {
         this(uid, Process.INVALID_PID, packageName, attributionTag, sDefaultToken,
                 (renouncedPermissions != null)
-                ? renouncedPermissions.toArray(new String[0]) : null, /*next*/ next);
+                ? renouncedPermissions.toArray(new String[0]) : null, Context.DEVICE_ID_DEFAULT,
+                /*next*/ next);
     }
 
     /** @hide */
     public AttributionSource(@NonNull AttributionSource current, @Nullable AttributionSource next) {
         this(current.getUid(), current.getPid(), current.getPackageName(),
                 current.getAttributionTag(), current.getToken(),
-                current.mAttributionSourceState.renouncedPermissions, next);
+                current.mAttributionSourceState.renouncedPermissions, current.getDeviceId(), next);
     }
 
     /** @hide */
     public AttributionSource(int uid, int pid, @Nullable String packageName,
-            @Nullable String attributionTag, @Nullable String[] renouncedPermissions,
+            @Nullable String attributionTag, @Nullable String[] renouncedPermissions, int deviceId,
             @Nullable AttributionSource next) {
-        this(uid, pid, packageName, attributionTag, sDefaultToken, renouncedPermissions, next);
+        this(uid, pid, packageName, attributionTag, sDefaultToken, renouncedPermissions, deviceId,
+                next);
     }
 
     /** @hide */
@@ -155,6 +157,16 @@
             @Nullable String attributionTag, @NonNull IBinder token,
             @Nullable String[] renouncedPermissions,
             @Nullable AttributionSource next) {
+        this(uid, pid, packageName, attributionTag, token, renouncedPermissions,
+                Context.DEVICE_ID_DEFAULT, next);
+    }
+
+    /** @hide */
+    @TestApi
+    public AttributionSource(int uid, int pid, @Nullable String packageName,
+            @Nullable String attributionTag, @NonNull IBinder token,
+            @Nullable String[] renouncedPermissions,
+            int deviceId, @Nullable AttributionSource next) {
         mAttributionSourceState = new AttributionSourceState();
         mAttributionSourceState.uid = uid;
         mAttributionSourceState.pid = pid;
@@ -162,6 +174,7 @@
         mAttributionSourceState.packageName = packageName;
         mAttributionSourceState.attributionTag = attributionTag;
         mAttributionSourceState.renouncedPermissions = renouncedPermissions;
+        mAttributionSourceState.deviceId = deviceId;
         mAttributionSourceState.next = (next != null) ? new AttributionSourceState[]
                 {next.mAttributionSourceState} : new AttributionSourceState[0];
     }
@@ -197,25 +210,31 @@
     /** @hide */
     public AttributionSource withNextAttributionSource(@Nullable AttributionSource next) {
         return new AttributionSource(getUid(), getPid(), getPackageName(), getAttributionTag(),
-                getToken(), mAttributionSourceState.renouncedPermissions, next);
+                getToken(), mAttributionSourceState.renouncedPermissions, getDeviceId(), next);
     }
 
     /** @hide */
     public AttributionSource withPackageName(@Nullable String packageName) {
         return new AttributionSource(getUid(), getPid(), packageName, getAttributionTag(),
-               getToken(), mAttributionSourceState.renouncedPermissions, getNext());
+               getToken(), mAttributionSourceState.renouncedPermissions, getDeviceId(), getNext());
     }
 
     /** @hide */
     public AttributionSource withToken(@NonNull Binder token) {
         return new AttributionSource(getUid(), getPid(), getPackageName(), getAttributionTag(),
-                token, mAttributionSourceState.renouncedPermissions, getNext());
+                token, mAttributionSourceState.renouncedPermissions, getDeviceId(), getNext());
     }
 
     /** @hide */
     public AttributionSource withPid(int pid) {
         return new AttributionSource(getUid(), pid, getPackageName(), getAttributionTag(),
-                getToken(), mAttributionSourceState.renouncedPermissions, getNext());
+                getToken(), mAttributionSourceState.renouncedPermissions, getDeviceId(), getNext());
+    }
+
+    /** @hide */
+    public AttributionSource withDeviceId(int deviceId) {
+        return new AttributionSource(getUid(), getPid(), getPackageName(), getAttributionTag(),
+                getToken(), mAttributionSourceState.renouncedPermissions, deviceId, getNext());
     }
 
     /** @hide */
@@ -259,6 +278,7 @@
         try {
             return new AttributionSource.Builder(uid)
                 .setPid(Process.myPid())
+                .setDeviceId(Context.DEVICE_ID_DEFAULT)
                 .setPackageName(AppGlobals.getPackageManager().getPackagesForUid(uid)[0])
                 .build();
         } catch (Exception ignored) {
@@ -497,6 +517,13 @@
     }
 
     /**
+     * The device ID for which permissions are checked.
+     */
+    public int getDeviceId() {
+        return mAttributionSourceState.deviceId;
+    }
+
+    /**
      * Unique token for that source.
      *
      * @hide
@@ -662,6 +689,19 @@
         }
 
         /**
+         * Set the device ID for this attribution source, permission check would happen
+         * against this device ID.
+         *
+         * @return the builder
+         */
+        public @NonNull Builder setDeviceId(int deviceId) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x12;
+            mAttributionSourceState.deviceId = deviceId;
+            return this;
+        }
+
+        /**
          * The next app to receive the permission protected data.
          *
          * @deprecated Use {@link setNextAttributionSource} instead.
@@ -703,6 +743,9 @@
             if ((mBuilderFieldsSet & 0x10) == 0) {
                 mAttributionSourceState.renouncedPermissions = null;
             }
+            if ((mBuilderFieldsSet & 0x12) == 0) {
+                mAttributionSourceState.deviceId = Context.DEVICE_ID_DEFAULT;
+            }
             if ((mBuilderFieldsSet & 0x20) == 0) {
                 mAttributionSourceState.next = null;
             }
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 6d82922..2a6d84b 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -5298,6 +5298,13 @@
     public static final String APP_PREDICTION_SERVICE = "app_prediction";
 
     /**
+     * Used for reading system-wide, overridable flags.
+     *
+     * @hide
+     */
+    public static final String FEATURE_FLAGS_SERVICE = "feature_flags";
+
+    /**
      * Official published name of the search ui service.
      *
      * <p><b>NOTE: </b> this service is optional; callers of
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index c10062c..fe108a5 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -6546,6 +6546,14 @@
     public static final String EXTRA_REASON = "android.intent.extra.REASON";
 
     /**
+     * Intent extra: Whether to show the wipe progress UI or to skip it.
+     *
+     * <p>Type: boolean
+     * @hide
+     */
+    public static final String EXTRA_SHOW_WIPE_PROGRESS = "android.intent.extra.SHOW_WIPE_PROGRESS";
+
+    /**
      * {@hide}
      * This extra will be send together with {@link #ACTION_FACTORY_RESET}
      */
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 2948bd9..bef023e 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -1432,7 +1432,6 @@
             INSTALL_ALLOW_DOWNGRADE,
             INSTALL_STAGED,
             INSTALL_REQUEST_UPDATE_OWNERSHIP,
-            INSTALL_DONT_EXTRACT_BASELINE_PROFILES,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface InstallFlags {}
@@ -1647,13 +1646,6 @@
      */
     public static final int INSTALL_FROM_MANAGED_USER_OR_PROFILE = 1 << 26;
 
-    /**
-     * Flag parameter for {@link PackageInstaller.SessionParams} to indicate that do not extract
-     * the baseline profiles when parsing the apk
-     * @hide
-     */
-    public static final int INSTALL_DONT_EXTRACT_BASELINE_PROFILES = 1 << 27;
-
     /** @hide */
     @IntDef(flag = true, value = {
             DONT_KILL_APP,
diff --git a/core/java/android/content/pm/dex/DexMetadataHelper.java b/core/java/android/content/pm/dex/DexMetadataHelper.java
index 3b53c25..e75aa06 100644
--- a/core/java/android/content/pm/dex/DexMetadataHelper.java
+++ b/core/java/android/content/pm/dex/DexMetadataHelper.java
@@ -23,9 +23,6 @@
 import android.content.pm.parsing.PackageLite;
 import android.content.pm.parsing.result.ParseInput;
 import android.content.pm.parsing.result.ParseResult;
-import android.content.res.AssetFileDescriptor;
-import android.content.res.AssetManager;
-import android.os.ParcelFileDescriptor.AutoCloseInputStream;
 import android.os.SystemProperties;
 import android.util.ArrayMap;
 import android.util.JsonReader;
@@ -36,7 +33,6 @@
 import com.android.internal.security.VerityUtils;
 
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
@@ -48,7 +44,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.zip.ZipEntry;
-import java.util.zip.ZipOutputStream;
 
 /**
  * Helper class used to compute and validate the location of dex metadata files.
@@ -67,11 +62,6 @@
 
     private static final String DEX_METADATA_FILE_EXTENSION = ".dm";
 
-    private static final String PROFILE_FILE_NAME = "primary.prof";
-    private static final String PROFILE_METADATA_FILE_NAME = "primary.profm";
-    private static final String BASELINE_PROFILE_SOURCE_RELATIVE_PATH = "dexopt/baseline.prof";
-    private static final String BASELINE_PROFILE_METADATA_RELATIVE_PATH = "dexopt/baseline.profm";
-
     private DexMetadataHelper() {}
 
     /** Return true if the given file is a dex metadata file. */
@@ -323,76 +313,4 @@
         }
     }
 
-    /**
-     * Extract the baseline profiles from the assets directory in the apk. Then create a
-     * ZIP archive with the (.dm) file extension (e.g. foo.dm from foo.apk) to include the
-     * baseline profiles and put the DexMetadata file in the same directory with the apk.
-     *
-     * @param assetManager The {@link AssetManager} to use.
-     * @param apkPath The path of the apk
-     * @return {@code true} if the extraction is successful. Otherwise, return {@code false}.
-     *
-     * @see #buildDexMetadataPathForApk(String)
-     */
-    public static boolean extractBaselineProfilesToDexMetadataFileFromApk(AssetManager assetManager,
-            String apkPath) {
-        if (!ApkLiteParseUtils.isApkPath(apkPath)) {
-            if (DEBUG) {
-                Log.d(TAG, "It is not an apk file: " + apkPath);
-            }
-            return false;
-        }
-
-        // get the name of the DexMetadata file from the path of the apk
-        final File dmFile = new File(buildDexMetadataPathForApk(apkPath));
-        boolean success = false;
-
-        // load profile and profile metadata from assets directory in the apk
-        try (InputStream profileIs = openStreamFromAssets(assetManager,
-                BASELINE_PROFILE_SOURCE_RELATIVE_PATH);
-             InputStream profileMetadataIs = openStreamFromAssets(assetManager,
-                     BASELINE_PROFILE_METADATA_RELATIVE_PATH)) {
-            // Create the zip archive file and write the baseline profiles into it.
-            try (FileOutputStream fos = new FileOutputStream(dmFile)) {
-                try (ZipOutputStream zipOs = new ZipOutputStream(fos)) {
-                    zipOs.putNextEntry(new ZipEntry(PROFILE_FILE_NAME));
-                    zipOs.write(profileIs.readAllBytes());
-                    zipOs.closeEntry();
-
-                    zipOs.putNextEntry(new ZipEntry(PROFILE_METADATA_FILE_NAME));
-                    zipOs.write(profileMetadataIs.readAllBytes());
-                    zipOs.closeEntry();
-                    success = true;
-                }
-            }
-        } catch (IOException e) {
-            if (DEBUG) {
-                Log.e(TAG, "Extract baseline profiles from apk failed: " + e.getMessage());
-            }
-        } finally {
-            if (!success) {
-                if (dmFile.exists()) {
-                    dmFile.delete();
-                }
-            }
-        }
-        return success;
-    }
-
-    /**
-     * Loads an {@link AutoCloseInputStream} from assets with the path.
-     *
-     * @param assetManager The {@link AssetManager} to use.
-     * @param path The source file's relative path.
-     * @return An AutoCloseInputStream in case the file was successfully read.
-     * @throws IOException If anything goes wrong while opening or reading the file.
-     */
-    private static AutoCloseInputStream openStreamFromAssets(AssetManager assetManager, String path)
-            throws IOException {
-        AssetFileDescriptor descriptor = assetManager.openFd(path);
-        // Based on the java doc of AssetFileDescriptor#createInputStream, it will always return
-        // an AutoCloseInputStream. It should be fine we cast it from FileInputStream to
-        // AutoCloseInputStream here.
-        return (AutoCloseInputStream) descriptor.createInputStream();
-    }
 }
diff --git a/core/java/android/flags/BooleanFlag.java b/core/java/android/flags/BooleanFlag.java
new file mode 100644
index 0000000..d4a35b2
--- /dev/null
+++ b/core/java/android/flags/BooleanFlag.java
@@ -0,0 +1,52 @@
+/*
+ * 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.flags;
+
+import android.annotation.NonNull;
+
+/**
+ * A flag representing a true or false value.
+ *
+ * The value will always be the same during the lifetime of the process it is read in.
+ *
+ * @hide
+ */
+public class BooleanFlag extends BooleanFlagBase {
+    private final boolean mDefault;
+
+    /**
+     * @param namespace A namespace for this flag. See {@link android.provider.DeviceConfig}.
+     * @param name A name for this flag.
+     * @param defaultValue The value of this flag if no other override is present.
+     */
+    BooleanFlag(String namespace, String name, boolean defaultValue) {
+        super(namespace, name);
+        mDefault = defaultValue;
+    }
+
+    @Override
+    @NonNull
+    public Boolean getDefault() {
+        return mDefault;
+    }
+
+    @Override
+    public BooleanFlag defineMetaData(String label, String description, String categoryName) {
+        super.defineMetaData(label, description, categoryName);
+        return this;
+    }
+}
diff --git a/core/java/android/flags/BooleanFlagBase.java b/core/java/android/flags/BooleanFlagBase.java
new file mode 100644
index 0000000..985dbe3
--- /dev/null
+++ b/core/java/android/flags/BooleanFlagBase.java
@@ -0,0 +1,82 @@
+/*
+ * 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.flags;
+
+import android.annotation.NonNull;
+
+abstract class BooleanFlagBase implements Flag<Boolean> {
+
+    private final String mNamespace;
+    private final String mName;
+    private String mLabel;
+    private String mDescription;
+    private String mCategoryName;
+
+    /**
+     * @param namespace A namespace for this flag. See {@link android.provider.DeviceConfig}.
+     * @param name A name for this flag.
+     */
+    BooleanFlagBase(String namespace, String name) {
+        mNamespace = namespace;
+        mName = name;
+        mLabel = name;
+    }
+
+    public abstract Boolean getDefault();
+
+    @Override
+    @NonNull
+    public String getNamespace() {
+        return mNamespace;
+    }
+
+    @Override
+    @NonNull
+    public String getName() {
+        return mName;
+    }
+
+    @Override
+    public BooleanFlagBase defineMetaData(String label, String description, String categoryName) {
+        mLabel = label;
+        mDescription = description;
+        mCategoryName = categoryName;
+        return this;
+    }
+
+    @Override
+    @NonNull
+    public String getLabel() {
+        return mLabel;
+    }
+
+    @Override
+    public String getDescription() {
+        return mDescription;
+    }
+
+    @Override
+    public String getCategoryName() {
+        return mCategoryName;
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return getNamespace() + "." + getName() + "[" + getDefault() + "]";
+    }
+}
diff --git a/core/java/android/flags/DynamicBooleanFlag.java b/core/java/android/flags/DynamicBooleanFlag.java
new file mode 100644
index 0000000..271a8c5f4
--- /dev/null
+++ b/core/java/android/flags/DynamicBooleanFlag.java
@@ -0,0 +1,50 @@
+/*
+ * 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.flags;
+
+/**
+ * A flag representing a true or false value.
+ *
+ * The value may be different from one read to the next.
+ *
+ * @hide
+ */
+public class DynamicBooleanFlag extends BooleanFlagBase implements DynamicFlag<Boolean> {
+
+    private final boolean mDefault;
+
+    /**
+     * @param namespace A namespace for this flag. See {@link android.provider.DeviceConfig}.
+     * @param name A name for this flag.
+     * @param defaultValue The value of this flag if no other override is present.
+     */
+    DynamicBooleanFlag(String namespace, String name, boolean defaultValue) {
+        super(namespace, name);
+        mDefault = defaultValue;
+    }
+
+    @Override
+    public Boolean getDefault() {
+        return mDefault;
+    }
+
+    @Override
+    public DynamicBooleanFlag defineMetaData(String label, String description, String categoryName) {
+        super.defineMetaData(label, description, categoryName);
+        return this;
+    }
+}
diff --git a/core/java/android/flags/DynamicFlag.java b/core/java/android/flags/DynamicFlag.java
new file mode 100644
index 0000000..68819c5
--- /dev/null
+++ b/core/java/android/flags/DynamicFlag.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.flags;
+
+/**
+ * A flag for which the value may be different from one read to the next.
+ *
+ * @param <T> The type of value that this flag stores. E.g. Boolean or String.
+ *
+ * @hide
+ */
+public interface DynamicFlag<T> extends Flag<T> {
+    @Override
+    default boolean isDynamic() {
+        return true;
+    }
+}
diff --git a/core/java/android/flags/FeatureFlags.java b/core/java/android/flags/FeatureFlags.java
new file mode 100644
index 0000000..8d3112c
--- /dev/null
+++ b/core/java/android/flags/FeatureFlags.java
@@ -0,0 +1,379 @@
+/*
+ * 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.flags;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A class for querying constants from the system - primarily booleans.
+ *
+ * Clients using this class can define their flags and their default values in one place,
+ * can override those values on running devices for debugging and testing purposes, and can control
+ * what flags are available to be used on release builds.
+ *
+ * TODO(b/279054964): A lot. This is skeleton code right now.
+ * @hide
+ */
+public class FeatureFlags {
+    private static final String TAG = "FeatureFlags";
+    private static FeatureFlags sInstance;
+    private static final Object sInstanceLock = new Object();
+
+    private final Set<Flag<?>> mKnownFlags = new ArraySet<>();
+    private final Set<Flag<?>> mDirtyFlags = new ArraySet<>();
+
+    private IFeatureFlags mIFeatureFlags;
+    private final Map<String, Map<String, Boolean>> mBooleanOverrides = new HashMap<>();
+    private final Set<ChangeListener> mListeners = new HashSet<>();
+
+    /**
+     * Obtain a per-process instance of FeatureFlags.
+     * @return A singleton instance of {@link FeatureFlags}.
+     */
+    @NonNull
+    public static FeatureFlags getInstance() {
+        synchronized (sInstanceLock) {
+            if (sInstance == null) {
+                sInstance = new FeatureFlags();
+            }
+        }
+
+        return sInstance;
+    }
+
+    /** See {@link FeatureFlagsFake}. */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    public static void setInstance(FeatureFlags instance) {
+        synchronized (sInstanceLock) {
+            sInstance = instance;
+        }
+    }
+
+    private final IFeatureFlagsCallback mIFeatureFlagsCallback = new IFeatureFlagsCallback.Stub() {
+        @Override
+        public void onFlagChange(SyncableFlag flag) {
+            for (Flag<?> f : mKnownFlags) {
+                if (flagEqualsSyncableFlag(f, flag)) {
+                    if (f instanceof DynamicFlag<?>) {
+                        if (f instanceof DynamicBooleanFlag) {
+                            String value = flag.getValue();
+                            if (value == null) {  // Null means any existing overrides were erased.
+                                value = ((DynamicBooleanFlag) f).getDefault().toString();
+                            }
+                            addBooleanOverride(flag.getNamespace(), flag.getName(), value);
+                        }
+                        FeatureFlags.this.onFlagChange((DynamicFlag<?>) f);
+                    }
+                    break;
+                }
+            }
+        }
+    };
+
+    private FeatureFlags() {
+        this(null);
+    }
+
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    public FeatureFlags(IFeatureFlags iFeatureFlags) {
+        mIFeatureFlags = iFeatureFlags;
+
+        if (mIFeatureFlags != null) {
+            try {
+                mIFeatureFlags.registerCallback(mIFeatureFlagsCallback);
+            } catch (RemoteException e) {
+                // Shouldn't happen with things passed into tests.
+                Log.e(TAG, "Could not register callbacks!", e);
+            }
+        }
+    }
+
+    /**
+     * Construct a new {@link BooleanFlag}.
+     *
+     * Use this instead of constructing a {@link BooleanFlag} directly, as it registers the flag
+     * with the internals of the flagging system.
+     */
+    @NonNull
+    public static BooleanFlag booleanFlag(
+            @NonNull String namespace, @NonNull String name, boolean def) {
+        return getInstance().addFlag(new BooleanFlag(namespace, name, def));
+    }
+
+    /**
+     * Construct a new {@link FusedOffFlag}.
+     *
+     * Use this instead of constructing a {@link FusedOffFlag} directly, as it registers the
+     * flag with the internals of the flagging system.
+     */
+    @NonNull
+    public static FusedOffFlag fusedOffFlag(@NonNull String namespace, @NonNull String name) {
+        return getInstance().addFlag(new FusedOffFlag(namespace, name));
+    }
+
+    /**
+     * Construct a new {@link FusedOnFlag}.
+     *
+     * Use this instead of constructing a {@link FusedOnFlag} directly, as it registers the flag
+     * with the internals of the flagging system.
+     */
+    @NonNull
+    public static FusedOnFlag fusedOnFlag(@NonNull String namespace, @NonNull String name) {
+        return getInstance().addFlag(new FusedOnFlag(namespace, name));
+    }
+
+    /**
+     * Construct a new {@link DynamicBooleanFlag}.
+     *
+     * Use this instead of constructing a {@link DynamicBooleanFlag} directly, as it registers
+     * the flag with the internals of the flagging system.
+     */
+    @NonNull
+    public static DynamicBooleanFlag dynamicBooleanFlag(
+            @NonNull String namespace, @NonNull String name, boolean def) {
+        return getInstance().addFlag(new DynamicBooleanFlag(namespace, name, def));
+    }
+
+    /**
+     * Add a listener to be alerted when a {@link DynamicFlag} changes.
+     *
+     * See also {@link #removeChangeListener(ChangeListener)}.
+     *
+     * @param listener The listener to add.
+     */
+    public void addChangeListener(@NonNull ChangeListener listener) {
+        mListeners.add(listener);
+    }
+
+    /**
+     * Remove a listener that was added earlier.
+     *
+     * See also {@link #addChangeListener(ChangeListener)}.
+     *
+     * @param listener The listener to remove.
+     */
+    public void removeChangeListener(@NonNull ChangeListener listener) {
+        mListeners.remove(listener);
+    }
+
+    protected void onFlagChange(@NonNull DynamicFlag<?> flag) {
+        for (ChangeListener l : mListeners) {
+            l.onFlagChanged(flag);
+        }
+    }
+
+    /**
+     * Returns whether the supplied flag is true or not.
+     *
+     * {@link BooleanFlag} should only be used in debug builds. They do not get optimized out.
+     *
+     * The first time a flag is read, its value is cached for the lifetime of the process.
+     */
+    public boolean isEnabled(@NonNull BooleanFlag flag) {
+        return getBooleanInternal(flag);
+    }
+
+    /**
+     * Returns whether the supplied flag is true or not.
+     *
+     * Always returns false.
+     */
+    public boolean isEnabled(@NonNull FusedOffFlag flag) {
+        return false;
+    }
+
+    /**
+     * Returns whether the supplied flag is true or not.
+     *
+     * Always returns true;
+     */
+    public boolean isEnabled(@NonNull FusedOnFlag flag) {
+        return true;
+    }
+
+    /**
+     * Returns whether the supplied flag is true or not.
+     *
+     * Can return a different value for the flag each time it is called if an override comes in.
+     */
+    public boolean isCurrentlyEnabled(@NonNull DynamicBooleanFlag flag) {
+        return getBooleanInternal(flag);
+    }
+
+    private boolean getBooleanInternal(Flag<Boolean> flag) {
+        sync();
+        Map<String, Boolean> ns = mBooleanOverrides.get(flag.getNamespace());
+        Boolean value = null;
+        if (ns != null) {
+            value = ns.get(flag.getName());
+        }
+        if (value == null) {
+            throw new IllegalStateException("Boolean flag being read but was not synced: " + flag);
+        }
+
+        return value;
+    }
+
+    private <T extends Flag<?>> T addFlag(T flag)  {
+        synchronized (FeatureFlags.class) {
+            mDirtyFlags.add(flag);
+            mKnownFlags.add(flag);
+        }
+        return flag;
+    }
+
+    /**
+     * Sync any known flags that have not yet been synced.
+     *
+     * This is called implicitly when any flag is read, and is not generally needed except in
+     * exceptional circumstances.
+     */
+    public void sync() {
+        synchronized (FeatureFlags.class) {
+            if (mDirtyFlags.isEmpty()) {
+                return;
+            }
+            syncInternal(mDirtyFlags);
+            mDirtyFlags.clear();
+        }
+    }
+
+    /**
+     * Called when new flags have been declared. Gives the implementation a chance to act on them.
+     *
+     * Guaranteed to be called from a synchronized, thread-safe context.
+     */
+    protected void syncInternal(Set<Flag<?>> dirtyFlags) {
+        IFeatureFlags iFeatureFlags = bind();
+        List<SyncableFlag> syncableFlags = new ArrayList<>();
+        for (Flag<?> f : dirtyFlags) {
+            syncableFlags.add(flagToSyncableFlag(f));
+        }
+
+        List<SyncableFlag> serverFlags = List.of();  // Need to initialize the list with something.
+        try {
+            // New values come back from the service.
+            serverFlags = iFeatureFlags.syncFlags(syncableFlags);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+
+        for (Flag<?> f : dirtyFlags) {
+            boolean found = false;
+            for (SyncableFlag sf : serverFlags) {
+                if (flagEqualsSyncableFlag(f, sf)) {
+                    if (f instanceof BooleanFlag || f instanceof DynamicBooleanFlag) {
+                        addBooleanOverride(sf.getNamespace(), sf.getName(), sf.getValue());
+                    }
+                    found = true;
+                    break;
+                }
+            }
+            if (!found) {
+                if (f instanceof BooleanFlag) {
+                    addBooleanOverride(
+                            f.getNamespace(),
+                            f.getName(),
+                            ((BooleanFlag) f).getDefault() ? "true" : "false");
+                }
+            }
+        }
+    }
+
+    private void addBooleanOverride(String namespace, String name, String override) {
+        Map<String, Boolean> nsOverrides = mBooleanOverrides.get(namespace);
+        if (nsOverrides == null) {
+            nsOverrides = new HashMap<>();
+            mBooleanOverrides.put(namespace, nsOverrides);
+        }
+        nsOverrides.put(name, parseBoolean(override));
+    }
+
+    private SyncableFlag flagToSyncableFlag(Flag<?> f) {
+        return new SyncableFlag(
+                f.getNamespace(),
+                f.getName(),
+                f.getDefault().toString(),
+                f instanceof DynamicFlag<?>);
+    }
+
+    private IFeatureFlags bind() {
+        if (mIFeatureFlags == null) {
+            mIFeatureFlags = IFeatureFlags.Stub.asInterface(
+                    ServiceManager.getService(Context.FEATURE_FLAGS_SERVICE));
+            try {
+                mIFeatureFlags.registerCallback(mIFeatureFlagsCallback);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Failed to listen for flag changes!");
+            }
+        }
+
+        return mIFeatureFlags;
+    }
+
+    static boolean parseBoolean(String value) {
+        // Check for a truish string.
+        boolean result = value.equalsIgnoreCase("true")
+                || value.equals("1")
+                || value.equalsIgnoreCase("t")
+                || value.equalsIgnoreCase("on");
+        if (!result) {  // Expect a falsish string, else log an error.
+            if (!(value.equalsIgnoreCase("false")
+                    || value.equals("0")
+                    || value.equalsIgnoreCase("f")
+                    || value.equalsIgnoreCase("off"))) {
+                Log.e(TAG,
+                        "Tried parsing " + value + " as boolean but it doesn't look like one. "
+                                + "Value expected to be one of true|false, 1|0, t|f, on|off.");
+            }
+        }
+        return result;
+    }
+
+    private static boolean flagEqualsSyncableFlag(Flag<?> f, SyncableFlag sf) {
+        return f.getName().equals(sf.getName()) && f.getNamespace().equals(sf.getNamespace());
+    }
+
+
+    /**
+     * A simpler listener that is alerted when a {@link DynamicFlag} changes.
+     *
+     * See {@link #addChangeListener(ChangeListener)}
+     */
+    public interface ChangeListener {
+        /**
+         * Called when a {@link DynamicFlag} changes.
+         *
+         * @param flag The flag that has changed.
+         */
+        void onFlagChanged(DynamicFlag<?> flag);
+    }
+}
diff --git a/core/java/android/flags/FeatureFlagsFake.java b/core/java/android/flags/FeatureFlagsFake.java
new file mode 100644
index 0000000..daedcda
--- /dev/null
+++ b/core/java/android/flags/FeatureFlagsFake.java
@@ -0,0 +1,115 @@
+/*
+ * 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.flags;
+
+import android.annotation.NonNull;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * An implementation of {@link FeatureFlags} for testing.
+ *
+ * Before you read a flag from using this Fake, you must set that flag using
+ * {@link #setFlagValue(BooleanFlagBase, boolean)}. This ensures that your tests are deterministic.
+ *
+ * If you are relying on {@link FeatureFlags#getInstance()} to access FeatureFlags in your code
+ * under test, (instead of dependency injection), you can pass an instance of this fake to
+ * {@link FeatureFlags#setInstance(FeatureFlags)}. Be sure to call that method again, passing null,
+ * to ensure hermetic testing - you don't want static state persisting between your test methods.
+ *
+ * @hide
+ */
+public class FeatureFlagsFake extends FeatureFlags {
+    private final Map<BooleanFlagBase, Boolean> mFlagValues = new HashMap<>();
+    private final Set<BooleanFlagBase> mReadFlags = new HashSet<>();
+
+    public FeatureFlagsFake(IFeatureFlags iFeatureFlags) {
+        super(iFeatureFlags);
+    }
+
+    @Override
+    public boolean isEnabled(@NonNull BooleanFlag flag) {
+        return requireFlag(flag);
+    }
+
+    @Override
+    public boolean isEnabled(@NonNull FusedOffFlag flag) {
+        return requireFlag(flag);
+    }
+
+    @Override
+    public boolean isEnabled(@NonNull FusedOnFlag flag) {
+        return requireFlag(flag);
+    }
+
+    @Override
+    public boolean isCurrentlyEnabled(@NonNull DynamicBooleanFlag flag) {
+        return requireFlag(flag);
+    }
+
+    @Override
+    protected void syncInternal(Set<Flag<?>> dirtyFlags) {
+    }
+
+    /**
+     * Explicitly set a flag's value for reading in tests.
+     *
+     * You _must_ call this for every flag your code-under-test will read. Otherwise, an
+     * {@link IllegalStateException} will be thrown.
+     *
+     * You are able to set values for {@link FusedOffFlag} and {@link FusedOnFlag}, despite those
+     * flags having a fixed value at compile time, since unit tests should still test the state of
+     * those flags as both true and false. I.e. a flag that is off might be turned on in a future
+     * build or vice versa.
+     *
+     * You can not call this method _after_ a non-dynamic flag has been read. Non-dynamic flags
+     * are held stable in the system, so changing a value after reading would not match
+     * real-implementation behavior.
+     *
+     * Calling this method will trigger any {@link android.flags.FeatureFlags.ChangeListener}s that
+     * are registered for the supplied flag if the flag is a {@link DynamicFlag}.
+     *
+     * @param flag  The BooleanFlag that you want to set a value for.
+     * @param value The value that the flag should return when accessed.
+     */
+    public void setFlagValue(@NonNull BooleanFlagBase flag, boolean value) {
+        if (!(flag instanceof DynamicBooleanFlag) && mReadFlags.contains(flag)) {
+            throw new RuntimeException(
+                    "You can not set the value of a flag after it has been read. Tried to set "
+                            + flag + " to " + value + " but it already " + mFlagValues.get(flag));
+        }
+        mFlagValues.put(flag, value);
+        if (flag instanceof DynamicBooleanFlag) {
+            onFlagChange((DynamicFlag<?>) flag);
+        }
+    }
+
+    private boolean requireFlag(BooleanFlagBase flag) {
+        if (!mFlagValues.containsKey(flag)) {
+            throw new IllegalStateException(
+                    "Tried to access " + flag + " in test but no overrided specified. You must "
+                            + "call #setFlagValue for each flag read in a test.");
+        }
+        mReadFlags.add(flag);
+
+        return mFlagValues.get(flag);
+    }
+
+}
diff --git a/core/java/android/flags/Flag.java b/core/java/android/flags/Flag.java
new file mode 100644
index 0000000..b97a4c8
--- /dev/null
+++ b/core/java/android/flags/Flag.java
@@ -0,0 +1,82 @@
+/*
+ * 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.flags;
+
+import android.annotation.NonNull;
+
+/**
+ * Base class for constants read via {@link android.flags.FeatureFlags}.
+ *
+ * @param <T> The type of value that this flag stores. E.g. Boolean or String.
+ *
+ * @hide
+ */
+public interface Flag<T> {
+    /** The namespace for a flag. Should combine uniquely with its name. */
+    @NonNull
+    String getNamespace();
+
+    /** The name of the flag. Should combine uniquely with its namespace. */
+    @NonNull
+    String getName();
+
+    /** The value of this flag if no override has been set. Null values are not supported. */
+    @NonNull
+    T getDefault();
+
+    /** Returns true if the value of this flag can change at runtime. */
+    default boolean isDynamic() {
+        return false;
+    }
+
+    /**
+     * Add human-readable details to the flag. Flag client's are not required to set this.
+     *
+     * See {@link #getLabel()}, {@link #getDescription()}, and {@link #getCategoryName()}.
+     *
+     * @return Returns `this`, to make a fluent api.
+     */
+    Flag<T> defineMetaData(String label, String description, String categoryName);
+
+    /**
+     * A human-readable name for the flag. Defaults to {@link #getName()}
+     *
+     * See {@link #defineMetaData(String, String, String)}
+     */
+    @NonNull
+    default String getLabel() {
+        return getName();
+    }
+
+    /**
+     * A human-readable description for the flag. Defaults to null if unset.
+     *
+     * See {@link #defineMetaData(String, String, String)}
+     */
+    default String getDescription() {
+        return null;
+    }
+
+    /**
+     * A human-readable category name for the flag. Defaults to null if unset.
+     *
+     * See {@link #defineMetaData(String, String, String)}
+     */
+    default String getCategoryName() {
+        return null;
+    }
+}
diff --git a/core/java/android/flags/FusedOffFlag.java b/core/java/android/flags/FusedOffFlag.java
new file mode 100644
index 0000000..6844b8f
--- /dev/null
+++ b/core/java/android/flags/FusedOffFlag.java
@@ -0,0 +1,49 @@
+/*
+ * 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.flags;
+
+import android.annotation.NonNull;
+import android.provider.DeviceConfig;
+
+/**
+ * A flag representing a false value.
+ *
+ * The flag can never be changed or overridden. It is false at compile time.
+ *
+ * @hide
+ */
+public final class FusedOffFlag extends BooleanFlagBase {
+    /**
+     * @param namespace A namespace for this flag. See {@link DeviceConfig}.
+     * @param name      A name for this flag.
+     */
+    FusedOffFlag(String namespace, String name) {
+        super(namespace, name);
+    }
+
+    @Override
+    @NonNull
+    public Boolean getDefault() {
+        return false;
+    }
+
+    @Override
+    public FusedOffFlag defineMetaData(String label, String description, String categoryName) {
+        super.defineMetaData(label, description, categoryName);
+        return this;
+    }
+}
diff --git a/core/java/android/flags/FusedOnFlag.java b/core/java/android/flags/FusedOnFlag.java
new file mode 100644
index 0000000..e9adba7
--- /dev/null
+++ b/core/java/android/flags/FusedOnFlag.java
@@ -0,0 +1,49 @@
+/*
+ * 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.flags;
+
+import android.annotation.NonNull;
+import android.provider.DeviceConfig;
+
+/**
+ * A flag representing a true value.
+ *
+ * The flag can never be changed or overridden. It is true at compile time.
+ *
+ * @hide
+ */
+public final class FusedOnFlag extends BooleanFlagBase {
+    /**
+     * @param namespace A namespace for this flag. See {@link DeviceConfig}.
+     * @param name      A name for this flag.
+     */
+    FusedOnFlag(String namespace, String name) {
+        super(namespace, name);
+    }
+
+    @Override
+    @NonNull
+    public Boolean getDefault() {
+        return true;
+    }
+
+    @Override
+    public FusedOnFlag defineMetaData(String label, String description, String categoryName) {
+        super.defineMetaData(label, description, categoryName);
+        return this;
+    }
+}
diff --git a/core/java/android/flags/IFeatureFlags.aidl b/core/java/android/flags/IFeatureFlags.aidl
new file mode 100644
index 0000000..3efcec9
--- /dev/null
+++ b/core/java/android/flags/IFeatureFlags.aidl
@@ -0,0 +1,89 @@
+/*
+ * 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.flags;
+
+import android.flags.IFeatureFlagsCallback;
+import android.flags.SyncableFlag;
+
+/**
+ * Binder interface for communicating with {@link com.android.server.flags.FeatureFlagsService}.
+ *
+ * This interface is used by {@link android.flags.FeatureFlags} and developers should use that to
+ * interface with the service. FeatureFlags is the "client" in this documentation.
+ *
+ * The methods allow client apps to communicate what flags they care about, and receive back
+ * current values for those flags. For stable flags, this is the finalized value until the device
+ * restarts. For {@link DynamicFlag}s, this is the last known value, though it may change in the
+ * future. Clients can listen for changes to flag values so that it can react accordingly.
+ * @hide
+ */
+interface IFeatureFlags {
+    /**
+     * Synchronize with the {@link com.android.server.flags.FeatureFlagsService} about flags of
+     * interest.
+     *
+     * The client should pass in a list of flags that it is using as {@link SyncableFlag}s, which
+     * includes what it thinks the default values of the flags are.
+     *
+     * The response will contain a list of matching SyncableFlags, whose values are set to what the
+     * value of the flags actually are. The client should update its internal state flag data to
+     * match.
+     *
+     * Generally speaking, if a flag that is passed in is new to the FeatureFlagsService, the
+     * service will cache the passed-in value, and return it back out. If, however, a different
+     * client has synced that flag with the service previously, FeatureFlagsService will return the
+     * existing cached value, which may or may not be what the current client passed in. This allows
+     * FeatureFlagsService to keep clients in agreement with one another.
+     */
+    List<SyncableFlag> syncFlags(in List<SyncableFlag> flagList);
+
+    /**
+     * Pass in an {@link IFeatureFlagsCallback} that will be called whenever a {@link DymamicFlag}
+     * changes.
+     */
+    void registerCallback(IFeatureFlagsCallback callback);
+
+    /**
+     * Remove a {@link IFeatureFlagsCallback} that was previously registered with
+     * {@link #registerCallback}.
+     */
+    void unregisterCallback(IFeatureFlagsCallback callback);
+
+    /**
+     * Query the {@link com.android.server.flags.FeatureFlagsService} for flags, but don't
+     * cache them. See {@link #syncFlags}.
+     *
+     * You almost certainly don't want this method. This is intended for the Flag Flipper
+     * application that needs to query the state of system but doesn't want to affect it by
+     * doing so. All other clients should use {@link syncFlags}.
+     */
+    List<SyncableFlag> queryFlags(in List<SyncableFlag> flagList);
+
+    /**
+     * Change a flags value in the system.
+     *
+     * This is intended for use by the Flag Flipper application.
+     */
+    void overrideFlag(in SyncableFlag flag);
+
+    /**
+     * Restore a flag to its default value.
+     *
+     * This is intended for use by the Flag Flipper application.
+     */
+    void resetFlag(in SyncableFlag flag);
+}
\ No newline at end of file
diff --git a/core/java/android/flags/IFeatureFlagsCallback.aidl b/core/java/android/flags/IFeatureFlagsCallback.aidl
new file mode 100644
index 0000000..f708667
--- /dev/null
+++ b/core/java/android/flags/IFeatureFlagsCallback.aidl
@@ -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.flags;
+
+import android.flags.SyncableFlag;
+
+/**
+ * Callback for {@link IFeatureFlags#registerCallback} to get alerts when a {@link DynamicFlag}
+ * changes.
+ *
+ * DynamicFlags can change at run time. Stable flags will never result in a call to this method.
+ *
+ * @hide
+ */
+oneway interface IFeatureFlagsCallback {
+    void onFlagChange(in SyncableFlag flag);
+}
\ No newline at end of file
diff --git a/core/java/android/flags/OWNERS b/core/java/android/flags/OWNERS
new file mode 100644
index 0000000..fa125c4
--- /dev/null
+++ b/core/java/android/flags/OWNERS
@@ -0,0 +1,7 @@
+# Bug component: 1306523
+
+mankoff@google.com
+pixel@google.com
+
+dsandler@android.com
+
diff --git a/core/java/android/flags/SyncableFlag.aidl b/core/java/android/flags/SyncableFlag.aidl
new file mode 100644
index 0000000..1526ec1
--- /dev/null
+++ b/core/java/android/flags/SyncableFlag.aidl
@@ -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 android.flags;
+
+/**
+ * A parcelable data class for serializing {@link Flag} across a Binder.
+ */
+parcelable SyncableFlag;
\ No newline at end of file
diff --git a/core/java/android/flags/SyncableFlag.java b/core/java/android/flags/SyncableFlag.java
new file mode 100644
index 0000000..449bcc3c
--- /dev/null
+++ b/core/java/android/flags/SyncableFlag.java
@@ -0,0 +1,112 @@
+/*
+ * 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.flags;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * @hide
+ */
+public final class SyncableFlag implements Parcelable {
+    private final String mNamespace;
+    private final String mName;
+    private final String mValue;
+    private final boolean mDynamic;
+    private final boolean mOverridden;
+
+    public SyncableFlag(
+            @NonNull String namespace,
+            @NonNull String name,
+            @NonNull String value,
+            boolean dynamic) {
+        this(namespace, name, value, dynamic, false);
+    }
+
+    public SyncableFlag(
+            @NonNull String namespace,
+            @NonNull String name,
+            @NonNull String value,
+            boolean dynamic,
+            boolean overridden
+    ) {
+        mNamespace = namespace;
+        mName = name;
+        mValue = value;
+        mDynamic = dynamic;
+        mOverridden = overridden;
+    }
+
+    @NonNull
+    public String getNamespace() {
+        return mNamespace;
+    }
+
+    @NonNull
+    public String getName() {
+        return mName;
+    }
+
+    @NonNull
+    public String getValue() {
+        return mValue;
+    }
+
+    public boolean isDynamic() {
+        return mDynamic;
+    }
+
+    public boolean isOverridden() {
+        return mOverridden;
+    }
+
+    @NonNull
+    public static final Parcelable.Creator<SyncableFlag> CREATOR = new Parcelable.Creator<>() {
+        public SyncableFlag createFromParcel(Parcel in) {
+            return new SyncableFlag(
+                    in.readString(),
+                    in.readString(),
+                    in.readString(),
+                    in.readBoolean(),
+                    in.readBoolean());
+        }
+
+        public SyncableFlag[] newArray(int size) {
+            return new SyncableFlag[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeString(mNamespace);
+        dest.writeString(mName);
+        dest.writeString(mValue);
+        dest.writeBoolean(mDynamic);
+        dest.writeBoolean(mOverridden);
+    }
+
+    @Override
+    public String toString() {
+        return getNamespace() + "." + getName() + "[" + getValue() + "]";
+    }
+}
diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java
index 2e40f60..912e8df 100644
--- a/core/java/android/hardware/biometrics/BiometricPrompt.java
+++ b/core/java/android/hardware/biometrics/BiometricPrompt.java
@@ -144,6 +144,7 @@
         private Context mContext;
         private IAuthService mService;
 
+        // LINT.IfChange
         /**
          * Creates a builder for a {@link BiometricPrompt} dialog.
          * @param context The {@link Context} that will be used to build the prompt.
@@ -417,6 +418,7 @@
          * @hide
          */
         @NonNull
+        @RequiresPermission(anyOf = {USE_BIOMETRIC_INTERNAL})
         public Builder setDisallowBiometricsIfPolicyExists(boolean checkDevicePolicyManager) {
             mPromptInfo.setDisallowBiometricsIfPolicyExists(checkDevicePolicyManager);
             return this;
@@ -429,6 +431,7 @@
          * @hide
          */
         @NonNull
+        @RequiresPermission(anyOf = {USE_BIOMETRIC_INTERNAL})
         public Builder setReceiveSystemEvents(boolean set) {
             mPromptInfo.setReceiveSystemEvents(set);
             return this;
@@ -442,6 +445,7 @@
          * @hide
          */
         @NonNull
+        @RequiresPermission(anyOf = {TEST_BIOMETRIC, USE_BIOMETRIC_INTERNAL})
         public Builder setIgnoreEnrollmentState(boolean ignoreEnrollmentState) {
             mPromptInfo.setIgnoreEnrollmentState(ignoreEnrollmentState);
             return this;
@@ -454,10 +458,12 @@
          * @hide
          */
         @NonNull
+        @RequiresPermission(anyOf = {TEST_BIOMETRIC, USE_BIOMETRIC_INTERNAL})
         public Builder setIsForLegacyFingerprintManager(int sensorId) {
             mPromptInfo.setIsForLegacyFingerprintManager(sensorId);
             return this;
         }
+        // LINT.ThenChange(frameworks/base/core/java/android/hardware/biometrics/PromptInfo.java)
 
         /**
          * Creates a {@link BiometricPrompt}.
diff --git a/core/java/android/hardware/biometrics/PromptInfo.java b/core/java/android/hardware/biometrics/PromptInfo.java
index 02aad1d..e275078 100644
--- a/core/java/android/hardware/biometrics/PromptInfo.java
+++ b/core/java/android/hardware/biometrics/PromptInfo.java
@@ -113,6 +113,7 @@
         dest.writeBoolean(mIsForLegacyFingerprintManager);
     }
 
+    // LINT.IfChange
     public boolean containsTestConfigurations() {
         if (mIsForLegacyFingerprintManager
                 && mAllowedSensorIds.size() == 1
@@ -122,6 +123,10 @@
             return true;
         } else if (mAllowBackgroundAuthentication) {
             return true;
+        } else if (mIsForLegacyFingerprintManager) {
+            return true;
+        } else if (mIgnoreEnrollmentState) {
+            return true;
         }
         return false;
     }
@@ -144,6 +149,7 @@
         }
         return false;
     }
+    // LINT.ThenChange(frameworks/base/core/java/android/hardware/biometrics/BiometricPrompt.java)
 
     // Setters
 
diff --git a/core/java/android/os/vibrator/persistence/VibrationXmlParser.java b/core/java/android/os/vibrator/persistence/VibrationXmlParser.java
index d41a428..e91e04e 100644
--- a/core/java/android/os/vibrator/persistence/VibrationXmlParser.java
+++ b/core/java/android/os/vibrator/persistence/VibrationXmlParser.java
@@ -28,7 +28,6 @@
 import com.android.internal.vibrator.persistence.XmlConstants;
 import com.android.internal.vibrator.persistence.XmlParserException;
 import com.android.internal.vibrator.persistence.XmlReader;
-import com.android.internal.vibrator.persistence.XmlSerializedVibration;
 import com.android.modules.utils.TypedXmlPullParser;
 
 import org.xmlpull.v1.XmlPullParser;
@@ -178,25 +177,62 @@
             // Ensure XML starts with expected root tag.
             XmlReader.readDocumentStartTag(parser, XmlConstants.TAG_VIBRATION);
 
-            int parserFlags = 0;
-            if ((flags & FLAG_ALLOW_HIDDEN_APIS) != 0) {
-                parserFlags |= XmlConstants.FLAG_ALLOW_HIDDEN_APIS;
-            }
-
             // Parse root tag as a vibration effect.
-            XmlSerializedVibration<VibrationEffect> serializedVibration =
-                    VibrationEffectXmlParser.parseTag(parser, parserFlags);
+            VibrationEffect effect = parseTag(parser, flags);
 
             // Ensure XML ends after root tag is consumed.
             XmlReader.readDocumentEndTag(parser);
 
-            return serializedVibration.deserialize();
-        } catch (XmlParserException e) {
+            return effect;
+        } catch (XmlParserException | VibrationXmlParserException e) {
             Slog.w(TAG, "Error parsing vibration XML", e);
             return null;
         }
     }
 
+    /**
+     * Parses XML content from given open {@link TypedXmlPullParser} into a {@link VibrationEffect}.
+     *
+     * <p>The provided parser should be pointing to a start of a valid vibration XML (i.e. to a
+     * start <vibration> tag). No other parser position, including start of document, is considered
+     * valid.
+     *
+     * <p>This method parses as long as it reads a valid vibration XML, and until an end vibration
+     * tag. After a successful parsing, the parser will point to the end vibration tag (i.e. to a
+     * </vibration> tag).
+     *
+     * @throws IOException error parsing from given {@link TypedXmlPullParser}.
+     * @throws VibrationXmlParserException if the XML tag cannot be parsed into a
+     *      {@link VibrationEffect}. The given {@code parser} might be pointing to a child XML tag
+     *      that caused the parser failure.
+     *
+     * @hide
+     */
+    @NonNull
+    public static VibrationEffect parseTag(@NonNull TypedXmlPullParser parser, @Flags int flags)
+            throws IOException, VibrationXmlParserException {
+        int parserFlags = 0;
+        if ((flags & VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS) != 0) {
+            parserFlags |= XmlConstants.FLAG_ALLOW_HIDDEN_APIS;
+        }
+        try {
+            return VibrationEffectXmlParser.parseTag(parser, parserFlags).deserialize();
+        } catch (XmlParserException e) {
+            throw new VibrationXmlParserException("Error parsing vibration effect.", e);
+        }
+    }
+
+    /**
+     * Represents an error while parsing a vibration XML input.
+     *
+     * @hide
+     */
+    public static final class VibrationXmlParserException extends Exception {
+        private VibrationXmlParserException(String message, Throwable cause) {
+            super(message, cause);
+        }
+    }
+
     private VibrationXmlParser() {
     }
 }
diff --git a/core/java/android/print/OWNERS b/core/java/android/print/OWNERS
index 28a24203..0809de2 100644
--- a/core/java/android/print/OWNERS
+++ b/core/java/android/print/OWNERS
@@ -1,4 +1,4 @@
 # Bug component: 47273
 
-svetoslavganov@android.com
-svetoslavganov@google.com
+anothermark@google.com
+kumarashishg@google.com
diff --git a/core/java/android/print/pdf/OWNERS b/core/java/android/print/pdf/OWNERS
deleted file mode 100644
index 28a24203..0000000
--- a/core/java/android/print/pdf/OWNERS
+++ /dev/null
@@ -1,4 +0,0 @@
-# Bug component: 47273
-
-svetoslavganov@android.com
-svetoslavganov@google.com
diff --git a/core/java/android/printservice/OWNERS b/core/java/android/printservice/OWNERS
index 28a24203..0809de2 100644
--- a/core/java/android/printservice/OWNERS
+++ b/core/java/android/printservice/OWNERS
@@ -1,4 +1,4 @@
 # Bug component: 47273
 
-svetoslavganov@android.com
-svetoslavganov@google.com
+anothermark@google.com
+kumarashishg@google.com
diff --git a/core/java/android/printservice/recommendation/OWNERS b/core/java/android/printservice/recommendation/OWNERS
deleted file mode 100644
index 28a24203..0000000
--- a/core/java/android/printservice/recommendation/OWNERS
+++ /dev/null
@@ -1,4 +0,0 @@
-# Bug component: 47273
-
-svetoslavganov@android.com
-svetoslavganov@google.com
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index cc3a662..e47fab4 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -868,6 +868,11 @@
          * This will trigger a {@link #onComputeColors()} call.
          */
         public void notifyColorsChanged() {
+            if (mDestroyed) {
+                Log.i(TAG, "Ignoring notifyColorsChanged(), Engine has already been destroyed.");
+                return;
+            }
+
             final long now = mClockFunction.get();
             if (now - mLastColorInvalidation < NOTIFY_COLORS_RATE_LIMIT_MS) {
                 Log.w(TAG, "This call has been deferred. You should only call "
@@ -2231,7 +2236,11 @@
             }
         }
 
-        void detach() {
+        /**
+         * @hide
+         */
+        @VisibleForTesting
+        public void detach() {
             if (mDestroyed) {
                 return;
             }
@@ -2449,6 +2458,14 @@
         }
 
         public void reportShown() {
+            if (mEngine == null) {
+                Log.i(TAG, "Can't report null engine as shown.");
+                return;
+            }
+            if (mEngine.mDestroyed) {
+                Log.i(TAG, "Engine was destroyed before we could draw.");
+                return;
+            }
             if (!mShownReported) {
                 mShownReported = true;
                 Trace.beginSection("WPMS.mConnection.engineShown");
diff --git a/core/java/android/window/WindowContextController.java b/core/java/android/window/WindowContextController.java
index eb270e2..4b9a957 100644
--- a/core/java/android/window/WindowContextController.java
+++ b/core/java/android/window/WindowContextController.java
@@ -21,7 +21,6 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.app.servertransaction.WindowTokenClientController;
 import android.content.Context;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -105,8 +104,7 @@
             throw new IllegalStateException("A Window Context can be only attached to "
                     + "a DisplayArea once.");
         }
-        mAttachedToDisplayArea = WindowTokenClientController.getInstance().attachToDisplayArea(
-                mToken, type, displayId, options)
+        mAttachedToDisplayArea = mToken.attachToDisplayArea(type, displayId, options)
                 ? AttachStatus.STATUS_ATTACHED : AttachStatus.STATUS_FAILED;
         if (mAttachedToDisplayArea == AttachStatus.STATUS_FAILED) {
             Log.w(TAG, "attachToDisplayArea fail, type:" + type + ", displayId:"
@@ -142,13 +140,13 @@
             throw new IllegalStateException("The Window Context should have been attached"
                     + " to a DisplayArea. AttachToDisplayArea:" + mAttachedToDisplayArea);
         }
-        WindowTokenClientController.getInstance().attachToWindowToken(mToken, windowToken);
+        mToken.attachToWindowToken(windowToken);
     }
 
     /** Detaches the window context from the node it's currently associated with. */
     public void detachIfNeeded() {
         if (mAttachedToDisplayArea == AttachStatus.STATUS_ATTACHED) {
-            WindowTokenClientController.getInstance().detachIfNeeded(mToken);
+            mToken.detachFromWindowContainerIfNeeded();
             mAttachedToDisplayArea = AttachStatus.STATUS_DETACHED;
             if (DEBUG_ATTACH) {
                 Log.d(TAG, "Detach Window Context.");
diff --git a/core/java/android/window/WindowInfosListenerForTest.java b/core/java/android/window/WindowInfosListenerForTest.java
index bdbce13..be88e53 100644
--- a/core/java/android/window/WindowInfosListenerForTest.java
+++ b/core/java/android/window/WindowInfosListenerForTest.java
@@ -20,6 +20,7 @@
 import android.annotation.NonNull;
 import android.annotation.RequiresPermission;
 import android.annotation.TestApi;
+import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.os.IBinder;
 import android.os.InputConfig;
@@ -78,14 +79,21 @@
          */
         public final boolean isVisible;
 
+        /**
+         * Return the transform to get the bounds from display space into window space.
+         */
+        @NonNull
+        public final Matrix transform;
+
         WindowInfo(@NonNull IBinder windowToken, @NonNull String name, int displayId,
-                @NonNull Rect bounds, int inputConfig) {
+                @NonNull Rect bounds, int inputConfig, @NonNull Matrix transform) {
             this.windowToken = windowToken;
             this.name = name;
             this.displayId = displayId;
             this.bounds = bounds;
             this.isTrustedOverlay = (inputConfig & InputConfig.TRUSTED_OVERLAY) != 0;
             this.isVisible = (inputConfig & InputConfig.NOT_VISIBLE) == 0;
+            this.transform = transform;
         }
 
         @Override
@@ -94,7 +102,8 @@
                     + ", frame=" + bounds
                     + ", isVisible=" + isVisible
                     + ", isTrustedOverlay=" + isTrustedOverlay
-                    + ", token=" + windowToken;
+                    + ", token=" + windowToken
+                    + ", transform=" + transform;
         }
     }
 
@@ -155,7 +164,7 @@
             var bounds = new Rect(handle.frameLeft, handle.frameTop, handle.frameRight,
                     handle.frameBottom);
             windowInfos.add(new WindowInfo(handle.getWindowToken(), handle.name, handle.displayId,
-                    bounds, handle.inputConfig));
+                    bounds, handle.inputConfig, handle.transform));
         }
         return windowInfos;
     }
diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java
index 632208c..849e0b3 100644
--- a/core/java/android/window/WindowOnBackInvokedDispatcher.java
+++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java
@@ -23,6 +23,7 @@
 import android.content.ContextWrapper;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
+import android.content.res.TypedArray;
 import android.os.Handler;
 import android.os.RemoteException;
 import android.os.SystemProperties;
@@ -33,6 +34,7 @@
 
 import androidx.annotation.VisibleForTesting;
 
+
 import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
@@ -62,6 +64,9 @@
             .getInt("persist.wm.debug.predictive_back", 1) != 0;
     private static final boolean ALWAYS_ENFORCE_PREDICTIVE_BACK = SystemProperties
             .getInt("persist.wm.debug.predictive_back_always_enforce", 0) != 0;
+    private static final boolean PREDICTIVE_BACK_FALLBACK_WINDOW_ATTRIBUTE =
+            SystemProperties.getInt("persist.wm.debug.predictive_back_fallback_window_attribute", 0)
+                    != 0;
     @Nullable
     private ImeOnBackInvokedDispatcher mImeDispatcher;
 
@@ -500,6 +505,31 @@
                             applicationInfo.packageName,
                             requestsPredictiveBack));
                 }
+
+                if (PREDICTIVE_BACK_FALLBACK_WINDOW_ATTRIBUTE && !requestsPredictiveBack) {
+                    // Compatibility check for legacy window style flag used by Wear OS.
+                    // Note on compatibility behavior:
+                    // 1. windowSwipeToDismiss should be respected for all apps not opted in.
+                    // 2. windowSwipeToDismiss should be true for all apps not opted in, which
+                    //    enables the PB animation for them.
+                    // 3. windowSwipeToDismiss=false should be respected for apps not opted in,
+                    //    which disables PB & onBackPressed caused by BackAnimController's
+                    //    setTrigger(true)
+                    TypedArray windowAttr =
+                            context.obtainStyledAttributes(
+                                    new int[] {android.R.attr.windowSwipeToDismiss});
+                    boolean windowSwipeToDismiss = true;
+                    if (windowAttr.getIndexCount() > 0) {
+                        windowSwipeToDismiss = windowAttr.getBoolean(0, true);
+                    }
+                    windowAttr.recycle();
+
+                    if (DEBUG) {
+                        Log.i(TAG, "falling back to windowSwipeToDismiss: " + windowSwipeToDismiss);
+                    }
+
+                    requestsPredictiveBack = windowSwipeToDismiss;
+                }
             }
 
             return requestsPredictiveBack;
diff --git a/core/java/android/window/WindowTokenClient.java b/core/java/android/window/WindowTokenClient.java
index 55b823b..a208634 100644
--- a/core/java/android/window/WindowTokenClient.java
+++ b/core/java/android/window/WindowTokenClient.java
@@ -23,10 +23,10 @@
 import android.annotation.BinderThread;
 import android.annotation.MainThread;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.ActivityThread;
 import android.app.IWindowToken;
 import android.app.ResourcesManager;
-import android.app.servertransaction.WindowTokenClientController;
 import android.content.Context;
 import android.content.res.CompatibilityInfo;
 import android.content.res.Configuration;
@@ -36,9 +36,14 @@
 import android.os.Debug;
 import android.os.Handler;
 import android.os.IBinder;
+import android.os.RemoteException;
 import android.util.Log;
+import android.view.IWindowManager;
+import android.view.WindowManager.LayoutParams.WindowType;
+import android.view.WindowManagerGlobal;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.function.pooled.PooledLambda;
 
 import java.lang.ref.WeakReference;
@@ -65,11 +70,15 @@
 
     private final ResourcesManager mResourcesManager = ResourcesManager.getInstance();
 
+    private IWindowManager mWms;
+
     @GuardedBy("itself")
     private final Configuration mConfiguration = new Configuration();
 
     private boolean mShouldDumpConfigForIme;
 
+    private boolean mAttachToWindowContainer;
+
     private final Handler mHandler = ActivityThread.currentActivityThread().getHandler();
 
     /**
@@ -92,6 +101,88 @@
     }
 
     /**
+     * Attaches this {@link WindowTokenClient} to a {@link com.android.server.wm.DisplayArea}.
+     *
+     * @param type The window type of the {@link WindowContext}
+     * @param displayId The {@link Context#getDisplayId() ID of display} to associate with
+     * @param options The window context launched option
+     * @return {@code true} if attaching successfully.
+     */
+    public boolean attachToDisplayArea(@WindowType int type, int displayId,
+            @Nullable Bundle options) {
+        try {
+            final Configuration configuration = getWindowManagerService()
+                    .attachWindowContextToDisplayArea(this, type, displayId, options);
+            if (configuration == null) {
+                return false;
+            }
+            onConfigurationChanged(configuration, displayId, false /* shouldReportConfigChange */);
+            mAttachToWindowContainer = true;
+            return true;
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Attaches this {@link WindowTokenClient} to a {@code DisplayContent}.
+     *
+     * @param displayId The {@link Context#getDisplayId() ID of display} to associate with
+     * @return {@code true} if attaching successfully.
+     */
+    public boolean attachToDisplayContent(int displayId) {
+        final IWindowManager wms = getWindowManagerService();
+        // #createSystemUiContext may call this method before WindowManagerService is initialized.
+        if (wms == null) {
+            return false;
+        }
+        try {
+            final Configuration configuration = wms.attachToDisplayContent(this, displayId);
+            if (configuration == null) {
+                return false;
+            }
+            onConfigurationChanged(configuration, displayId, false /* shouldReportConfigChange */);
+            mAttachToWindowContainer = true;
+            return true;
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Attaches this {@link WindowTokenClient} to a {@code windowToken}.
+     *
+     * @param windowToken the window token to associated with
+     */
+    public void attachToWindowToken(IBinder windowToken) {
+        try {
+            getWindowManagerService().attachWindowContextToWindowToken(this, windowToken);
+            mAttachToWindowContainer = true;
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** Detaches this {@link WindowTokenClient} from associated WindowContainer if there's one. */
+    public void detachFromWindowContainerIfNeeded() {
+        if (!mAttachToWindowContainer) {
+            return;
+        }
+        try {
+            getWindowManagerService().detachWindowContextFromWindowContainer(this);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    private IWindowManager getWindowManagerService() {
+        if (mWms == null) {
+            mWms = WindowManagerGlobal.getWindowManagerService();
+        }
+        return mWms;
+    }
+
+    /**
      * Called when {@link Configuration} updates from the server side receive.
      *
      * @param newConfig the updated {@link Configuration}
@@ -116,14 +207,15 @@
      * {@code shouldReportConfigChange} is {@code true}, which is usually from
      * {@link IWindowToken#onConfigurationChanged(Configuration, int)}
      * directly, while this method could be run on any thread if it is used to initialize
-     * Context's {@code Configuration} via {@link WindowTokenClientController#attachToDisplayArea}
-     * or {@link WindowTokenClientController#attachToDisplayContent}.
+     * Context's {@code Configuration} via {@link #attachToDisplayArea(int, int, Bundle)}
+     * or {@link #attachToDisplayContent(int)}.
      *
      * @param shouldReportConfigChange {@code true} to indicate that the {@code Configuration}
      *                                 should be dispatched to listeners.
      *
      */
     @AnyThread
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     public void onConfigurationChanged(Configuration newConfig, int newDisplayId,
             boolean shouldReportConfigChange) {
         final Context context = mContextRef.get();
diff --git a/core/java/com/android/internal/flags/CoreFlags.java b/core/java/com/android/internal/flags/CoreFlags.java
new file mode 100644
index 0000000..f177ef8
--- /dev/null
+++ b/core/java/com/android/internal/flags/CoreFlags.java
@@ -0,0 +1,93 @@
+/*
+ * 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.internal.flags;
+
+import android.flags.BooleanFlag;
+import android.flags.DynamicBooleanFlag;
+import android.flags.FeatureFlags;
+import android.flags.FusedOffFlag;
+import android.flags.FusedOnFlag;
+import android.flags.SyncableFlag;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Flags defined here are can be read by code in core.
+ *
+ * Flags not defined here will throw a security exception if third-party processes attempts to read
+ * them.
+ *
+ * DO NOT define a flag here unless you explicitly intend for that flag to be readable by code that
+ * runs inside a third party process.
+ */
+public abstract class CoreFlags {
+    private static final List<SyncableFlag> sKnownFlags = new ArrayList<>();
+
+    public static BooleanFlag BOOL_FLAG = booleanFlag("core", "bool_flag", false);
+    public static FusedOffFlag OFF_FLAG = fusedOffFlag("core", "off_flag");
+    public static FusedOnFlag ON_FLAG = fusedOnFlag("core", "on_flag");
+    public static DynamicBooleanFlag DYN_FLAG = dynamicBooleanFlag("core", "dyn_flag", true);
+
+    /** Returns true if the passed in flag matches a flag in this class. */
+    public static boolean isCoreFlag(SyncableFlag flag) {
+        for (SyncableFlag knownFlag : sKnownFlags) {
+            if (knownFlag.getName().equals(flag.getName())
+                    && knownFlag.getNamespace().equals(flag.getNamespace())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static List<SyncableFlag> getCoreFlags() {
+        return sKnownFlags;
+    }
+
+    private static BooleanFlag booleanFlag(String namespace, String name, boolean defaultValue) {
+        BooleanFlag f = FeatureFlags.booleanFlag(namespace, name, defaultValue);
+
+        sKnownFlags.add(new SyncableFlag(namespace, name, Boolean.toString(defaultValue), false));
+
+        return f;
+    }
+
+    private static FusedOffFlag fusedOffFlag(String namespace, String name) {
+        FusedOffFlag f = FeatureFlags.fusedOffFlag(namespace, name);
+
+        sKnownFlags.add(new SyncableFlag(namespace, name, "false", false));
+
+        return f;
+    }
+
+    private static FusedOnFlag fusedOnFlag(String namespace, String name) {
+        FusedOnFlag f = FeatureFlags.fusedOnFlag(namespace, name);
+
+        sKnownFlags.add(new SyncableFlag(namespace, name, "true", false));
+
+        return f;
+    }
+
+    private static DynamicBooleanFlag dynamicBooleanFlag(
+            String namespace, String name, boolean defaultValue) {
+        DynamicBooleanFlag f = FeatureFlags.dynamicBooleanFlag(namespace, name, defaultValue);
+
+        sKnownFlags.add(new SyncableFlag(namespace, name, Boolean.toString(defaultValue), true));
+
+        return f;
+    }
+}
diff --git a/core/java/com/android/internal/os/TimeoutRecord.java b/core/java/com/android/internal/os/TimeoutRecord.java
index 1d0ba3f..e9a8d4b 100644
--- a/core/java/com/android/internal/os/TimeoutRecord.java
+++ b/core/java/com/android/internal/os/TimeoutRecord.java
@@ -58,6 +58,7 @@
         int APP_REGISTERED = 7;
         int SHORT_FGS_TIMEOUT = 8;
         int JOB_SERVICE = 9;
+        int APP_START = 10;
     }
 
     /** Kind of timeout, e.g. BROADCAST_RECEIVER, etc. */
@@ -190,4 +191,10 @@
     public static TimeoutRecord forJobService(String reason) {
         return TimeoutRecord.endingNow(TimeoutKind.JOB_SERVICE, reason);
     }
+
+    /** Record for app startup timeout. */
+    @NonNull
+    public static TimeoutRecord forAppStart(String reason) {
+        return TimeoutRecord.endingNow(TimeoutKind.APP_START, reason);
+    }
 }
diff --git a/core/proto/android/os/system_properties.proto b/core/proto/android/os/system_properties.proto
index 3cedba0..5a3539a 100644
--- a/core/proto/android/os/system_properties.proto
+++ b/core/proto/android/os/system_properties.proto
@@ -172,7 +172,7 @@
         optional Status tombstoned = 29;
         optional Status ueventd = 30;
         optional Status update_engine = 31;
-        optional Status update_verifier_nonencrypted = 32;
+        optional Status update_verifier = 32;
         optional Status virtual_touchpad = 33;
         optional Status vndservicemanager = 34;
         optional Status vold = 35;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 0c8707d..baa47da 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -7663,6 +7663,24 @@
     <permission android:name="android.permission.GET_ANY_PROVIDER_TYPE"
                 android:protectionLevel="signature" />
 
+
+    <!-- @hide Allows internal applications to read and synchronize non-core flags.
+         Apps without this permission can only read a subset of flags specifically intended
+         for use in "core", (i.e. third party apps). Apps with this permission can define their
+         own flags, and federate those values with other system-level apps.
+         <p>Not for use by third-party applications.
+         <p>Protection level: signature
+    -->
+    <permission android:name="android.permission.SYNC_FLAGS"
+        android:protectionLevel="signature" />
+
+    <!-- @hide Allows internal applications to override flags in the FeatureFlags service.
+         <p>Not for use by third-party applications.
+         <p>Protection level: signature
+    -->
+    <permission android:name="android.permission.WRITE_FLAGS"
+        android:protectionLevel="signature" />
+
     <!-- Attribution for Geofencing service. -->
     <attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/>
     <!-- Attribution for Country Detector. -->
diff --git a/core/res/res/values-watch/config_material.xml b/core/res/res/values-watch/config_material.xml
index 03d3637..529f18b 100644
--- a/core/res/res/values-watch/config_material.xml
+++ b/core/res/res/values-watch/config_material.xml
@@ -30,9 +30,6 @@
     <!-- Always overscan by default to ensure onApplyWindowInsets will always be called. -->
     <bool name="config_windowOverscanByDefault">true</bool>
 
-    <!-- Enable windowSwipeToDismiss. -->
-    <bool name="config_windowSwipeToDismiss">true</bool>
-
     <!-- Style the scrollbars accoridngly. -->
     <drawable name="config_scrollbarThumbVertical">@drawable/scrollbar_vertical_thumb</drawable>
     <drawable name="config_scrollbarTrackVertical">@drawable/scrollbar_vertical_track</drawable>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index d828f33..a11eaa9 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -1786,6 +1786,10 @@
     <string name="biometric_dialog_default_title">Verify it\u2019s you</string>
     <!-- Subtitle shown on the system-provided biometric dialog, asking the user to authenticate with a biometric (e.g. fingerprint or face). [CHAR LIMIT=70] -->
     <string name="biometric_dialog_default_subtitle">Use your biometric to continue</string>
+    <!-- Subtitle shown on the system-provided biometric dialog, asking the user to authenticate with fingerprint. [CHAR LIMIT=70] -->
+    <string name="biometric_dialog_fingerprint_subtitle">Use your fingerprint to continue</string>
+    <!-- Subtitle shown on the system-provided biometric dialog, asking the user to authenticate with face. [CHAR LIMIT=70] -->
+    <string name="biometric_dialog_face_subtitle">Use your face to continue</string>
     <!-- Subtitle shown on the system-provided biometric dialog, asking the user to authenticate with a biometric (e.g. fingerprint or face) or their screen lock credential (i.e. PIN, pattern, or password). [CHAR LIMIT=90] -->
     <string name="biometric_or_screen_lock_dialog_default_subtitle">Use your biometric or screen lock to continue</string>
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 956c1f3..0951aec 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2581,6 +2581,8 @@
   <java-symbol type="string" name="biometric_or_screen_lock_app_setting_name" />
   <java-symbol type="string" name="biometric_dialog_default_title" />
   <java-symbol type="string" name="biometric_dialog_default_subtitle" />
+  <java-symbol type="string" name="biometric_dialog_face_subtitle" />
+  <java-symbol type="string" name="biometric_dialog_fingerprint_subtitle" />
   <java-symbol type="string" name="biometric_or_screen_lock_dialog_default_subtitle" />
   <java-symbol type="string" name="biometric_error_hw_unavailable" />
   <java-symbol type="string" name="biometric_error_user_canceled" />
diff --git a/core/tests/coretests/src/android/flags/FeatureFlagsTest.java b/core/tests/coretests/src/android/flags/FeatureFlagsTest.java
new file mode 100644
index 0000000..3fc9439
--- /dev/null
+++ b/core/tests/coretests/src/android/flags/FeatureFlagsTest.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.flags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+
+@SmallTest
+@Presubmit
+public class FeatureFlagsTest {
+
+    IFeatureFlagsFake mIFeatureFlagsFake = new IFeatureFlagsFake();
+    FeatureFlags mFeatureFlags = new FeatureFlags(mIFeatureFlagsFake);
+
+    @Before
+    public void setup() {
+        FeatureFlags.setInstance(mFeatureFlags);
+    }
+
+    @Test
+    public void testFusedOff_Disabled() {
+        FusedOffFlag flag = FeatureFlags.fusedOffFlag("test", "a");
+        assertThat(mFeatureFlags.isEnabled(flag)).isFalse();
+    }
+
+    @Test
+    public void testFusedOn_Enabled() {
+        FusedOnFlag flag = FeatureFlags.fusedOnFlag("test", "a");
+        assertThat(mFeatureFlags.isEnabled(flag)).isTrue();
+    }
+
+    @Test
+    public void testBooleanFlag_DefaultDisabled() {
+        BooleanFlag flag = FeatureFlags.booleanFlag("test", "a", false);
+        assertThat(mFeatureFlags.isEnabled(flag)).isFalse();
+    }
+
+    @Test
+    public void testBooleanFlag_DefaultEnabled() {
+        BooleanFlag flag = FeatureFlags.booleanFlag("test", "a", true);
+        assertThat(mFeatureFlags.isEnabled(flag)).isTrue();
+    }
+
+    @Test
+    public void testDynamicBooleanFlag_DefaultDisabled() {
+        DynamicBooleanFlag flag = FeatureFlags.dynamicBooleanFlag("test", "a", false);
+        assertThat(mFeatureFlags.isCurrentlyEnabled(flag)).isFalse();
+    }
+
+    @Test
+    public void testDynamicBooleanFlag_DefaultEnabled() {
+        DynamicBooleanFlag flag = FeatureFlags.dynamicBooleanFlag("test", "a", true);
+        assertThat(mFeatureFlags.isCurrentlyEnabled(flag)).isTrue();
+    }
+
+    @Test
+    public void testBooleanFlag_OverrideBeforeRead() {
+        BooleanFlag flag = FeatureFlags.booleanFlag("test", "a", false);
+        SyncableFlag syncableFlag = new SyncableFlag(
+                flag.getNamespace(), flag.getName(), "true", false);
+
+        mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag));
+
+        assertThat(mFeatureFlags.isEnabled(flag)).isTrue();
+    }
+
+    @Test
+    public void testFusedOffFlag_OverrideHasNoEffect() {
+        FusedOffFlag flag = FeatureFlags.fusedOffFlag("test", "a");
+        SyncableFlag syncableFlag = new SyncableFlag(
+                flag.getNamespace(), flag.getName(), "true", false);
+
+        mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag));
+
+        assertThat(mFeatureFlags.isEnabled(flag)).isFalse();
+    }
+
+    @Test
+    public void testFusedOnFlag_OverrideHasNoEffect() {
+        FusedOnFlag flag = FeatureFlags.fusedOnFlag("test", "a");
+        SyncableFlag syncableFlag = new SyncableFlag(
+                flag.getNamespace(), flag.getName(), "false", false);
+
+        mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag));
+
+        assertThat(mFeatureFlags.isEnabled(flag)).isTrue();
+    }
+
+    @Test
+    public void testDynamicFlag_OverrideBeforeRead() {
+        DynamicBooleanFlag flag = FeatureFlags.dynamicBooleanFlag("test", "a", false);
+        SyncableFlag syncableFlag = new SyncableFlag(
+                flag.getNamespace(), flag.getName(), "true", true);
+
+        mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag));
+
+        // Changes to true
+        assertThat(mFeatureFlags.isCurrentlyEnabled(flag)).isTrue();
+    }
+
+    @Test
+    public void testDynamicFlag_OverrideAfterRead() {
+        DynamicBooleanFlag flag = FeatureFlags.dynamicBooleanFlag("test", "a", false);
+        SyncableFlag syncableFlag = new SyncableFlag(
+                flag.getNamespace(), flag.getName(), "true", true);
+
+        // Starts false
+        assertThat(mFeatureFlags.isCurrentlyEnabled(flag)).isFalse();
+
+        mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag));
+
+        // Changes to true
+        assertThat(mFeatureFlags.isCurrentlyEnabled(flag)).isTrue();
+    }
+
+    @Test
+    public void testDynamicFlag_FiresListener() {
+        DynamicBooleanFlag flag = FeatureFlags.dynamicBooleanFlag("test", "a", false);
+        AtomicBoolean called = new AtomicBoolean(false);
+        FeatureFlags.ChangeListener listener = flag1 -> called.set(true);
+
+        mFeatureFlags.addChangeListener(listener);
+
+        SyncableFlag syncableFlag = new SyncableFlag(
+                flag.getNamespace(), flag.getName(), flag.getDefault().toString(), true);
+
+        mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag));
+
+        // Fires listener.
+        assertThat(called.get()).isTrue();
+    }
+}
diff --git a/core/tests/coretests/src/android/flags/IFeatureFlagsFake.java b/core/tests/coretests/src/android/flags/IFeatureFlagsFake.java
new file mode 100644
index 0000000..bc5d8aa
--- /dev/null
+++ b/core/tests/coretests/src/android/flags/IFeatureFlagsFake.java
@@ -0,0 +1,113 @@
+/*
+ * 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.flags;
+
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+class IFeatureFlagsFake implements IFeatureFlags {
+
+    private final Set<IFeatureFlagsCallback> mCallbacks = new HashSet<>();
+
+    List<SyncableFlag> mOverrides;
+
+    @Override
+    public IBinder asBinder() {
+        return null;
+    }
+
+    @Override
+    public List<SyncableFlag> syncFlags(List<SyncableFlag> flagList) {
+        return mOverrides == null ? flagList : mOverrides;
+    }
+
+    @Override
+    public List<SyncableFlag> queryFlags(List<SyncableFlag> flagList) {
+        return mOverrides == null ? flagList : mOverrides;    }
+
+    @Override
+    public void overrideFlag(SyncableFlag syncableFlag) {
+        SyncableFlag match = findFlag(syncableFlag);
+        if (match != null) {
+            mOverrides.remove(match);
+        }
+
+        mOverrides.add(syncableFlag);
+
+        for (IFeatureFlagsCallback cb : mCallbacks) {
+            try {
+                cb.onFlagChange(syncableFlag);
+            } catch (RemoteException e) {
+                // does not happen in fakes.
+            }
+        }
+    }
+
+    @Override
+    public void resetFlag(SyncableFlag syncableFlag) {
+        SyncableFlag match = findFlag(syncableFlag);
+        if (match != null) {
+            mOverrides.remove(match);
+        }
+
+        for (IFeatureFlagsCallback cb : mCallbacks) {
+            try {
+                cb.onFlagChange(syncableFlag);
+            } catch (RemoteException e) {
+                // does not happen in fakes.
+            }
+        }
+    }
+
+    private SyncableFlag findFlag(SyncableFlag syncableFlag) {
+        SyncableFlag match = null;
+        for (SyncableFlag sf : mOverrides) {
+            if (sf.getName().equals(syncableFlag.getName())
+                    && sf.getNamespace().equals(syncableFlag.getNamespace())) {
+                match = sf;
+                break;
+            }
+        }
+
+        return match;
+    }
+    @Override
+    public void registerCallback(IFeatureFlagsCallback callback) {
+        mCallbacks.add(callback);
+    }
+
+    @Override
+    public void unregisterCallback(IFeatureFlagsCallback callback) {
+        mCallbacks.remove(callback);
+    }
+
+    public void setFlagOverrides(List<SyncableFlag> flagList) {
+        mOverrides = flagList;
+        for (SyncableFlag sf : flagList) {
+            for (IFeatureFlagsCallback cb : mCallbacks) {
+                try {
+                    cb.onFlagChange(sf);
+                } catch (RemoteException e) {
+                    // does not happen in fakes.
+                }
+            }
+        }
+    }
+}
diff --git a/core/tests/coretests/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java b/core/tests/coretests/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
index c0fdf9f..b31af89 100644
--- a/core/tests/coretests/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
+++ b/core/tests/coretests/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
@@ -29,10 +29,14 @@
 import android.os.VibrationEffect;
 import android.os.vibrator.PrebakedSegment;
 import android.platform.test.annotations.Presubmit;
+import android.util.Xml;
+
+import com.android.modules.utils.TypedXmlPullParser;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
+import org.xmlpull.v1.XmlPullParser;
 
 import java.io.IOException;
 import java.io.StringReader;
@@ -65,6 +69,61 @@
     }
 
     @Test
+    public void testParseTag_succeedAndParserPointsToEndVibrationTag() throws Exception {
+        VibrationEffect effect = VibrationEffect.startComposition()
+                .addPrimitive(PRIMITIVE_CLICK)
+                .addPrimitive(PRIMITIVE_TICK, 0.2497f)
+                .compose();
+        String xml = "<vibration>"
+                + "<primitive-effect name=\"click\"/>"
+                + "<primitive-effect name=\"tick\" scale=\"0.2497\"/>"
+                + "</vibration>";
+        VibrationEffect effect2 = VibrationEffect.startComposition()
+                .addPrimitive(PRIMITIVE_LOW_TICK, 1f, 356)
+                .addPrimitive(PRIMITIVE_SPIN, 0.6364f, 7)
+                .compose();
+        String xml2 = "<vibration>"
+                + "<primitive-effect name=\"low_tick\" delayMs=\"356\"/>"
+                + "<primitive-effect name=\"spin\" scale=\"0.6364\" delayMs=\"7\"/>"
+                + "</vibration>";
+
+        TypedXmlPullParser parser = createXmlPullParser(xml);
+        assertParseTagSucceeds(parser, effect);
+        parser.next();
+        assertEndOfDocument(parser);
+
+        // Test no-issues when an end-tag follows the vibration XML.
+        // To test this, starting with the corresponding "start-tag" is necessary.
+        parser = createXmlPullParser("<next-tag>" + xml + "</next-tag>");
+        // Move the parser once to point to the "<vibration> tag.
+        parser.next();
+        assertParseTagSucceeds(parser, effect);
+        parser.next();
+        assertEndTag(parser, "next-tag");
+
+        parser = createXmlPullParser(xml + "<next-tag>");
+        assertParseTagSucceeds(parser, effect);
+        parser.next();
+        assertStartTag(parser, "next-tag");
+
+        parser = createXmlPullParser(xml + xml2);
+        assertParseTagSucceeds(parser, effect);
+        parser.next();
+        assertParseTagSucceeds(parser, effect2);
+        parser.next();
+        assertEndOfDocument(parser);
+    }
+
+    @Test
+    public void testParseTag_badXml_throwsException() throws Exception {
+        assertParseTagFails(
+                "<vibration>random text<primitive-effect name=\"click\"/></vibration>");
+        assertParseTagFails("<bad-tag><primitive-effect name=\"click\"/></vibration>");
+        assertParseTagFails("<primitive-effect name=\"click\"/></vibration>");
+        assertParseTagFails("<vibration><primitive-effect name=\"click\"/>");
+    }
+
+    @Test
     public void testPrimitives_allSucceed() throws IOException {
         VibrationEffect effect = VibrationEffect.startComposition()
                 .addPrimitive(PRIMITIVE_CLICK)
@@ -172,6 +231,41 @@
         assertThat(parse(xml, /* flags= */ 0)).isEqualTo(effect);
     }
 
+    private TypedXmlPullParser createXmlPullParser(String xml) throws Exception {
+        TypedXmlPullParser parser = Xml.newFastPullParser();
+        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+        parser.setInput(new StringReader(xml));
+        parser.next(); // read START_DOCUMENT
+        return parser;
+    }
+
+    /**
+     * Asserts parsing vibration from an open TypedXmlPullParser succeeds, and that the parser
+     * points to the end "vibration" tag.
+     */
+    private void assertParseTagSucceeds(
+            TypedXmlPullParser parser, VibrationEffect effect) throws Exception {
+        assertThat(parseTag(parser)).isEqualTo(effect);
+
+        assertThat(parser.getEventType()).isEqualTo(XmlPullParser.END_TAG);
+        assertThat(parser.getName()).isEqualTo("vibration");
+    }
+
+    private void assertEndTag(TypedXmlPullParser parser, String expectedTagName) throws Exception {
+        assertThat(parser.getName()).isEqualTo(expectedTagName);
+        assertThat(parser.getEventType()).isEqualTo(parser.END_TAG);
+    }
+
+    private void assertStartTag(TypedXmlPullParser parser, String expectedTagName)
+            throws Exception {
+        assertThat(parser.getName()).isEqualTo(expectedTagName);
+        assertThat(parser.getEventType()).isEqualTo(parser.START_TAG);
+    }
+
+    private void assertEndOfDocument(TypedXmlPullParser parser) throws Exception {
+        assertThat(parser.getEventType()).isEqualTo(parser.END_DOCUMENT);
+    }
+
     private void assertHiddenApisParserSucceeds(String xml, VibrationEffect effect)
             throws IOException {
         assertThat(parse(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS)).isEqualTo(effect);
@@ -183,6 +277,12 @@
                 () -> serialize(effect, /* flags= */ 0));
     }
 
+    private void assertParseTagFails(String xml) {
+        assertThrows("Expected parsing to fail for " + xml,
+                VibrationXmlParser.VibrationXmlParserException.class,
+                () -> parseTag(createXmlPullParser(xml)));
+    }
+
     private void assertPublicApisSerializerSucceeds(VibrationEffect effect,
             String... expectedSegments) throws IOException {
         assertSerializationContainsSegments(serialize(effect, /* flags= */ 0), expectedSegments);
@@ -214,6 +314,11 @@
         return VibrationXmlParser.parse(new StringReader(xml), flags);
     }
 
+    private static VibrationEffect parseTag(TypedXmlPullParser parser)
+            throws IOException, VibrationXmlParser.VibrationXmlParserException {
+        return VibrationXmlParser.parseTag(parser, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS);
+    }
+
     private static String serialize(VibrationEffect effect, @VibrationXmlSerializer.Flags int flags)
             throws IOException {
         StringWriter writer = new StringWriter();
diff --git a/core/tests/coretests/src/android/window/WindowContextControllerTest.java b/core/tests/coretests/src/android/window/WindowContextControllerTest.java
index 467d555..a52d2e8 100644
--- a/core/tests/coretests/src/android/window/WindowContextControllerTest.java
+++ b/core/tests/coretests/src/android/window/WindowContextControllerTest.java
@@ -24,13 +24,11 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 
-import android.app.servertransaction.WindowTokenClientController;
 import android.os.Binder;
 import android.platform.test.annotations.Presubmit;
 
@@ -58,8 +56,6 @@
 public class WindowContextControllerTest {
     private WindowContextController mController;
     @Mock
-    private WindowTokenClientController mWindowTokenClientController;
-    @Mock
     private WindowTokenClient mMockToken;
 
     @Before
@@ -67,9 +63,7 @@
         MockitoAnnotations.initMocks(this);
         mController = new WindowContextController(mMockToken);
         doNothing().when(mMockToken).onConfigurationChanged(any(), anyInt(), anyBoolean());
-        WindowTokenClientController.overrideInstance(mWindowTokenClientController);
-        doReturn(true).when(mWindowTokenClientController).attachToDisplayArea(
-                eq(mMockToken), anyInt(), anyInt(), any());
+        doReturn(true).when(mMockToken).attachToDisplayArea(anyInt(), anyInt(), any());
     }
 
     @Test(expected = IllegalStateException.class)
@@ -84,7 +78,7 @@
     public void testDetachIfNeeded_NotAttachedYet_DoNothing() {
         mController.detachIfNeeded();
 
-        verify(mWindowTokenClientController, never()).detachIfNeeded(any());
+        verify(mMockToken, never()).detachFromWindowContainerIfNeeded();
     }
 
     @Test
diff --git a/data/etc/platform.xml b/data/etc/platform.xml
index 2c85fe4..c4530f6 100644
--- a/data/etc/platform.xml
+++ b/data/etc/platform.xml
@@ -346,4 +346,8 @@
 
     <!-- Allow IMS service entitlement app to schedule jobs to run when app in background. -->
     <allow-in-power-save-except-idle package="com.android.imsserviceentitlement" />
+
+    <!-- Allow device lock controller app to schedule jobs and alarms when app in background,
+        otherwise, it may not be able to enforce provision for managed devices. -->
+    <allow-in-power-save package="com.android.devicelockcontroller" />
 </permissions>
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index aa0a8d9..3206dd2 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -257,6 +257,7 @@
         <permission name="android.permission.CLEAR_APP_CACHE"/>
         <permission name="android.permission.ACCESS_INSTANT_APPS" />
         <permission name="android.permission.CONNECTIVITY_INTERNAL"/>
+        <permission name="android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS" />
         <permission name="android.permission.DELETE_CACHE_FILES"/>
         <permission name="android.permission.DELETE_PACKAGES"/>
         <permission name="android.permission.DUMP"/>
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreRSACipherSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreRSACipherSpi.java
index 2b1515a..3bb2564 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreRSACipherSpi.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreRSACipherSpi.java
@@ -18,7 +18,6 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.content.pm.PackageManager;
 import android.hardware.security.keymint.KeyParameter;
 import android.security.keymaster.KeymasterDefs;
 import android.security.keystore.KeyProperties;
@@ -300,12 +299,6 @@
             return false;
         }
 
-        private static boolean hasKeyMintV2() {
-            PackageManager pm = android.app.AppGlobals.getInitialApplication().getPackageManager();
-            return pm.hasSystemFeature(PackageManager.FEATURE_HARDWARE_KEYSTORE, 200)
-                    && !pm.hasSystemFeature(PackageManager.FEATURE_HARDWARE_KEYSTORE, 300);
-        }
-
         @Override
         protected final void addAlgorithmSpecificParametersToBegin(
                 @NonNull List<KeyParameter> parameters, Authorization[] keyCharacteristics) {
@@ -314,12 +307,11 @@
                     KeymasterDefs.KM_TAG_DIGEST, mKeymasterDigest
             ));
             // Only add the KM_TAG_RSA_OAEP_MGF_DIGEST tag to begin() if the MGF Digest is
-            // present in the key properties or KeyMint version is 200. Keys generated prior to
-            // Android 14 did not have this tag (Keystore didn't add it) and hence not present in
-            // imported key as well, so specifying any MGF digest tag would cause a begin()
-            // operation (on an Android 14 device) to fail (with a key that was generated on
-            // Android 13 or below).
-            if (isMgfDigestTagPresentInKeyProperties(keyCharacteristics) || hasKeyMintV2()) {
+            // present in the key properties. Keys generated prior to Android 14 did not have
+            // this tag (Keystore didn't add it) so specifying any MGF digest tag would cause
+            // a begin() operation (on an Android 14 device) to fail (with a key that was generated
+            // on Android 13 or below).
+            if (isMgfDigestTagPresentInKeyProperties(keyCharacteristics)) {
                 parameters.add(KeyStore2ParameterUtils.makeEnum(
                         KeymasterDefs.KM_TAG_RSA_OAEP_MGF_DIGEST, mKeymasterMgf1Digest
                 ));
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
index 0059577..ccf9552 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
@@ -296,7 +296,7 @@
      * {@link IllegalArgumentException} since this can cause negative UI effects down stream.
      *
      * @param context a proxy for the {@link android.view.Window} that contains the
-     * {@link DisplayFeature}.
+     *                {@link DisplayFeature}.
      * @return a {@link List}  of {@link DisplayFeature}s that are within the
      * {@link android.view.Window} of the {@link Activity}
      */
@@ -328,10 +328,32 @@
             rotateRectToDisplayRotation(displayId, featureRect);
             transformToWindowSpaceRect(windowConfiguration, featureRect);
 
-            if (!isZero(featureRect)) {
+            if (isZero(featureRect)) {
                 // TODO(b/228641877): Remove guarding when fixed.
-                features.add(new FoldingFeature(featureRect, baseFeature.getType(), state));
+                continue;
             }
+            if (featureRect.left != 0 && featureRect.top != 0) {
+                throw new IllegalArgumentException("Bounding rectangle must start at the top or "
+                        + "left of the window. BaseFeatureRect: " + baseFeature.getRect()
+                        + ", FeatureRect: " + featureRect
+                        + ", WindowConfiguration: " + windowConfiguration);
+
+            }
+            if (featureRect.left == 0
+                    && featureRect.width() != windowConfiguration.getBounds().width()) {
+                throw new IllegalArgumentException("Horizontal FoldingFeature must have full width."
+                        + " BaseFeatureRect: " + baseFeature.getRect()
+                        + ", FeatureRect: " + featureRect
+                        + ", WindowConfiguration: " + windowConfiguration);
+            }
+            if (featureRect.top == 0
+                    && featureRect.height() != windowConfiguration.getBounds().height()) {
+                throw new IllegalArgumentException("Vertical FoldingFeature must have full height."
+                        + " BaseFeatureRect: " + baseFeature.getRect()
+                        + ", FeatureRect: " + featureRect
+                        + ", WindowConfiguration: " + windowConfiguration);
+            }
+            features.add(new FoldingFeature(featureRect, baseFeature.getType(), state));
         }
         return features;
     }
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 6d14440..f8d7b6b 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
@@ -202,17 +202,18 @@
     }
 
     /**
-     * Moves a single task to freeform and sets the taskBounds to the passed in bounds,
-     * startBounds
+     * The first part of the animated move to desktop transition. Applies the changes to move task
+     * to desktop mode and sets the taskBounds to the passed in bounds, startBounds. This is
+     * followed with a call to {@link finishMoveToDesktop} or {@link cancelMoveToDesktop}.
      */
-    fun moveToFreeform(
+    fun startMoveToDesktop(
             taskInfo: RunningTaskInfo,
             startBounds: Rect,
             dragToDesktopValueAnimator: MoveToDesktopAnimator
     ) {
         KtProtoLog.v(
             WM_SHELL_DESKTOP_MODE,
-            "DesktopTasksController: moveToFreeform with bounds taskId=%d",
+            "DesktopTasksController: startMoveToDesktop taskId=%d",
             taskInfo.taskId
         )
         val wct = WindowContainerTransaction()
@@ -221,18 +222,21 @@
         wct.setBounds(taskInfo.token, startBounds)
 
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
-            enterDesktopTaskTransitionHandler.startMoveToFreeformAnimation(wct,
-                    dragToDesktopValueAnimator, mOnAnimationFinishedCallback)
+            enterDesktopTaskTransitionHandler.startMoveToDesktop(wct, dragToDesktopValueAnimator,
+                    mOnAnimationFinishedCallback)
         } else {
             shellTaskOrganizer.applyTransaction(wct)
         }
     }
 
-    /** Brings apps to front and sets freeform task bounds */
-    private fun moveToDesktopWithAnimation(taskInfo: RunningTaskInfo, freeformBounds: Rect) {
+    /**
+     * The second part of the animated move to desktop transition, called after
+     * {@link startMoveToDesktop}. Brings apps to front and sets freeform task bounds.
+     */
+    private fun finalizeMoveToDesktop(taskInfo: RunningTaskInfo, freeformBounds: Rect) {
         KtProtoLog.v(
             WM_SHELL_DESKTOP_MODE,
-            "DesktopTasksController: moveToDesktop with animation taskId=%d",
+            "DesktopTasksController: finalizeMoveToDesktop taskId=%d",
             taskInfo.taskId
         )
         val wct = WindowContainerTransaction()
@@ -241,8 +245,8 @@
         wct.setBounds(taskInfo.token, freeformBounds)
 
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
-            enterDesktopTaskTransitionHandler.startTransition(
-                Transitions.TRANSIT_ENTER_DESKTOP_MODE, wct, mOnAnimationFinishedCallback)
+            enterDesktopTaskTransitionHandler.finalizeMoveToDesktop(wct,
+                    mOnAnimationFinishedCallback)
         } else {
             shellTaskOrganizer.applyTransaction(wct)
             releaseVisualIndicator()
@@ -272,13 +276,14 @@
     }
 
     /**
-     * Move a task to fullscreen after being dragged from fullscreen and released back into
-     * status bar area
+     * The second part of the animated move to desktop transition, called after
+     * {@link startMoveToDesktop}. Move a task to fullscreen after being dragged from fullscreen
+     * and released back into status bar area.
      */
-    fun cancelMoveToFreeform(task: RunningTaskInfo, moveToDesktopAnimator: MoveToDesktopAnimator) {
+    fun cancelMoveToDesktop(task: RunningTaskInfo, moveToDesktopAnimator: MoveToDesktopAnimator) {
         KtProtoLog.v(
             WM_SHELL_DESKTOP_MODE,
-            "DesktopTasksController: cancelMoveToFreeform taskId=%d",
+            "DesktopTasksController: cancelMoveToDesktop taskId=%d",
             task.taskId
         )
         val wct = WindowContainerTransaction()
@@ -784,7 +789,7 @@
             taskInfo: RunningTaskInfo,
             freeformBounds: Rect
     ) {
-        moveToDesktopWithAnimation(taskInfo, freeformBounds)
+        finalizeMoveToDesktop(taskInfo, freeformBounds)
     }
 
     private fun getStatusBarHeight(taskInfo: RunningTaskInfo): Int {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java
index 650cac5..1acf783 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java
@@ -79,7 +79,7 @@
      * @param wct WindowContainerTransaction for transition
      * @param onAnimationEndCallback to be called after animation
      */
-    public void startTransition(@WindowManager.TransitionType int type,
+    private void startTransition(@WindowManager.TransitionType int type,
             @NonNull WindowContainerTransaction wct,
             Consumer<SurfaceControl.Transaction> onAnimationEndCallback) {
         mOnAnimationFinishedCallback = onAnimationEndCallback;
@@ -88,17 +88,29 @@
     }
 
     /**
-     * Starts Transition of type TRANSIT_ENTER_FREEFORM
+     * Starts Transition of type TRANSIT_START_MOVE_TO_DESKTOP_MODE
      * @param wct WindowContainerTransaction for transition
      * @param moveToDesktopAnimator Animator that shrinks and positions task during two part move
      *                              to desktop animation
      * @param onAnimationEndCallback to be called after animation
      */
-    public void startMoveToFreeformAnimation(@NonNull WindowContainerTransaction wct,
+    public void startMoveToDesktop(@NonNull WindowContainerTransaction wct,
             @NonNull MoveToDesktopAnimator moveToDesktopAnimator,
             Consumer<SurfaceControl.Transaction> onAnimationEndCallback) {
         mMoveToDesktopAnimator = moveToDesktopAnimator;
-        startTransition(Transitions.TRANSIT_ENTER_FREEFORM, wct, onAnimationEndCallback);
+        startTransition(Transitions.TRANSIT_START_MOVE_TO_DESKTOP_MODE, wct,
+                onAnimationEndCallback);
+    }
+
+    /**
+     * Starts Transition of type TRANSIT_FINALIZE_MOVE_TO_DESKTOP_MODE
+     * @param wct WindowContainerTransaction for transition
+     * @param onAnimationEndCallback to be called after animation
+     */
+    public void finalizeMoveToDesktop(@NonNull WindowContainerTransaction wct,
+            Consumer<SurfaceControl.Transaction> onAnimationEndCallback) {
+        startTransition(Transitions.TRANSIT_FINALIZE_MOVE_TO_DESKTOP_MODE, wct,
+                onAnimationEndCallback);
     }
 
     /**
@@ -155,7 +167,7 @@
         }
 
         final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
-        if (type == Transitions.TRANSIT_ENTER_FREEFORM
+        if (type == Transitions.TRANSIT_START_MOVE_TO_DESKTOP_MODE
                 && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
             // Transitioning to freeform but keeping fullscreen bounds, so the crop is set
             // to null and we don't require an animation
@@ -182,7 +194,7 @@
         }
 
         Rect endBounds = change.getEndAbsBounds();
-        if (type == Transitions.TRANSIT_ENTER_DESKTOP_MODE
+        if (type == Transitions.TRANSIT_FINALIZE_MOVE_TO_DESKTOP_MODE
                 && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM
                 && !endBounds.isEmpty()) {
             // This Transition animates a task to freeform bounds after being dragged into freeform
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index af8ef17..7699b4b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -737,12 +737,23 @@
         Intent fillInIntent2 = null;
         final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent1);
         final String packageName2 = SplitScreenUtils.getPackageName(pendingIntent2);
+        final ActivityOptions activityOptions1 = options1 != null
+                ? ActivityOptions.fromBundle(options1) : ActivityOptions.makeBasic();
+        final ActivityOptions activityOptions2 = options2 != null
+                ? ActivityOptions.fromBundle(options2) : ActivityOptions.makeBasic();
         if (samePackage(packageName1, packageName2, userId1, userId2)) {
             if (supportMultiInstancesSplit(packageName1)) {
                 fillInIntent1 = new Intent();
                 fillInIntent1.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
                 fillInIntent2 = new Intent();
                 fillInIntent2.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+
+                if (shortcutInfo1 != null) {
+                    activityOptions1.setApplyMultipleTaskFlagForShortcut(true);
+                }
+                if (shortcutInfo2 != null) {
+                    activityOptions2.setApplyMultipleTaskFlagForShortcut(true);
+                }
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
             } else {
                 pendingIntent2 = null;
@@ -754,9 +765,10 @@
                         Toast.LENGTH_SHORT).show();
             }
         }
-        mStageCoordinator.startIntents(pendingIntent1, fillInIntent1, shortcutInfo1, options1,
-                pendingIntent2, fillInIntent2, shortcutInfo2, options2, splitPosition, splitRatio,
-                remoteTransition, instanceId);
+        mStageCoordinator.startIntents(pendingIntent1, fillInIntent1, shortcutInfo1,
+                activityOptions1.toBundle(), pendingIntent2, fillInIntent2, shortcutInfo2,
+                activityOptions2.toBundle(), splitPosition, splitRatio, remoteTransition,
+                instanceId);
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index e52fd00..dc78c9b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -407,7 +407,7 @@
                             change.getEndAbsBounds().width(), change.getEndAbsBounds().height());
                 }
                 // Rotation change of independent non display window container.
-                if (change.getParent() == null
+                if (change.getParent() == null && !change.hasFlags(FLAG_IS_DISPLAY)
                         && change.getStartRotation() != change.getEndRotation()) {
                     startRotationAnimation(startTransaction, change, info,
                             ROTATION_ANIMATION_ROTATE, animations, onAnimFinish);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 7565996..4ca383f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -149,11 +149,13 @@
     /** Transition type for maximize to freeform transition. */
     public static final int TRANSIT_RESTORE_FROM_MAXIMIZE = WindowManager.TRANSIT_FIRST_CUSTOM + 9;
 
-    /** Transition type to freeform in desktop mode. */
-    public static final int TRANSIT_ENTER_FREEFORM = WindowManager.TRANSIT_FIRST_CUSTOM + 10;
+    /** Transition type for starting the move to desktop mode. */
+    public static final int TRANSIT_START_MOVE_TO_DESKTOP_MODE =
+            WindowManager.TRANSIT_FIRST_CUSTOM + 10;
 
-    /** Transition type to freeform in desktop mode. */
-    public static final int TRANSIT_ENTER_DESKTOP_MODE = WindowManager.TRANSIT_FIRST_CUSTOM + 11;
+    /** Transition type for finalizing the move to desktop mode. */
+    public static final int TRANSIT_FINALIZE_MOVE_TO_DESKTOP_MODE =
+            WindowManager.TRANSIT_FIRST_CUSTOM + 11;
 
     /** Transition type to fullscreen from desktop mode. */
     public static final int TRANSIT_EXIT_DESKTOP_MODE = WindowManager.TRANSIT_FIRST_CUSTOM + 12;
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 1a18fc2..80cf96a 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
@@ -197,7 +197,7 @@
             @NonNull TransitionInfo info,
             @NonNull TransitionInfo.Change change) {
         if (change.getMode() == WindowManager.TRANSIT_CHANGE
-                && (info.getType() == Transitions.TRANSIT_ENTER_DESKTOP_MODE
+                && (info.getType() == Transitions.TRANSIT_FINALIZE_MOVE_TO_DESKTOP_MODE
                 || info.getType() == Transitions.TRANSIT_CANCEL_ENTERING_DESKTOP_MODE
                 || info.getType() == Transitions.TRANSIT_EXIT_DESKTOP_MODE
                 || info.getType() == Transitions.TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE)) {
@@ -616,7 +616,7 @@
                     } else if (mMoveToDesktopAnimator != null) {
                         relevantDecor.incrementRelayoutBlock();
                         mDesktopTasksController.ifPresent(
-                                c -> c.cancelMoveToFreeform(relevantDecor.mTaskInfo,
+                                c -> c.cancelMoveToDesktop(relevantDecor.mTaskInfo,
                                         mMoveToDesktopAnimator));
                         mMoveToDesktopAnimator = null;
                         return;
@@ -643,7 +643,7 @@
                                     mDragToDesktopAnimationStartBounds, relevantDecor.mTaskInfo,
                                     relevantDecor.mTaskSurface);
                             mDesktopTasksController.ifPresent(
-                                    c -> c.moveToFreeform(relevantDecor.mTaskInfo,
+                                    c -> c.startMoveToDesktop(relevantDecor.mTaskInfo,
                                             mDragToDesktopAnimationStartBounds,
                                             mMoveToDesktopAnimator));
                             mMoveToDesktopAnimator.startAnimation();
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/SplitScreenUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/SplitScreenUtils.kt
index fd56a6e..8a3c2c9 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/SplitScreenUtils.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/SplitScreenUtils.kt
@@ -42,7 +42,7 @@
 import com.android.server.wm.flicker.testapp.ActivityOptions.SplitScreen.Primary
 import org.junit.Assert.assertNotNull
 
-internal object SplitScreenUtils {
+object SplitScreenUtils {
     private const val TIMEOUT_MS = 3_000L
     private const val DRAG_DURATION_MS = 1_000L
     private const val NOTIFICATION_SCROLLER = "notification_stack_scroller"
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt
index 0f9579d..69c8ecd 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt
@@ -17,7 +17,6 @@
 package com.android.wm.shell.flicker.appcompat
 
 import android.content.Context
-import android.system.helpers.CommandsHelper
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.legacy.FlickerBuilder
 import android.tools.device.flicker.legacy.FlickerTestData
@@ -29,15 +28,18 @@
 import com.android.wm.shell.flicker.appWindowIsVisibleAtStart
 import com.android.wm.shell.flicker.appWindowKeepVisible
 import com.android.wm.shell.flicker.layerKeepVisible
-import org.junit.After
+
 import org.junit.Assume
 import org.junit.Before
+import org.junit.Rule
 
 abstract class BaseAppCompat(flicker: LegacyFlickerTest) : BaseTest(flicker) {
     protected val context: Context = instrumentation.context
     protected val letterboxApp = LetterboxAppHelper(instrumentation)
-    lateinit var cmdHelper: CommandsHelper
-    private lateinit var letterboxStyle: HashMap<String, String>
+
+    @JvmField
+    @Rule
+    val letterboxRule: LetterboxRule = LetterboxRule()
 
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit
@@ -52,50 +54,7 @@
 
     @Before
     fun before() {
-        cmdHelper = CommandsHelper.getInstance(instrumentation)
-        Assume.assumeTrue(tapl.isTablet && isIgnoreOrientationRequest())
-        letterboxStyle = mapLetterboxStyle()
-        resetLetterboxStyle()
-        setLetterboxEducationEnabled(false)
-    }
-
-    @After
-    fun after() {
-        resetLetterboxStyle()
-    }
-
-    private fun mapLetterboxStyle(): HashMap<String, String> {
-        val res = cmdHelper.executeShellCommand("wm get-letterbox-style")
-        val lines = res.lines()
-        val map = HashMap<String, String>()
-        for (line in lines) {
-            val keyValuePair = line.split(":")
-            if (keyValuePair.size == 2) {
-                val key = keyValuePair[0].trim()
-                map[key] = keyValuePair[1].trim()
-            }
-        }
-        return map
-    }
-
-    private fun getLetterboxStyle(): HashMap<String, String> {
-        if (!::letterboxStyle.isInitialized) {
-            letterboxStyle = mapLetterboxStyle()
-        }
-        return letterboxStyle
-    }
-
-    private fun resetLetterboxStyle() {
-        cmdHelper.executeShellCommand("wm reset-letterbox-style")
-    }
-
-    private fun setLetterboxEducationEnabled(enabled: Boolean) {
-        cmdHelper.executeShellCommand("wm set-letterbox-style --isEducationEnabled $enabled")
-    }
-
-    private fun isIgnoreOrientationRequest(): Boolean {
-        val res = cmdHelper.executeShellCommand("wm get-ignore-orientation-request")
-        return res != null && res.contains("true")
+        Assume.assumeTrue(tapl.isTablet && letterboxRule.isIgnoreOrientationRequest)
     }
 
     fun FlickerTestData.setStartRotation() = setRotation(flicker.scenario.startRotation)
@@ -115,7 +74,7 @@
 
     /** Only run on tests with config_letterboxActivityCornersRadius != 0 in devices */
     private fun assumeLetterboxRoundedCornersEnabled() {
-        Assume.assumeTrue(getLetterboxStyle().getValue("Corner radius") != "0")
+        Assume.assumeTrue(letterboxRule.hasCornerRadius)
     }
 
     fun assertLetterboxAppVisibleAtStartAndEnd() {
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/LetterboxRule.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/LetterboxRule.kt
new file mode 100644
index 0000000..5a1136f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/LetterboxRule.kt
@@ -0,0 +1,108 @@
+/*
+ * 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.wm.shell.flicker.appcompat
+
+import android.app.Instrumentation
+import android.system.helpers.CommandsHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * JUnit Rule to handle letterboxStyles and states
+ */
+class LetterboxRule(
+        private val withLetterboxEducationEnabled: Boolean = false,
+        private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation(),
+        private val cmdHelper: CommandsHelper = CommandsHelper.getInstance(instrumentation)
+) : TestRule {
+
+    private val execAdb: (String) -> String = {cmd -> cmdHelper.executeShellCommand(cmd)}
+    private lateinit var _letterboxStyle: MutableMap<String, String>
+
+    val letterboxStyle: Map<String, String>
+        get() {
+            if (!::_letterboxStyle.isInitialized) {
+                _letterboxStyle = mapLetterboxStyle()
+            }
+            return _letterboxStyle
+        }
+
+    val cornerRadius: Int?
+        get() = asInt(letterboxStyle["Corner radius"])
+
+    val hasCornerRadius: Boolean
+        get() {
+            val radius = cornerRadius
+            return radius != null && radius > 0
+        }
+
+    val isIgnoreOrientationRequest: Boolean
+        get() = execAdb("wm get-ignore-orientation-request")?.contains("true") ?: false
+
+    override fun apply(base: Statement?, description: Description?): Statement {
+        resetLetterboxStyle()
+        _letterboxStyle = mapLetterboxStyle()
+        val isLetterboxEducationEnabled = _letterboxStyle.getValue("Is education enabled")
+        var hasLetterboxEducationStateChanged = false
+        if ("$withLetterboxEducationEnabled" != isLetterboxEducationEnabled) {
+            hasLetterboxEducationStateChanged = true
+            execAdb("wm set-letterbox-style --isEducationEnabled " +
+                    withLetterboxEducationEnabled)
+        }
+        return try {
+            object : Statement() {
+                @Throws(Throwable::class)
+                override fun evaluate() {
+                    base!!.evaluate()
+                }
+            }
+        } finally {
+            if (hasLetterboxEducationStateChanged) {
+                execAdb("wm set-letterbox-style --isEducationEnabled " +
+                        isLetterboxEducationEnabled
+                )
+            }
+            resetLetterboxStyle()
+        }
+    }
+
+    private fun mapLetterboxStyle(): HashMap<String, String> {
+        val res = execAdb("wm get-letterbox-style")
+        val lines = res.lines()
+        val map = HashMap<String, String>()
+        for (line in lines) {
+            val keyValuePair = line.split(":")
+            if (keyValuePair.size == 2) {
+                val key = keyValuePair[0].trim()
+                map[key] = keyValuePair[1].trim()
+            }
+        }
+        return map
+    }
+
+    private fun resetLetterboxStyle() {
+        execAdb("wm reset-letterbox-style")
+    }
+
+    private fun asInt(str: String?): Int? = try {
+        str?.toInt()
+    } catch (e: NumberFormatException) {
+        null
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt
index a7bd258..67d5718 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt
@@ -31,7 +31,7 @@
 /**
  * Test launching app in size compat mode.
  *
- * To run this test: `atest WMShellFlickerTests:OpenAppInSizeCompatModeTest`
+ * To run this test: `atest WMShellFlickerTestsOther:OpenAppInSizeCompatModeTest`
  *
  * Actions:
  * ```
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt
new file mode 100644
index 0000000..e6ca261
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt
@@ -0,0 +1,128 @@
+/*
+ * 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.wm.shell.flicker.appcompat
+
+import android.platform.test.annotations.Postsubmit
+import android.tools.common.Rotation
+import android.tools.common.flicker.assertions.FlickerTest
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * Test launching app in size compat mode.
+ *
+ * To run this test: `atest WMShellFlickerTestsOther:OpenTransparentActivityTest`
+ *
+ * Actions:
+ * ```
+ *     Launch a letteboxed app and then a transparent activity from it. We test the bounds
+ *     are the same.
+ * ```
+ *
+ * Notes:
+ * ```
+ *     Some default assertions (e.g., nav bar, status bar and screen covered)
+ *     are inherited [BaseTest]
+ * ```
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+class OpenTransparentActivityTest(flicker: LegacyFlickerTest) : TransparentBaseAppCompat(flicker) {
+
+    /** {@inheritDoc} */
+    override val transition: FlickerBuilder.() -> Unit
+        get() = {
+            setup {
+                letterboxTranslucentLauncherApp.launchViaIntent(wmHelper)
+            }
+            transitions {
+                waitAndGetLaunchTransparent()?.click() ?: error("Launch Transparent not found")
+            }
+            teardown {
+                letterboxTranslucentApp.exit(wmHelper)
+                letterboxTranslucentLauncherApp.exit(wmHelper)
+            }
+        }
+
+    /**
+     * Checks the transparent activity is launched on top of the opaque one
+     */
+    @Postsubmit
+    @Test
+    fun translucentActivityIsLaunchedOnTopOfOpaqueActivity() {
+        flicker.assertWm {
+            this.isAppWindowOnTop(letterboxTranslucentLauncherApp)
+                .then()
+                .isAppWindowOnTop(letterboxTranslucentApp)
+        }
+    }
+
+    /**
+     * Checks that the activity is letterboxed
+     */
+    @Postsubmit
+    @Test
+    fun translucentActivityIsLetterboxed() {
+        flicker.assertLayers { isVisible(ComponentNameMatcher.LETTERBOX) }
+    }
+
+    /**
+     * Checks that the translucent activity inherits bounds from the opaque one.
+     */
+    @Postsubmit
+    @Test
+    fun translucentActivityInheritsBoundsFromOpaqueActivity() {
+        flicker.assertLayersEnd {
+            this.visibleRegion(letterboxTranslucentApp)
+                .coversExactly(visibleRegion(letterboxTranslucentLauncherApp).region)
+        }
+    }
+
+    /**
+     * Checks that the translucent activity has rounded corners
+     */
+    @Postsubmit
+    @Test
+    fun translucentActivityHasRoundedCorners() {
+        flicker.assertLayersEnd {
+            this.hasRoundedCorners(letterboxTranslucentApp)
+        }
+    }
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [FlickerTestFactory.rotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): Collection<FlickerTest> {
+            return LegacyFlickerTestFactory
+                .nonRotationTests(supportedRotations = listOf(Rotation.ROTATION_90))
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt
index e875aae..68fa8d2 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt
@@ -32,7 +32,9 @@
 /**
  * Test launching a fixed portrait letterboxed app in landscape and repositioning to the right.
  *
- * To run this test: `atest WMShellFlickerTests:RepositionFixedPortraitAppTest` Actions:
+ * To run this test: `atest WMShellFlickerTestsOther:RepositionFixedPortraitAppTest`
+ *
+ * Actions:
  *
  *  ```
  *      Launch a fixed portrait app in landscape to letterbox app
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RestartAppInSizeCompatModeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RestartAppInSizeCompatModeTest.kt
index a18a144..fcb6931a 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RestartAppInSizeCompatModeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RestartAppInSizeCompatModeTest.kt
@@ -31,7 +31,7 @@
 /**
  * Test restarting app in size compat mode.
  *
- * To run this test: `atest WMShellFlickerTests:RestartAppInSizeCompatModeTest`
+ * To run this test: `atest WMShellFlickerTestsOther:RestartAppInSizeCompatModeTest`
  *
  * Actions:
  * ```
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/TransparentBaseAppCompat.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/TransparentBaseAppCompat.kt
new file mode 100644
index 0000000..ea0392c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/TransparentBaseAppCompat.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.wm.shell.flicker.appcompat
+
+import android.content.Context
+import android.tools.device.flicker.legacy.FlickerTestData
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.helpers.FIND_TIMEOUT
+import android.tools.device.traces.parsers.toFlickerComponent
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiObject2
+import androidx.test.uiautomator.Until
+import com.android.server.wm.flicker.helpers.LetterboxAppHelper
+import com.android.server.wm.flicker.testapp.ActivityOptions
+import com.android.wm.shell.flicker.BaseTest
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Rule
+
+abstract class TransparentBaseAppCompat(flicker: LegacyFlickerTest) : BaseTest(flicker) {
+    protected val context: Context = instrumentation.context
+    protected val letterboxTranslucentLauncherApp = LetterboxAppHelper(
+        instrumentation,
+        launcherName = ActivityOptions.LaunchTransparentActivity.LABEL,
+        component = ActivityOptions.LaunchTransparentActivity.COMPONENT.toFlickerComponent()
+    )
+    protected val letterboxTranslucentApp = LetterboxAppHelper(
+        instrumentation,
+        launcherName = ActivityOptions.TransparentActivity.LABEL,
+        component = ActivityOptions.TransparentActivity.COMPONENT.toFlickerComponent()
+    )
+
+    @JvmField
+    @Rule
+    val letterboxRule: LetterboxRule = LetterboxRule()
+
+    @Before
+    fun before() {
+        Assume.assumeTrue(tapl.isTablet && letterboxRule.isIgnoreOrientationRequest)
+    }
+
+    protected fun FlickerTestData.waitAndGetLaunchTransparent(): UiObject2? =
+        device.wait(
+            Until.findObject(By.text("Launch Transparent")),
+            FIND_TIMEOUT
+        )
+
+    protected fun FlickerTestData.goBack() = device.pressBack()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/CopyContentInSplitGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/CopyContentInSplitGesturalNavLandscapeBenchmark.kt
deleted file mode 100644
index 566adec..0000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/CopyContentInSplitGesturalNavLandscapeBenchmark.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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.wm.shell.flicker.service.splitscreen.benchmark
-
-import android.platform.test.annotations.PlatinumTest
-import android.platform.test.annotations.Presubmit
-import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.service.splitscreen.scenarios.CopyContentInSplit
-import org.junit.Test
-
-@RequiresDevice
-class CopyContentInSplitGesturalNavLandscapeBenchmark : CopyContentInSplit(Rotation.ROTATION_90) {
-    @PlatinumTest(focusArea = "sysui")
-    @Presubmit
-    @Test
-    override fun copyContentInSplit() = super.copyContentInSplit()
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DragDividerToResizeGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DragDividerToResizeGesturalNavLandscapeBenchmark.kt
deleted file mode 100644
index 0495079..0000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DragDividerToResizeGesturalNavLandscapeBenchmark.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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.wm.shell.flicker.service.splitscreen.benchmark
-
-import android.platform.test.annotations.PlatinumTest
-import android.platform.test.annotations.Presubmit
-import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.service.splitscreen.scenarios.DragDividerToResize
-import org.junit.Test
-
-@RequiresDevice
-class DragDividerToResizeGesturalNavLandscapeBenchmark : DragDividerToResize(Rotation.ROTATION_90) {
-    @PlatinumTest(focusArea = "sysui")
-    @Presubmit
-    @Test
-    override fun dragDividerToResize() = super.dragDividerToResize()
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBetweenSplitPairsGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBetweenSplitPairsGesturalNavPortraitBenchmark.kt
deleted file mode 100644
index b074f2c..0000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBetweenSplitPairsGesturalNavPortraitBenchmark.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * 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.wm.shell.flicker.service.splitscreen.benchmark
-
-import android.platform.test.annotations.PlatinumTest
-import android.platform.test.annotations.Presubmit
-import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBetweenSplitPairs
-import org.junit.Test
-
-@RequiresDevice
-class SwitchBetweenSplitPairsGesturalNavPortraitBenchmark :
-    SwitchBetweenSplitPairs(Rotation.ROTATION_0) {
-    @PlatinumTest(focusArea = "sysui")
-    @Presubmit
-    @Test
-    override fun switchBetweenSplitPairs() = super.switchBetweenSplitPairs()
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/UnlockKeyguardToSplitScreenGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/UnlockKeyguardToSplitScreenGesturalNavPortraitBenchmark.kt
deleted file mode 100644
index 840401c..0000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/UnlockKeyguardToSplitScreenGesturalNavPortraitBenchmark.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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.wm.shell.flicker.service.splitscreen.benchmark
-
-import android.platform.test.annotations.PlatinumTest
-import android.platform.test.annotations.Presubmit
-import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.service.splitscreen.scenarios.UnlockKeyguardToSplitScreen
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.BlockJUnit4ClassRunner
-
-@RequiresDevice
-@RunWith(BlockJUnit4ClassRunner::class)
-class UnlockKeyguardToSplitScreenGesturalNavPortraitBenchmark : UnlockKeyguardToSplitScreen() {
-    @PlatinumTest(focusArea = "sysui")
-    @Presubmit
-    @Test
-    override fun unlockKeyguardToSplitScreen() = super.unlockKeyguardToSplitScreen()
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/CopyContentInSplitGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/CopyContentInSplitGesturalNavLandscape.kt
similarity index 81%
copy from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/CopyContentInSplitGesturalNavPortraitBenchmark.kt
copy to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/CopyContentInSplitGesturalNavLandscape.kt
index 92b6227..2494054 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/CopyContentInSplitGesturalNavPortraitBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/CopyContentInSplitGesturalNavLandscape.kt
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.CopyContentInSplit
 import org.junit.Test
 
-@RequiresDevice
-class CopyContentInSplitGesturalNavPortraitBenchmark : CopyContentInSplit(Rotation.ROTATION_0) {
+class CopyContentInSplitGesturalNavLandscape : CopyContentInSplit(Rotation.ROTATION_90) {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
     @Test
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/CopyContentInSplitGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/CopyContentInSplitGesturalNavPortrait.kt
similarity index 81%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/CopyContentInSplitGesturalNavPortraitBenchmark.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/CopyContentInSplitGesturalNavPortrait.kt
index 92b6227..57943ec 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/CopyContentInSplitGesturalNavPortraitBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/CopyContentInSplitGesturalNavPortrait.kt
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.CopyContentInSplit
 import org.junit.Test
 
-@RequiresDevice
-class CopyContentInSplitGesturalNavPortraitBenchmark : CopyContentInSplit(Rotation.ROTATION_0) {
+class CopyContentInSplitGesturalNavPortrait : CopyContentInSplit(Rotation.ROTATION_0) {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
     @Test
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByDividerGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DismissSplitScreenByDividerGesturalNavLandscape.kt
similarity index 84%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByDividerGesturalNavLandscapeBenchmark.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DismissSplitScreenByDividerGesturalNavLandscape.kt
index e6d56b5..6f0e202 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByDividerGesturalNavLandscapeBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DismissSplitScreenByDividerGesturalNavLandscape.kt
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.DismissSplitScreenByDivider
 import org.junit.Test
 
-@RequiresDevice
-class DismissSplitScreenByDividerGesturalNavLandscapeBenchmark :
+class DismissSplitScreenByDividerGesturalNavLandscape :
     DismissSplitScreenByDivider(Rotation.ROTATION_90) {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByDividerGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DismissSplitScreenByDividerGesturalNavPortrait.kt
similarity index 84%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByDividerGesturalNavPortraitBenchmark.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DismissSplitScreenByDividerGesturalNavPortrait.kt
index 6752c58..dac8fa2 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByDividerGesturalNavPortraitBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DismissSplitScreenByDividerGesturalNavPortrait.kt
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.DismissSplitScreenByDivider
 import org.junit.Test
 
-@RequiresDevice
-class DismissSplitScreenByDividerGesturalNavPortraitBenchmark :
+class DismissSplitScreenByDividerGesturalNavPortrait :
     DismissSplitScreenByDivider(Rotation.ROTATION_0) {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByGoHomeGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DismissSplitScreenByGoHomeGesturalNavLandscape.kt
similarity index 84%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByGoHomeGesturalNavLandscapeBenchmark.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DismissSplitScreenByGoHomeGesturalNavLandscape.kt
index 7c9ab99..baecc16 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByGoHomeGesturalNavLandscapeBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DismissSplitScreenByGoHomeGesturalNavLandscape.kt
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.DismissSplitScreenByGoHome
 import org.junit.Test
 
-@RequiresDevice
-class DismissSplitScreenByGoHomeGesturalNavLandscapeBenchmark :
+class DismissSplitScreenByGoHomeGesturalNavLandscape :
     DismissSplitScreenByGoHome(Rotation.ROTATION_90) {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByGoHomeGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DismissSplitScreenByGoHomeGesturalNavPortrait.kt
similarity index 84%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByGoHomeGesturalNavPortraitBenchmark.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DismissSplitScreenByGoHomeGesturalNavPortrait.kt
index 4b79571..3063ea5 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByGoHomeGesturalNavPortraitBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DismissSplitScreenByGoHomeGesturalNavPortrait.kt
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.DismissSplitScreenByGoHome
 import org.junit.Test
 
-@RequiresDevice
-class DismissSplitScreenByGoHomeGesturalNavPortraitBenchmark :
+class DismissSplitScreenByGoHomeGesturalNavPortrait :
     DismissSplitScreenByGoHome(Rotation.ROTATION_0) {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DragDividerToResizeGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DragDividerToResizeGesturalNavLandscape.kt
similarity index 81%
copy from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DragDividerToResizeGesturalNavPortraitBenchmark.kt
copy to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DragDividerToResizeGesturalNavLandscape.kt
index 71ef48b..41660ba 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DragDividerToResizeGesturalNavPortraitBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DragDividerToResizeGesturalNavLandscape.kt
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.DragDividerToResize
 import org.junit.Test
 
-@RequiresDevice
-class DragDividerToResizeGesturalNavPortraitBenchmark : DragDividerToResize(Rotation.ROTATION_0) {
+class DragDividerToResizeGesturalNavLandscape : DragDividerToResize(Rotation.ROTATION_90) {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
     @Test
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DragDividerToResizeGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DragDividerToResizeGesturalNavPortrait.kt
similarity index 81%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DragDividerToResizeGesturalNavPortraitBenchmark.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DragDividerToResizeGesturalNavPortrait.kt
index 71ef48b..c41ffb7 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DragDividerToResizeGesturalNavPortraitBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DragDividerToResizeGesturalNavPortrait.kt
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.DragDividerToResize
 import org.junit.Test
 
-@RequiresDevice
-class DragDividerToResizeGesturalNavPortraitBenchmark : DragDividerToResize(Rotation.ROTATION_0) {
+class DragDividerToResizeGesturalNavPortrait : DragDividerToResize(Rotation.ROTATION_0) {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
     @Test
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromAllAppsGesturalNavLandscape.kt
similarity index 84%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsGesturalNavLandscapeBenchmark.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromAllAppsGesturalNavLandscape.kt
index c78729c..afde55b 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsGesturalNavLandscapeBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromAllAppsGesturalNavLandscape.kt
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromAllApps
 import org.junit.Test
 
-@RequiresDevice
-class EnterSplitScreenByDragFromAllAppsGesturalNavLandscapeBenchmark :
+class EnterSplitScreenByDragFromAllAppsGesturalNavLandscape :
     EnterSplitScreenByDragFromAllApps(Rotation.ROTATION_90) {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromAllAppsGesturalNavPortrait.kt
similarity index 84%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsGesturalNavPortraitBenchmark.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromAllAppsGesturalNavPortrait.kt
index 30bce2f6..3765fc4 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsGesturalNavPortraitBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromAllAppsGesturalNavPortrait.kt
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromAllApps
 import org.junit.Test
 
-@RequiresDevice
-class EnterSplitScreenByDragFromAllAppsGesturalNavPortraitBenchmark :
+class EnterSplitScreenByDragFromAllAppsGesturalNavPortrait :
     EnterSplitScreenByDragFromAllApps(Rotation.ROTATION_0) {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromNotificationGesturalNavLandscape.kt
similarity index 88%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationGesturalNavLandscapeBenchmark.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromNotificationGesturalNavLandscape.kt
index b33ea7c..1e128fd 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationGesturalNavLandscapeBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromNotificationGesturalNavLandscape.kt
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromNotification
 import org.junit.Test
 
-@RequiresDevice
-class EnterSplitScreenByDragFromNotificationGesturalNavLandscapeBenchmark :
+class EnterSplitScreenByDragFromNotificationGesturalNavLandscape :
     EnterSplitScreenByDragFromNotification(Rotation.ROTATION_90) {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromNotificationGesturalNavPortrait.kt
similarity index 84%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationGesturalNavPortraitBenchmark.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromNotificationGesturalNavPortrait.kt
index 07a86a5..7767872 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationGesturalNavPortraitBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromNotificationGesturalNavPortrait.kt
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromNotification
 import org.junit.Test
 
-@RequiresDevice
-class EnterSplitScreenByDragFromNotificationGesturalNavPortraitBenchmark :
+class EnterSplitScreenByDragFromNotificationGesturalNavPortrait :
     EnterSplitScreenByDragFromNotification(Rotation.ROTATION_0) {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromShortcutGesturalNavLandscape.kt
similarity index 84%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutGesturalNavLandscapeBenchmark.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromShortcutGesturalNavLandscape.kt
index 9a1d127..4ca4bd1 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutGesturalNavLandscapeBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromShortcutGesturalNavLandscape.kt
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromShortcut
 import org.junit.Test
 
-@RequiresDevice
-class EnterSplitScreenByDragFromShortcutGesturalNavLandscapeBenchmark :
+class EnterSplitScreenByDragFromShortcutGesturalNavLandscape :
     EnterSplitScreenByDragFromShortcut(Rotation.ROTATION_90) {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromShortcutGesturalNavPortrait.kt
similarity index 84%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutGesturalNavPortraitBenchmark.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromShortcutGesturalNavPortrait.kt
index 266e268..2d9d258 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutGesturalNavPortraitBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromShortcutGesturalNavPortrait.kt
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromShortcut
 import org.junit.Test
 
-@RequiresDevice
-class EnterSplitScreenByDragFromShortcutGesturalNavPortraitBenchmark :
+class EnterSplitScreenByDragFromShortcutGesturalNavPortrait :
     EnterSplitScreenByDragFromShortcut(Rotation.ROTATION_0) {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromTaskbarGesturalNavLandscape.kt
similarity index 84%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarGesturalNavLandscapeBenchmark.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromTaskbarGesturalNavLandscape.kt
index 83fc30b..c9282ac 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarGesturalNavLandscapeBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromTaskbarGesturalNavLandscape.kt
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromTaskbar
 import org.junit.Test
 
-@RequiresDevice
-class EnterSplitScreenByDragFromTaskbarGesturalNavLandscapeBenchmark :
+class EnterSplitScreenByDragFromTaskbarGesturalNavLandscape :
     EnterSplitScreenByDragFromTaskbar(Rotation.ROTATION_90) {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromTaskbarGesturalNavPortrait.kt
similarity index 84%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarGesturalNavPortraitBenchmark.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromTaskbarGesturalNavPortrait.kt
index b2f1929..68c6d6c 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarGesturalNavPortraitBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromTaskbarGesturalNavPortrait.kt
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromTaskbar
 import org.junit.Test
 
-@RequiresDevice
-class EnterSplitScreenByDragFromTaskbarGesturalNavPortraitBenchmark :
+class EnterSplitScreenByDragFromTaskbarGesturalNavPortrait :
     EnterSplitScreenByDragFromTaskbar(Rotation.ROTATION_0) {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenFromOverviewGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenFromOverviewGesturalNavLandscape.kt
similarity index 84%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenFromOverviewGesturalNavLandscapeBenchmark.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenFromOverviewGesturalNavLandscape.kt
index dae92dd..304529e 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenFromOverviewGesturalNavLandscapeBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenFromOverviewGesturalNavLandscape.kt
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenFromOverview
 import org.junit.Test
 
-@RequiresDevice
-class EnterSplitScreenFromOverviewGesturalNavLandscapeBenchmark :
+class EnterSplitScreenFromOverviewGesturalNavLandscape :
     EnterSplitScreenFromOverview(Rotation.ROTATION_90) {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenFromOverviewGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenFromOverviewGesturalNavPortrait.kt
similarity index 84%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenFromOverviewGesturalNavPortraitBenchmark.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenFromOverviewGesturalNavPortrait.kt
index 732047b..53a6b44 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenFromOverviewGesturalNavPortraitBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenFromOverviewGesturalNavPortrait.kt
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenFromOverview
 import org.junit.Test
 
-@RequiresDevice
-class EnterSplitScreenFromOverviewGesturalNavPortraitBenchmark :
+class EnterSplitScreenFromOverviewGesturalNavPortrait :
     EnterSplitScreenFromOverview(Rotation.ROTATION_0) {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchAppByDoubleTapDividerGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchAppByDoubleTapDividerGesturalNavLandscape.kt
similarity index 84%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchAppByDoubleTapDividerGesturalNavLandscapeBenchmark.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchAppByDoubleTapDividerGesturalNavLandscape.kt
index 1de7efd..a467830 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchAppByDoubleTapDividerGesturalNavLandscapeBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchAppByDoubleTapDividerGesturalNavLandscape.kt
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchAppByDoubleTapDivider
 import org.junit.Test
 
-@RequiresDevice
-class SwitchAppByDoubleTapDividerGesturalNavLandscapeBenchmark :
+class SwitchAppByDoubleTapDividerGesturalNavLandscape :
     SwitchAppByDoubleTapDivider(Rotation.ROTATION_90) {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchAppByDoubleTapDividerGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchAppByDoubleTapDividerGesturalNavPortrait.kt
similarity index 84%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchAppByDoubleTapDividerGesturalNavPortraitBenchmark.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchAppByDoubleTapDividerGesturalNavPortrait.kt
index 1a046aa..1524233 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchAppByDoubleTapDividerGesturalNavPortraitBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchAppByDoubleTapDividerGesturalNavPortrait.kt
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchAppByDoubleTapDivider
 import org.junit.Test
 
-@RequiresDevice
-class SwitchAppByDoubleTapDividerGesturalNavPortraitBenchmark :
+class SwitchAppByDoubleTapDividerGesturalNavPortrait :
     SwitchAppByDoubleTapDivider(Rotation.ROTATION_0) {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromAnotherAppGesturalNavLandscape.kt
similarity index 84%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppGesturalNavLandscapeBenchmark.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromAnotherAppGesturalNavLandscape.kt
index 6e88f0e..0389659 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppGesturalNavLandscapeBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromAnotherAppGesturalNavLandscape.kt
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromAnotherApp
 import org.junit.Test
 
-@RequiresDevice
-class SwitchBackToSplitFromAnotherAppGesturalNavLandscapeBenchmark :
+class SwitchBackToSplitFromAnotherAppGesturalNavLandscape :
     SwitchBackToSplitFromAnotherApp(Rotation.ROTATION_90) {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromAnotherAppGesturalNavPortrait.kt
similarity index 84%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppGesturalNavPortraitBenchmark.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromAnotherAppGesturalNavPortrait.kt
index d26a29c..7fadf18 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppGesturalNavPortraitBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromAnotherAppGesturalNavPortrait.kt
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromAnotherApp
 import org.junit.Test
 
-@RequiresDevice
-class SwitchBackToSplitFromAnotherAppGesturalNavPortraitBenchmark :
+class SwitchBackToSplitFromAnotherAppGesturalNavPortrait :
     SwitchBackToSplitFromAnotherApp(Rotation.ROTATION_0) {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromHomeGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromHomeGesturalNavLandscape.kt
similarity index 84%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromHomeGesturalNavLandscapeBenchmark.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromHomeGesturalNavLandscape.kt
index 4a552b0..148cc52 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromHomeGesturalNavLandscapeBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromHomeGesturalNavLandscape.kt
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromHome
 import org.junit.Test
 
-@RequiresDevice
-class SwitchBackToSplitFromHomeGesturalNavLandscapeBenchmark :
+class SwitchBackToSplitFromHomeGesturalNavLandscape :
     SwitchBackToSplitFromHome(Rotation.ROTATION_90) {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromHomeGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromHomeGesturalNavPortrait.kt
similarity index 84%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromHomeGesturalNavPortraitBenchmark.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromHomeGesturalNavPortrait.kt
index b7376ea..0641fb0 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromHomeGesturalNavPortraitBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromHomeGesturalNavPortrait.kt
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromHome
 import org.junit.Test
 
-@RequiresDevice
-class SwitchBackToSplitFromHomeGesturalNavPortraitBenchmark :
+class SwitchBackToSplitFromHomeGesturalNavPortrait :
     SwitchBackToSplitFromHome(Rotation.ROTATION_0) {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromRecentGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromRecentGesturalNavLandscape.kt
similarity index 84%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromRecentGesturalNavLandscapeBenchmark.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromRecentGesturalNavLandscape.kt
index b2d05e4..741f871 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromRecentGesturalNavLandscapeBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromRecentGesturalNavLandscape.kt
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromRecent
 import org.junit.Test
 
-@RequiresDevice
-class SwitchBackToSplitFromRecentGesturalNavLandscapeBenchmark :
+class SwitchBackToSplitFromRecentGesturalNavLandscape :
     SwitchBackToSplitFromRecent(Rotation.ROTATION_90) {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromRecentGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromRecentGesturalNavPortrait.kt
similarity index 84%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromRecentGesturalNavPortraitBenchmark.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromRecentGesturalNavPortrait.kt
index 6de31b1..778e2d6 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromRecentGesturalNavPortraitBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromRecentGesturalNavPortrait.kt
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromRecent
 import org.junit.Test
 
-@RequiresDevice
-class SwitchBackToSplitFromRecentGesturalNavPortraitBenchmark :
+class SwitchBackToSplitFromRecentGesturalNavPortrait :
     SwitchBackToSplitFromRecent(Rotation.ROTATION_0) {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBetweenSplitPairsGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBetweenSplitPairsGesturalNavLandscape.kt
similarity index 80%
copy from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBetweenSplitPairsGesturalNavLandscapeBenchmark.kt
copy to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBetweenSplitPairsGesturalNavLandscape.kt
index aab18a6..dcdca70 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBetweenSplitPairsGesturalNavLandscapeBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBetweenSplitPairsGesturalNavLandscape.kt
@@ -14,18 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBetweenSplitPairs
 import org.junit.Test
 
-@RequiresDevice
-class SwitchBetweenSplitPairsGesturalNavLandscapeBenchmark :
-    SwitchBetweenSplitPairs(Rotation.ROTATION_90) {
+class SwitchBetweenSplitPairsGesturalNavLandscape : SwitchBetweenSplitPairs(Rotation.ROTATION_90) {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
     @Test
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBetweenSplitPairsGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBetweenSplitPairsGesturalNavPortrait.kt
similarity index 80%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBetweenSplitPairsGesturalNavLandscapeBenchmark.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBetweenSplitPairsGesturalNavPortrait.kt
index aab18a6..3c69311 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBetweenSplitPairsGesturalNavLandscapeBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBetweenSplitPairsGesturalNavPortrait.kt
@@ -14,18 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBetweenSplitPairs
 import org.junit.Test
 
-@RequiresDevice
-class SwitchBetweenSplitPairsGesturalNavLandscapeBenchmark :
-    SwitchBetweenSplitPairs(Rotation.ROTATION_90) {
+class SwitchBetweenSplitPairsGesturalNavPortrait : SwitchBetweenSplitPairs(Rotation.ROTATION_0) {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
     @Test
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/UnlockKeyguardToSplitScreenGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/UnlockKeyguardToSplitScreenGesturalNavLandscape.kt
similarity index 82%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/UnlockKeyguardToSplitScreenGesturalNavLandscapeBenchmark.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/UnlockKeyguardToSplitScreenGesturalNavLandscape.kt
index c402aa4..c6566f5 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/UnlockKeyguardToSplitScreenGesturalNavLandscapeBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/UnlockKeyguardToSplitScreenGesturalNavLandscape.kt
@@ -14,19 +14,17 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.UnlockKeyguardToSplitScreen
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.BlockJUnit4ClassRunner
 
-@RequiresDevice
 @RunWith(BlockJUnit4ClassRunner::class)
-class UnlockKeyguardToSplitScreenGesturalNavLandscapeBenchmark : UnlockKeyguardToSplitScreen() {
+class UnlockKeyguardToSplitScreenGesturalNavLandscape : UnlockKeyguardToSplitScreen() {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
     @Test
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/UnlockKeyguardToSplitScreenGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/UnlockKeyguardToSplitScreenGesturalNavPortrait.kt
similarity index 82%
copy from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/UnlockKeyguardToSplitScreenGesturalNavLandscapeBenchmark.kt
copy to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/UnlockKeyguardToSplitScreenGesturalNavPortrait.kt
index c402aa4..bb1a502 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/UnlockKeyguardToSplitScreenGesturalNavLandscapeBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/platinum/UnlockKeyguardToSplitScreenGesturalNavPortrait.kt
@@ -14,19 +14,17 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.service.splitscreen.benchmark
+package com.android.wm.shell.flicker.service.splitscreen.platinum
 
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
-import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.service.splitscreen.scenarios.UnlockKeyguardToSplitScreen
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.BlockJUnit4ClassRunner
 
-@RequiresDevice
 @RunWith(BlockJUnit4ClassRunner::class)
-class UnlockKeyguardToSplitScreenGesturalNavLandscapeBenchmark : UnlockKeyguardToSplitScreen() {
+class UnlockKeyguardToSplitScreenGesturalNavPortrait : UnlockKeyguardToSplitScreen() {
     @PlatinumTest(focusArea = "sysui")
     @Presubmit
     @Test
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt
index d44d177..9f9d4bb 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt
@@ -47,6 +47,7 @@
     fun setup() {
         tapl.setEnableRotation(true)
         tapl.setExpectedRotation(rotation.value)
+        tapl.workspace.switchToOverview().dismissAllTasks()
 
         SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp)
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandlerTest.java
index c6642f3..885ae38 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandlerTest.java
@@ -99,16 +99,17 @@
         final int taskId = 1;
         WindowContainerTransaction wct = new WindowContainerTransaction();
         doReturn(mToken).when(mTransitions)
-                .startTransition(Transitions.TRANSIT_ENTER_FREEFORM, wct,
+                .startTransition(Transitions.TRANSIT_START_MOVE_TO_DESKTOP_MODE, wct,
                         mEnterDesktopTaskTransitionHandler);
         doReturn(taskId).when(mMoveToDesktopAnimator).getTaskId();
 
-        mEnterDesktopTaskTransitionHandler.startMoveToFreeformAnimation(wct,
+        mEnterDesktopTaskTransitionHandler.startMoveToDesktop(wct,
                 mMoveToDesktopAnimator, null);
 
         TransitionInfo.Change change =
                 createChange(WindowManager.TRANSIT_CHANGE, taskId, WINDOWING_MODE_FREEFORM);
-        TransitionInfo info = createTransitionInfo(Transitions.TRANSIT_ENTER_FREEFORM, change);
+        TransitionInfo info = createTransitionInfo(Transitions.TRANSIT_START_MOVE_TO_DESKTOP_MODE,
+                change);
 
 
         assertTrue(mEnterDesktopTaskTransitionHandler
@@ -120,17 +121,18 @@
 
     @Test
     public void testTransitEnterDesktopModeAnimation() throws Throwable {
-        final int transitionType = Transitions.TRANSIT_ENTER_DESKTOP_MODE;
+        final int transitionType = Transitions.TRANSIT_FINALIZE_MOVE_TO_DESKTOP_MODE;
         final int taskId = 1;
         WindowContainerTransaction wct = new WindowContainerTransaction();
         doReturn(mToken).when(mTransitions)
                 .startTransition(transitionType, wct, mEnterDesktopTaskTransitionHandler);
-        mEnterDesktopTaskTransitionHandler.startTransition(transitionType, wct, null);
+        mEnterDesktopTaskTransitionHandler.finalizeMoveToDesktop(wct, null);
 
         TransitionInfo.Change change =
                 createChange(WindowManager.TRANSIT_CHANGE, taskId, WINDOWING_MODE_FREEFORM);
         change.setEndAbsBounds(new Rect(0, 0, 1, 1));
-        TransitionInfo info = createTransitionInfo(Transitions.TRANSIT_ENTER_DESKTOP_MODE, change);
+        TransitionInfo info = createTransitionInfo(
+                Transitions.TRANSIT_FINALIZE_MOVE_TO_DESKTOP_MODE, change);
 
         runOnUiThread(() -> {
             try {
diff --git a/libs/hwui/pipeline/skia/ShaderCache.cpp b/libs/hwui/pipeline/skia/ShaderCache.cpp
index f71e728..b870023 100644
--- a/libs/hwui/pipeline/skia/ShaderCache.cpp
+++ b/libs/hwui/pipeline/skia/ShaderCache.cpp
@@ -88,6 +88,9 @@
         mBlobCache.reset(new FileBlobCache(maxKeySize, maxValueSize, maxTotalSize, mFilename));
         validateCache(identity, size);
         mInitialized = true;
+        if (identity != nullptr && size > 0 && mIDHash.size()) {
+            set(&sIDKey, sizeof(sIDKey), mIDHash.data(), mIDHash.size());
+        }
     }
 }
 
@@ -96,11 +99,6 @@
     mFilename = filename;
 }
 
-BlobCache* ShaderCache::getBlobCacheLocked() {
-    LOG_ALWAYS_FATAL_IF(!mInitialized, "ShaderCache has not been initialized");
-    return mBlobCache.get();
-}
-
 sk_sp<SkData> ShaderCache::load(const SkData& key) {
     ATRACE_NAME("ShaderCache::load");
     size_t keySize = key.size();
@@ -115,8 +113,7 @@
     if (!valueBuffer) {
         return nullptr;
     }
-    BlobCache* bc = getBlobCacheLocked();
-    size_t valueSize = bc->get(key.data(), keySize, valueBuffer, mObservedBlobValueSize);
+    size_t valueSize = mBlobCache->get(key.data(), keySize, valueBuffer, mObservedBlobValueSize);
     int maxTries = 3;
     while (valueSize > mObservedBlobValueSize && maxTries > 0) {
         mObservedBlobValueSize = std::min(valueSize, maxValueSize);
@@ -126,7 +123,7 @@
             return nullptr;
         }
         valueBuffer = newValueBuffer;
-        valueSize = bc->get(key.data(), keySize, valueBuffer, mObservedBlobValueSize);
+        valueSize = mBlobCache->get(key.data(), keySize, valueBuffer, mObservedBlobValueSize);
         maxTries--;
     }
     if (!valueSize) {
@@ -143,16 +140,17 @@
     return SkData::MakeFromMalloc(valueBuffer, valueSize);
 }
 
-namespace {
-// Helper for BlobCache::set to trace the result.
-void set(BlobCache* cache, const void* key, size_t keySize, const void* value, size_t valueSize) {
-    switch (cache->set(key, keySize, value, valueSize)) {
+void ShaderCache::set(const void* key, size_t keySize, const void* value, size_t valueSize) {
+    switch (mBlobCache->set(key, keySize, value, valueSize)) {
         case BlobCache::InsertResult::kInserted:
             // This is what we expect/hope. It means the cache is large enough.
             return;
         case BlobCache::InsertResult::kDidClean: {
             ATRACE_FORMAT("ShaderCache: evicted an entry to fit {key: %lu value %lu}!", keySize,
                           valueSize);
+            if (mIDHash.size()) {
+                set(&sIDKey, sizeof(sIDKey), mIDHash.data(), mIDHash.size());
+            }
             return;
         }
         case BlobCache::InsertResult::kNotEnoughSpace: {
@@ -172,15 +170,10 @@
         }
     }
 }
-}  // namespace
 
 void ShaderCache::saveToDiskLocked() {
     ATRACE_NAME("ShaderCache::saveToDiskLocked");
     if (mInitialized && mBlobCache) {
-        if (mIDHash.size()) {
-            auto key = sIDKey;
-            set(mBlobCache.get(), &key, sizeof(key), mIDHash.data(), mIDHash.size());
-        }
         // The most straightforward way to make ownership shared
         mMutex.unlock();
         mMutex.lock_shared();
@@ -209,11 +202,10 @@
 
     const void* value = data.data();
 
-    BlobCache* bc = getBlobCacheLocked();
     if (mInStoreVkPipelineInProgress) {
         if (mOldPipelineCacheSize == -1) {
             // Record the initial pipeline cache size stored in the file.
-            mOldPipelineCacheSize = bc->get(key.data(), keySize, nullptr, 0);
+            mOldPipelineCacheSize = mBlobCache->get(key.data(), keySize, nullptr, 0);
         }
         if (mNewPipelineCacheSize != -1 && mNewPipelineCacheSize == valueSize) {
             // There has not been change in pipeline cache size. Stop trying to save.
@@ -228,7 +220,7 @@
         mNewPipelineCacheSize = -1;
         mTryToStorePipelineCache = true;
     }
-    set(bc, key.data(), keySize, value, valueSize);
+    set(key.data(), keySize, value, valueSize);
 
     if (!mSavePending && mDeferredSaveDelayMs > 0) {
         mSavePending = true;
diff --git a/libs/hwui/pipeline/skia/ShaderCache.h b/libs/hwui/pipeline/skia/ShaderCache.h
index c63a65a..6ccb212 100644
--- a/libs/hwui/pipeline/skia/ShaderCache.h
+++ b/libs/hwui/pipeline/skia/ShaderCache.h
@@ -97,20 +97,18 @@
     void operator=(const ShaderCache&) = delete;
 
     /**
-     * "getBlobCacheLocked" returns the BlobCache object being used to store the
-     * key/value blob pairs.  If the BlobCache object has not yet been created,
-     * this will do so, loading the serialized cache contents from disk if
-     * possible.
-     */
-    BlobCache* getBlobCacheLocked() REQUIRES(mMutex);
-
-    /**
      * "validateCache" updates the cache to match the given identity.  If the
      * cache currently has the wrong identity, all entries in the cache are cleared.
      */
     bool validateCache(const void* identity, ssize_t size) REQUIRES(mMutex);
 
     /**
+     * Helper for BlobCache::set to trace the result and ensure the identity hash
+     * does not get evicted.
+     */
+    void set(const void* key, size_t keySize, const void* value, size_t valueSize) REQUIRES(mMutex);
+
+    /**
      * "saveToDiskLocked" attempts to save the current contents of the cache to
      * disk. If the identity hash exists, we will insert the identity hash into
      * the cache for next validation.
@@ -128,11 +126,9 @@
     bool mInitialized GUARDED_BY(mMutex) = false;
 
     /**
-     * "mBlobCache" is the cache in which the key/value blob pairs are stored.  It
-     * is initially NULL, and will be initialized by getBlobCacheLocked the
-     * first time it's needed.
-     * The blob cache contains the Android build number. We treat version mismatches as an empty
-     * cache (logic implemented in BlobCache::unflatten).
+     * "mBlobCache" is the cache in which the key/value blob pairs are stored.
+     * The blob cache contains the Android build number. We treat version mismatches
+     * as an empty cache (logic implemented in BlobCache::unflatten).
      */
     std::unique_ptr<FileBlobCache> mBlobCache GUARDED_BY(mMutex);
 
diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp
index eb45707..f340945 100644
--- a/libs/hwui/renderthread/VulkanManager.cpp
+++ b/libs/hwui/renderthread/VulkanManager.cpp
@@ -41,7 +41,7 @@
 namespace uirenderer {
 namespace renderthread {
 
-static std::array<std::string_view, 19> sEnableExtensions{
+static std::array<std::string_view, 20> sEnableExtensions{
         VK_KHR_BIND_MEMORY_2_EXTENSION_NAME,
         VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME,
         VK_KHR_EXTERNAL_MEMORY_CAPABILITIES_EXTENSION_NAME,
@@ -61,6 +61,7 @@
         VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME,
         VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME,
         VK_KHR_ANDROID_SURFACE_EXTENSION_NAME,
+        VK_EXT_GLOBAL_PRIORITY_EXTENSION_NAME,
 };
 
 static bool shouldEnableExtension(const std::string_view& extension) {
diff --git a/libs/hwui/tests/macrobench/main.cpp b/libs/hwui/tests/macrobench/main.cpp
index f3f32eb..e227999 100644
--- a/libs/hwui/tests/macrobench/main.cpp
+++ b/libs/hwui/tests/macrobench/main.cpp
@@ -14,30 +14,32 @@
  * limitations under the License.
  */
 
-#include "tests/common/LeakChecker.h"
-#include "tests/common/TestScene.h"
-
-#include "Properties.h"
-#include "hwui/Typeface.h"
-#include "HardwareBitmapUploader.h"
-#include "renderthread/RenderProxy.h"
-
+#include <android-base/parsebool.h>
 #include <benchmark/benchmark.h>
+#include <errno.h>
+#include <fcntl.h>
 #include <fnmatch.h>
 #include <getopt.h>
 #include <pthread.h>
 #include <stdio.h>
+#include <sys/stat.h>
+#include <sys/types.h>
 #include <unistd.h>
+
+#include <regex>
 #include <string>
 #include <unordered_map>
 #include <vector>
 
-#include <errno.h>
-#include <fcntl.h>
-#include <sys/stat.h>
-#include <sys/types.h>
+#include "HardwareBitmapUploader.h"
+#include "Properties.h"
+#include "hwui/Typeface.h"
+#include "renderthread/RenderProxy.h"
+#include "tests/common/LeakChecker.h"
+#include "tests/common/TestScene.h"
 
 using namespace android;
+using namespace android::base;
 using namespace android::uirenderer;
 using namespace android::uirenderer::test;
 
@@ -69,6 +71,9 @@
   --onscreen           Render tests on device screen. By default tests
                        are offscreen rendered
   --benchmark_format   Set output format. Possible values are tabular, json, csv
+  --benchmark_list_tests Lists the tests that would run but does not run them
+  --benchmark_filter=<regex> Filters the test set to the given regex. If prefixed with `-` and test
+                       that doesn't match the given regex is run
   --renderer=TYPE      Sets the render pipeline to use. May be skiagl or skiavk
   --skip-leak-check    Skips the memory leak check
   --report-gpu-memory[=verbose]  Dumps the GPU memory usage after each test run
@@ -140,6 +145,9 @@
     if (!strcmp(format, "tabular")) {
         gBenchmarkReporter.reset(new benchmark::ConsoleReporter());
     } else if (!strcmp(format, "json")) {
+        // We cannot print the leak check if outputing to JSON as that will break
+        // JSON parsers since it's not JSON-formatted
+        gRunLeakCheck = false;
         gBenchmarkReporter.reset(new benchmark::JSONReporter());
     } else {
         fprintf(stderr, "Unknown format '%s'\n", format);
@@ -160,6 +168,24 @@
     return true;
 }
 
+static void addTestsThatMatchFilter(std::string spec) {
+    if (spec.empty() || spec == "all") {
+        spec = ".";  // Regexp that matches all benchmarks
+    }
+    bool isNegativeFilter = false;
+    if (spec[0] == '-') {
+        spec.replace(0, 1, "");
+        isNegativeFilter = true;
+    }
+    std::regex re(spec, std::regex_constants::extended);
+    for (auto& iter : TestScene::testMap()) {
+        if ((isNegativeFilter && !std::regex_search(iter.first, re)) ||
+            (!isNegativeFilter && std::regex_search(iter.first, re))) {
+            gRunTests.push_back(iter.second);
+        }
+    }
+}
+
 // For options that only exist in long-form. Anything in the
 // 0-255 range is reserved for short options (which just use their ASCII value)
 namespace LongOpts {
@@ -170,6 +196,8 @@
     ReportFrametime,
     CpuSet,
     BenchmarkFormat,
+    BenchmarkListTests,
+    BenchmarkFilter,
     Onscreen,
     Offscreen,
     Renderer,
@@ -179,14 +207,16 @@
 }
 
 static const struct option LONG_OPTIONS[] = {
-        {"frames", required_argument, nullptr, 'f'},
-        {"repeat", required_argument, nullptr, 'r'},
+        {"count", required_argument, nullptr, 'c'},
+        {"runs", required_argument, nullptr, 'r'},
         {"help", no_argument, nullptr, 'h'},
         {"list", no_argument, nullptr, LongOpts::List},
         {"wait-for-gpu", no_argument, nullptr, LongOpts::WaitForGpu},
         {"report-frametime", optional_argument, nullptr, LongOpts::ReportFrametime},
         {"cpuset", required_argument, nullptr, LongOpts::CpuSet},
         {"benchmark_format", required_argument, nullptr, LongOpts::BenchmarkFormat},
+        {"benchmark_list_tests", optional_argument, nullptr, LongOpts::BenchmarkListTests},
+        {"benchmark_filter", required_argument, nullptr, LongOpts::BenchmarkFilter},
         {"onscreen", no_argument, nullptr, LongOpts::Onscreen},
         {"offscreen", no_argument, nullptr, LongOpts::Offscreen},
         {"renderer", required_argument, nullptr, LongOpts::Renderer},
@@ -197,8 +227,12 @@
 static const char* SHORT_OPTIONS = "c:r:h";
 
 void parseOptions(int argc, char* argv[]) {
+    benchmark::BenchmarkReporter::Context::executable_name = (argc > 0) ? argv[0] : "unknown";
+
     int c;
     bool error = false;
+    bool listTestsOnly = false;
+    bool testsAreFiltered = false;
     opterr = 0;
 
     while (true) {
@@ -272,6 +306,21 @@
                 }
                 break;
 
+            case LongOpts::BenchmarkListTests:
+                if (!optarg || ParseBool(optarg) == ParseBoolResult::kTrue) {
+                    listTestsOnly = true;
+                }
+                break;
+
+            case LongOpts::BenchmarkFilter:
+                if (!optarg) {
+                    error = true;
+                    break;
+                }
+                addTestsThatMatchFilter(optarg);
+                testsAreFiltered = true;
+                break;
+
             case LongOpts::Renderer:
                 if (!optarg) {
                     error = true;
@@ -346,11 +395,18 @@
                 }
             }
         } while (optind < argc);
-    } else {
+    } else if (gRunTests.empty() && !testsAreFiltered) {
         for (auto& iter : TestScene::testMap()) {
             gRunTests.push_back(iter.second);
         }
     }
+
+    if (listTestsOnly) {
+        for (auto& iter : gRunTests) {
+            std::cout << iter.name << std::endl;
+        }
+        exit(EXIT_SUCCESS);
+    }
 }
 
 int main(int argc, char* argv[]) {
diff --git a/media/java/android/media/MediaRoute2Info.java b/media/java/android/media/MediaRoute2Info.java
index 1393660..91fa873 100644
--- a/media/java/android/media/MediaRoute2Info.java
+++ b/media/java/android/media/MediaRoute2Info.java
@@ -450,27 +450,27 @@
     public static final String FEATURE_REMOTE_GROUP_PLAYBACK =
             "android.media.route.feature.REMOTE_GROUP_PLAYBACK";
 
-    final String mId;
-    final CharSequence mName;
-    final List<String> mFeatures;
+    private final String mId;
+    private final CharSequence mName;
+    private final List<String> mFeatures;
     @Type
-    final int mType;
-    final boolean mIsSystem;
-    final Uri mIconUri;
-    final CharSequence mDescription;
+    private final int mType;
+    private final boolean mIsSystem;
+    private final Uri mIconUri;
+    private final CharSequence mDescription;
     @ConnectionState
-    final int mConnectionState;
-    final String mClientPackageName;
-    final String mPackageName;
-    final int mVolumeHandling;
-    final int mVolumeMax;
-    final int mVolume;
-    final String mAddress;
-    final Set<String> mDeduplicationIds;
-    final Bundle mExtras;
-    final String mProviderId;
-    final boolean mIsVisibilityRestricted;
-    final Set<String> mAllowedPackages;
+    private final int mConnectionState;
+    private final String mClientPackageName;
+    private final String mPackageName;
+    private final int mVolumeHandling;
+    private final int mVolumeMax;
+    private final int mVolume;
+    private final String mAddress;
+    private final Set<String> mDeduplicationIds;
+    private final Bundle mExtras;
+    private final String mProviderId;
+    private final boolean mIsVisibilityRestricted;
+    private final Set<String> mAllowedPackages;
 
     MediaRoute2Info(@NonNull Builder builder) {
         mId = builder.mId;
diff --git a/media/java/android/media/OWNERS b/media/java/android/media/OWNERS
index 6d6a9f8..bbe5e06 100644
--- a/media/java/android/media/OWNERS
+++ b/media/java/android/media/OWNERS
@@ -10,5 +10,8 @@
 
 per-file *Image* = file:/graphics/java/android/graphics/OWNERS
 
+per-file ExifInterface.java,ExifInterfaceUtils.java,IMediaHTTPConnection.aidl,IMediaHTTPService.aidl,JetPlayer.java,MediaDataSource.java,MediaExtractor.java,MediaHTTPConnection.java,MediaHTTPService.java,MediaPlayer.java=set noparent
+per-file ExifInterface.java,ExifInterfaceUtils.java,IMediaHTTPConnection.aidl,IMediaHTTPService.aidl,JetPlayer.java,MediaDataSource.java,MediaExtractor.java,MediaHTTPConnection.java,MediaHTTPService.java,MediaPlayer.java=file:platform/frameworks/av:/media/janitors/media_solutions_OWNERS
+
 # Haptics team also works on Ringtone
 per-file *Ringtone* = file:/services/core/java/com/android/server/vibrator/OWNERS
diff --git a/media/jni/OWNERS b/media/jni/OWNERS
index 96894d1..e12d828 100644
--- a/media/jni/OWNERS
+++ b/media/jni/OWNERS
@@ -3,3 +3,6 @@
 
 # extra for TV related files
 per-file android_media_tv_*=hgchen@google.com,quxiangfang@google.com
+
+per-file android_media_JetPlayer.cpp,android_media_MediaDataSource.cpp,android_media_MediaDataSource.h,android_media_MediaPlayer.java=set noparent
+per-file android_media_JetPlayer.cpp,android_media_MediaDataSource.cpp,android_media_MediaDataSource.h,android_media_MediaPlayer.java=file:platform/frameworks/av:/media/janitors/media_solutions_OWNERS
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallFailed.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallFailed.java
index 3505cfb..74f04e0 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallFailed.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallFailed.java
@@ -33,8 +33,6 @@
 
 import androidx.annotation.Nullable;
 
-import java.io.File;
-
 /**
  * Installation failed: Return status code to the caller or display failure UI to user
  */
@@ -101,14 +99,8 @@
             // Set header icon and title
             PackageUtil.AppSnippet as;
             PackageManager pm = getPackageManager();
-
-            if ("package".equals(packageURI.getScheme())) {
-                as = new PackageUtil.AppSnippet(pm.getApplicationLabel(appInfo),
-                        pm.getApplicationIcon(appInfo));
-            } else {
-                final File sourceFile = new File(packageURI.getPath());
-                as = PackageUtil.getAppSnippet(this, appInfo, sourceFile);
-            }
+            as = intent.getParcelableExtra(PackageInstallerActivity.EXTRA_APP_SNIPPET,
+                    PackageUtil.AppSnippet.class);
 
             // Store label for dialog
             mLabel = as.label;
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java
index 7bea339..1088ace 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java
@@ -16,6 +16,7 @@
 
 package com.android.packageinstaller;
 
+import static com.android.packageinstaller.PackageInstallerActivity.EXTRA_APP_SNIPPET;
 import static com.android.packageinstaller.PackageInstallerActivity.EXTRA_STAGED_SESSION_ID;
 
 import android.app.PendingIntent;
@@ -86,7 +87,8 @@
             // ContentResolver.SCHEME_FILE
             // STAGED_SESSION_ID extra contains an ID of a previously staged install session.
             final File sourceFile = new File(mPackageURI.getPath());
-            PackageUtil.AppSnippet as = PackageUtil.getAppSnippet(this, appInfo, sourceFile);
+            PackageUtil.AppSnippet as = getIntent()
+                    .getParcelableExtra(EXTRA_APP_SNIPPET, PackageUtil.AppSnippet.class);
 
             mAlert.setIcon(as.icon);
             mAlert.setTitle(as.label);
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java
index ff991d2..fbc9525 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java
@@ -22,7 +22,6 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
-import android.net.Uri;
 import android.os.Bundle;
 import android.util.Log;
 import android.view.View;
@@ -30,7 +29,6 @@
 
 import androidx.annotation.Nullable;
 
-import java.io.File;
 import java.util.List;
 
 /**
@@ -65,18 +63,8 @@
             ApplicationInfo appInfo =
                     intent.getParcelableExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO);
             mAppPackageName = appInfo.packageName;
-            Uri packageURI = intent.getData();
-
-            // Set header icon and title
-            PackageManager pm = getPackageManager();
-
-            if ("package".equals(packageURI.getScheme())) {
-                mAppSnippet = new PackageUtil.AppSnippet(pm.getApplicationLabel(appInfo),
-                        pm.getApplicationIcon(appInfo));
-            } else {
-                File sourceFile = new File(packageURI.getPath());
-                mAppSnippet = PackageUtil.getAppSnippet(this, appInfo, sourceFile);
-            }
+            mAppSnippet = intent.getParcelableExtra(PackageInstallerActivity.EXTRA_APP_SNIPPET,
+                    PackageUtil.AppSnippet.class);
 
             mLaunchIntent = getPackageManager().getLaunchIntentForPackage(mAppPackageName);
 
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
index bc7fa02..b66679a 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
@@ -84,6 +84,7 @@
     static final String EXTRA_CALLING_ATTRIBUTION_TAG = "EXTRA_CALLING_ATTRIBUTION_TAG";
     static final String EXTRA_ORIGINAL_SOURCE_INFO = "EXTRA_ORIGINAL_SOURCE_INFO";
     static final String EXTRA_STAGED_SESSION_ID = "EXTRA_STAGED_SESSION_ID";
+    static final String EXTRA_APP_SNIPPET = "EXTRA_APP_SNIPPET";
     private static final String ALLOW_UNKNOWN_SOURCES_KEY =
             PackageInstallerActivity.class.getName() + "ALLOW_UNKNOWN_SOURCES_KEY";
 
@@ -708,6 +709,9 @@
         if (stagedSessionId > 0) {
             newIntent.putExtra(EXTRA_STAGED_SESSION_ID, stagedSessionId);
         }
+        if (mAppSnippet != null) {
+            newIntent.putExtra(EXTRA_APP_SNIPPET, mAppSnippet);
+        }
         newIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
         if (mLocalLOGV) Log.i(TAG, "downloaded app uri=" + mPackageURI);
         startActivity(newIntent);
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageUtil.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageUtil.java
index c2d4f18..5880a29 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageUtil.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageUtil.java
@@ -28,8 +28,13 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ProviderInfo;
 import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
 import android.os.UserHandle;
 import android.util.Log;
 import android.view.View;
@@ -117,7 +122,7 @@
                 icon);
     }
 
-    static final class AppSnippet {
+    static final class AppSnippet implements Parcelable {
         @NonNull public CharSequence label;
         @Nullable public Drawable icon;
         public AppSnippet(@NonNull CharSequence label, @Nullable Drawable icon) {
@@ -125,10 +130,52 @@
             this.icon = icon;
         }
 
+        private AppSnippet(Parcel in) {
+            label = in.readString();
+            Bitmap bmp = in.readParcelable(getClass().getClassLoader(), Bitmap.class);
+            icon = new BitmapDrawable(Resources.getSystem(), bmp);
+        }
+
         @Override
         public String toString() {
             return "AppSnippet[" + label + (icon != null ? "(has" : "(no ") + " icon)]";
         }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(@NonNull Parcel dest, int flags) {
+            dest.writeString(label.toString());
+            Bitmap bmp = getBitmapFromDrawable(icon);
+            dest.writeParcelable(bmp, 0);
+        }
+
+        private Bitmap getBitmapFromDrawable(Drawable drawable) {
+            // Create an empty bitmap with the dimensions of our drawable
+            final Bitmap bmp = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
+                    drawable.getIntrinsicHeight(),
+                    Bitmap.Config.ARGB_8888);
+            // Associate it with a canvas. This canvas will draw the icon on the bitmap
+            final Canvas canvas = new Canvas(bmp);
+            // Draw the drawable in the canvas. The canvas will ultimately paint the drawable in the
+            // bitmap held within
+            drawable.draw(canvas);
+
+            return bmp;
+        }
+
+        public static final Parcelable.Creator<AppSnippet> CREATOR = new Parcelable.Creator<>() {
+            public AppSnippet createFromParcel(Parcel in) {
+                return new AppSnippet(in);
+            }
+
+            public AppSnippet[] newArray(int size) {
+                return new AppSnippet[size];
+            }
+        };
     }
 
     /**
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt
index a0ff216..ddb92b1 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt
@@ -27,14 +27,17 @@
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
-import androidx.compose.material3.Divider
+import androidx.compose.material3.HorizontalDivider
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import com.android.settingslib.development.DevelopmentSettingsEnabler
 import com.android.settingslib.spa.framework.compose.rememberDrawablePainter
 import com.android.settingslib.spa.framework.theme.SettingsDimension
 import com.android.settingslib.spa.widget.ui.SettingsBody
@@ -80,11 +83,22 @@
     }
 
     @Composable
-    fun FooterAppVersion() {
+    fun FooterAppVersion(showPackageName: Boolean = rememberIsDevelopmentSettingsEnabled()) {
         if (packageInfo.versionName == null) return
-        Divider()
-        Box(modifier = Modifier.padding(SettingsDimension.itemPadding)) {
+        HorizontalDivider()
+        Column(modifier = Modifier.padding(SettingsDimension.itemPadding)) {
             SettingsBody(stringResource(R.string.version_text, packageInfo.versionNameBidiWrapped))
+            if (showPackageName) {
+                SettingsBody(packageInfo.packageName)
+            }
+        }
+    }
+
+    @Composable
+    private fun rememberIsDevelopmentSettingsEnabled(): Boolean {
+        val context = LocalContext.current
+        return remember {
+            DevelopmentSettingsEnabler.isDevelopmentSettingsEnabled(context)
         }
     }
 
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListSwitchItem.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListSwitchItem.kt
index e846687..155905b 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListSwitchItem.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListSwitchItem.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -19,26 +19,26 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.State
 import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.widget.preference.SwitchPreference
 import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
-import com.android.settingslib.spa.widget.preference.TwoTargetSwitchPreference
 import com.android.settingslib.spaprivileged.model.app.AppRecord
 
 @Composable
 fun <T : AppRecord> AppListItemModel<T>.AppListSwitchItem(
-    onClick: () -> Unit,
     checked: State<Boolean?>,
     changeable: State<Boolean>,
     onCheckedChange: ((newChecked: Boolean) -> Unit)?,
 ) {
-    TwoTargetSwitchPreference(
+    SwitchPreference(
         model = object : SwitchPreferenceModel {
             override val title = label
             override val summary = this@AppListSwitchItem.summary
+            override val icon = @Composable {
+                AppIcon(record.app, SettingsDimension.appIconItemSize)
+            }
             override val checked = checked
             override val changeable = changeable
             override val onCheckedChange = onCheckedChange
         },
-        icon = { AppIcon(record.app, SettingsDimension.appIconItemSize) },
-        onClick = onClick,
     )
 }
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListTwoTargetSwitchItem.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListTwoTargetSwitchItem.kt
new file mode 100644
index 0000000..99d3840
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListTwoTargetSwitchItem.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.settingslib.spaprivileged.template.app
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
+import com.android.settingslib.spa.widget.preference.TwoTargetSwitchPreference
+import com.android.settingslib.spaprivileged.model.app.AppRecord
+
+@Composable
+fun <T : AppRecord> AppListItemModel<T>.AppListTwoTargetSwitchItem(
+    onClick: () -> Unit,
+    checked: State<Boolean?>,
+    changeable: State<Boolean>,
+    onCheckedChange: ((newChecked: Boolean) -> Unit)?,
+) {
+    TwoTargetSwitchPreference(
+        model = object : SwitchPreferenceModel {
+            override val title = label
+            override val summary = this@AppListTwoTargetSwitchItem.summary
+            override val checked = checked
+            override val changeable = changeable
+            override val onCheckedChange = onCheckedChange
+        },
+        icon = { AppIcon(record.app, SettingsDimension.appIconItemSize) },
+        onClick = onClick,
+    )
+}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppInfoTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppInfoTest.kt
index bb56c10..6831e56 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppInfoTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppInfoTest.kt
@@ -93,6 +93,7 @@
         val packageInfo = PackageInfo().apply {
             applicationInfo = APP
             versionName = VERSION_NAME
+            packageName = PACKAGE_NAME
         }
         val appInfoProvider = AppInfoProvider(packageInfo)
 
@@ -105,9 +106,45 @@
         composeTestRule.onNodeWithText("version $VERSION_NAME").assertIsDisplayed()
     }
 
+    @Test
+    fun footerAppVersion_developmentEnabled_packageNameIsDisplayed() {
+        val packageInfo = PackageInfo().apply {
+            applicationInfo = APP
+            versionName = VERSION_NAME
+            packageName = PACKAGE_NAME
+        }
+        val appInfoProvider = AppInfoProvider(packageInfo)
+
+        composeTestRule.setContent {
+            CompositionLocalProvider(LocalContext provides context) {
+                appInfoProvider.FooterAppVersion(true)
+            }
+        }
+        composeTestRule.onNodeWithText(PACKAGE_NAME).assertIsDisplayed()
+    }
+
+
+    @Test
+    fun footerAppVersion_developmentDisabled_packageNameDoesNotExist() {
+        val packageInfo = PackageInfo().apply {
+            applicationInfo = APP
+            versionName = VERSION_NAME
+            packageName = PACKAGE_NAME
+        }
+        val appInfoProvider = AppInfoProvider(packageInfo)
+
+        composeTestRule.setContent {
+            CompositionLocalProvider(LocalContext provides context) {
+                appInfoProvider.FooterAppVersion(false)
+            }
+        }
+        composeTestRule.onNodeWithText(PACKAGE_NAME).assertDoesNotExist()
+    }
+
     private companion object {
         const val LABEL = "Label"
         const val VERSION_NAME = "VersionName"
+        const val PACKAGE_NAME = "package.name"
         val APP = object : ApplicationInfo() {
             override fun loadLabel(pm: PackageManager) = LABEL
         }
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListSwitchItemTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListSwitchItemTest.kt
index abdcd3b..2fd1b10 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListSwitchItemTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListSwitchItemTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -43,7 +43,6 @@
     fun appLabel_displayed() {
         composeTestRule.setContent {
             ITEM_MODEL.AppListSwitchItem(
-                onClick = {},
                 checked = stateOf(null),
                 changeable = stateOf(false),
                 onCheckedChange = {},
@@ -57,7 +56,6 @@
     fun summary_displayed() {
         composeTestRule.setContent {
             ITEM_MODEL.AppListSwitchItem(
-                onClick = {},
                 checked = stateOf(null),
                 changeable = stateOf(false),
                 onCheckedChange = {},
@@ -68,27 +66,9 @@
     }
 
     @Test
-    fun title_onClick() {
-        var titleClicked = false
-        composeTestRule.setContent {
-            ITEM_MODEL.AppListSwitchItem(
-                onClick = { titleClicked = true },
-                checked = stateOf(false),
-                changeable = stateOf(false),
-                onCheckedChange = {},
-            )
-        }
-
-        composeTestRule.onNodeWithText(LABEL).performClick()
-
-        assertThat(titleClicked).isTrue()
-    }
-
-    @Test
     fun switch_checkIsNull() {
         composeTestRule.setContent {
             ITEM_MODEL.AppListSwitchItem(
-                onClick = {},
                 checked = stateOf(null),
                 changeable = stateOf(false),
                 onCheckedChange = {},
@@ -102,7 +82,6 @@
     fun switch_checked() {
         composeTestRule.setContent {
             ITEM_MODEL.AppListSwitchItem(
-                onClick = {},
                 checked = stateOf(true),
                 changeable = stateOf(false),
                 onCheckedChange = {},
@@ -116,7 +95,6 @@
     fun switch_notChecked() {
         composeTestRule.setContent {
             ITEM_MODEL.AppListSwitchItem(
-                onClick = {},
                 checked = stateOf(false),
                 changeable = stateOf(false),
                 onCheckedChange = {},
@@ -130,7 +108,6 @@
     fun switch_changeable() {
         composeTestRule.setContent {
             ITEM_MODEL.AppListSwitchItem(
-                onClick = {},
                 checked = stateOf(false),
                 changeable = stateOf(true),
                 onCheckedChange = {},
@@ -144,7 +121,6 @@
     fun switch_notChangeable() {
         composeTestRule.setContent {
             ITEM_MODEL.AppListSwitchItem(
-                onClick = {},
                 checked = stateOf(false),
                 changeable = stateOf(false),
                 onCheckedChange = {},
@@ -159,7 +135,6 @@
         var switchClicked = false
         composeTestRule.setContent {
             ITEM_MODEL.AppListSwitchItem(
-                onClick = {},
                 checked = stateOf(false),
                 changeable = stateOf(true),
                 onCheckedChange = { switchClicked = true },
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTwoTargetSwitchItemTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTwoTargetSwitchItemTest.kt
new file mode 100644
index 0000000..6e7fc8e
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTwoTargetSwitchItemTest.kt
@@ -0,0 +1,189 @@
+/*
+ * 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.settingslib.spaprivileged.template.app
+
+import android.content.pm.ApplicationInfo
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsEnabled
+import androidx.compose.ui.test.assertIsNotEnabled
+import androidx.compose.ui.test.assertIsOff
+import androidx.compose.ui.test.assertIsOn
+import androidx.compose.ui.test.isToggleable
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.framework.compose.stateOf
+import com.android.settingslib.spaprivileged.model.app.AppRecord
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class AppListTwoTargetSwitchItemTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    @Test
+    fun appLabel_displayed() {
+        composeTestRule.setContent {
+            ITEM_MODEL.AppListTwoTargetSwitchItem(
+                onClick = {},
+                checked = stateOf(null),
+                changeable = stateOf(false),
+                onCheckedChange = {},
+            )
+        }
+
+        composeTestRule.onNodeWithText(LABEL).assertIsDisplayed()
+    }
+
+    @Test
+    fun summary_displayed() {
+        composeTestRule.setContent {
+            ITEM_MODEL.AppListTwoTargetSwitchItem(
+                onClick = {},
+                checked = stateOf(null),
+                changeable = stateOf(false),
+                onCheckedChange = {},
+            )
+        }
+
+        composeTestRule.onNodeWithText(SUMMARY).assertIsDisplayed()
+    }
+
+    @Test
+    fun title_onClick() {
+        var titleClicked = false
+        composeTestRule.setContent {
+            ITEM_MODEL.AppListTwoTargetSwitchItem(
+                onClick = { titleClicked = true },
+                checked = stateOf(false),
+                changeable = stateOf(false),
+                onCheckedChange = {},
+            )
+        }
+
+        composeTestRule.onNodeWithText(LABEL).performClick()
+
+        assertThat(titleClicked).isTrue()
+    }
+
+    @Test
+    fun switch_checkIsNull() {
+        composeTestRule.setContent {
+            ITEM_MODEL.AppListTwoTargetSwitchItem(
+                onClick = {},
+                checked = stateOf(null),
+                changeable = stateOf(false),
+                onCheckedChange = {},
+            )
+        }
+
+        composeTestRule.onNode(isToggleable()).assertDoesNotExist()
+    }
+
+    @Test
+    fun switch_checked() {
+        composeTestRule.setContent {
+            ITEM_MODEL.AppListTwoTargetSwitchItem(
+                onClick = {},
+                checked = stateOf(true),
+                changeable = stateOf(false),
+                onCheckedChange = {},
+            )
+        }
+
+        composeTestRule.onNode(isToggleable()).assertIsOn()
+    }
+
+    @Test
+    fun switch_notChecked() {
+        composeTestRule.setContent {
+            ITEM_MODEL.AppListTwoTargetSwitchItem(
+                onClick = {},
+                checked = stateOf(false),
+                changeable = stateOf(false),
+                onCheckedChange = {},
+            )
+        }
+
+        composeTestRule.onNode(isToggleable()).assertIsOff()
+    }
+
+    @Test
+    fun switch_changeable() {
+        composeTestRule.setContent {
+            ITEM_MODEL.AppListTwoTargetSwitchItem(
+                onClick = {},
+                checked = stateOf(false),
+                changeable = stateOf(true),
+                onCheckedChange = {},
+            )
+        }
+
+        composeTestRule.onNode(isToggleable()).assertIsEnabled()
+    }
+
+    @Test
+    fun switch_notChangeable() {
+        composeTestRule.setContent {
+            ITEM_MODEL.AppListTwoTargetSwitchItem(
+                onClick = {},
+                checked = stateOf(false),
+                changeable = stateOf(false),
+                onCheckedChange = {},
+            )
+        }
+
+        composeTestRule.onNode(isToggleable()).assertIsNotEnabled()
+    }
+
+    @Test
+    fun switch_onClick() {
+        var switchClicked = false
+        composeTestRule.setContent {
+            ITEM_MODEL.AppListTwoTargetSwitchItem(
+                onClick = {},
+                checked = stateOf(false),
+                changeable = stateOf(true),
+                onCheckedChange = { switchClicked = true },
+            )
+        }
+
+        composeTestRule.onNode(isToggleable()).performClick()
+
+        assertThat(switchClicked).isTrue()
+    }
+
+    private companion object {
+        const val PACKAGE_NAME = "package.name"
+        const val LABEL = "Label"
+        const val SUMMARY = "Summary"
+        val APP = ApplicationInfo().apply {
+            packageName = PACKAGE_NAME
+        }
+        val ITEM_MODEL = AppListItemModel(
+            record = object : AppRecord {
+                override val app = APP
+            },
+            label = LABEL,
+            summary = stateOf(SUMMARY),
+        )
+    }
+}
diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java
index 612a928..1a938d6 100644
--- a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java
+++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java
@@ -179,7 +179,8 @@
      * Check whether tile has order.
      */
     public boolean hasOrder() {
-        return mMetaData.containsKey(META_DATA_KEY_ORDER)
+        return mMetaData != null
+                && mMetaData.containsKey(META_DATA_KEY_ORDER)
                 && mMetaData.get(META_DATA_KEY_ORDER) instanceof Integer;
     }
 
@@ -204,7 +205,7 @@
         CharSequence title = null;
         ensureMetadataNotStale(context);
         final PackageManager packageManager = context.getPackageManager();
-        if (mMetaData.containsKey(META_DATA_PREFERENCE_TITLE)) {
+        if (mMetaData != null && mMetaData.containsKey(META_DATA_PREFERENCE_TITLE)) {
             if (mMetaData.containsKey(META_DATA_PREFERENCE_TITLE_URI)) {
                 // If has as uri to provide dynamic title, skip loading here. UI will later load
                 // at tile binding time.
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index f522fd1..2118d2c 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -356,11 +356,7 @@
         connectDevice();
     }
 
-    public HearingAidInfo getHearingAidInfo() {
-        return mHearingAidInfo;
-    }
-
-    public void setHearingAidInfo(HearingAidInfo hearingAidInfo) {
+    void setHearingAidInfo(HearingAidInfo hearingAidInfo) {
         mHearingAidInfo = hearingAidInfo;
     }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
index 0c1b793..441d3a5 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
@@ -20,6 +20,7 @@
 import android.bluetooth.BluetoothCsipSetCoordinator;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothProfile;
+import android.bluetooth.le.ScanFilter;
 import android.content.Context;
 import android.util.Log;
 
@@ -114,10 +115,21 @@
     /**
      * Create and return a new {@link CachedBluetoothDevice}. This assumes
      * that {@link #findDevice} has already been called and returned null.
-     * @param device the address of the new Bluetooth device
+     * @param device the new Bluetooth device
      * @return the newly created CachedBluetoothDevice object
      */
     public CachedBluetoothDevice addDevice(BluetoothDevice device) {
+        return addDevice(device, /*leScanFilters=*/null);
+    }
+
+    /**
+     * Create and return a new {@link CachedBluetoothDevice}. This assumes
+     * that {@link #findDevice} has already been called and returned null.
+     * @param device the new Bluetooth device
+     * @param leScanFilters the BLE scan filters which the device matched
+     * @return the newly created CachedBluetoothDevice object
+     */
+    public CachedBluetoothDevice addDevice(BluetoothDevice device, List<ScanFilter> leScanFilters) {
         CachedBluetoothDevice newDevice;
         final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
         synchronized (this) {
@@ -125,7 +137,7 @@
             if (newDevice == null) {
                 newDevice = new CachedBluetoothDevice(mContext, profileManager, device);
                 mCsipDeviceManager.initCsipDeviceIfNeeded(newDevice);
-                mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(newDevice);
+                mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(newDevice, leScanFilters);
                 if (!mCsipDeviceManager.setMemberDeviceIfNeeded(newDevice)
                         && !mHearingAidDeviceManager.setSubDeviceIfNeeded(newDevice)) {
                     mCachedDevices.add(newDevice);
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
index e5e5782..efba953 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
@@ -18,10 +18,13 @@
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHearingAid;
 import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothUuid;
+import android.bluetooth.le.ScanFilter;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.media.AudioDeviceAttributes;
 import android.media.audiopolicy.AudioProductStrategy;
+import android.os.ParcelUuid;
 import android.provider.Settings;
 import android.util.Log;
 
@@ -59,7 +62,8 @@
         mRoutingHelper = routingHelper;
     }
 
-    void initHearingAidDeviceIfNeeded(CachedBluetoothDevice newDevice) {
+    void initHearingAidDeviceIfNeeded(CachedBluetoothDevice newDevice,
+            List<ScanFilter> leScanFilters) {
         long hiSyncId = getHiSyncId(newDevice.getDevice());
         if (isValidHiSyncId(hiSyncId)) {
             // Once hiSyncId is valid, assign hearing aid info
@@ -68,6 +72,21 @@
                     .setAshaDeviceMode(getDeviceMode(newDevice.getDevice()))
                     .setHiSyncId(hiSyncId);
             newDevice.setHearingAidInfo(infoBuilder.build());
+        } else if (leScanFilters != null && !newDevice.isHearingAidDevice()) {
+            // If the device is added with hearing aid scan filter during pairing, set an empty
+            // hearing aid info to indicate it's a hearing aid device. The info will be updated
+            // when corresponding profiles connected.
+            for (ScanFilter leScanFilter: leScanFilters) {
+                final ParcelUuid serviceUuid = leScanFilter.getServiceUuid();
+                final ParcelUuid serviceDataUuid = leScanFilter.getServiceDataUuid();
+                if (BluetoothUuid.HEARING_AID.equals(serviceUuid)
+                        || BluetoothUuid.HAS.equals(serviceUuid)
+                        || BluetoothUuid.HEARING_AID.equals(serviceDataUuid)
+                        || BluetoothUuid.HAS.equals(serviceDataUuid)) {
+                    newDevice.setHearingAidInfo(new HearingAidInfo.Builder().build());
+                    break;
+                }
+            }
         }
     }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractWifiMacAddressPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractWifiMacAddressPreferenceController.java
index 1d433e7..5bc27195 100644
--- a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractWifiMacAddressPreferenceController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractWifiMacAddressPreferenceController.java
@@ -59,7 +59,7 @@
 
     @Override
     public boolean isAvailable() {
-        return true;
+        return mWifiManager != null;
     }
 
     @Override
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
index 7a48838..bff51e3 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
@@ -496,16 +496,6 @@
         return info.getName();
     }
 
-    boolean shouldDisableMediaOutput(String packageName) {
-        if (TextUtils.isEmpty(packageName)) {
-            Log.w(TAG, "shouldDisableMediaOutput() package name is null or empty!");
-            return true;
-        }
-
-        // Disable when there is no transferable route
-        return getTransferableRoutes(packageName).isEmpty();
-    }
-
     @TargetApi(Build.VERSION_CODES.R)
     boolean shouldEnableVolumeSeekBar(RoutingSessionInfo sessionInfo) {
         return sessionInfo.isSystemSession() // System sessions are not remote
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
index 8479df1..fd5ca84 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
@@ -453,13 +453,6 @@
     }
 
     /**
-     * Returns {@code true} if needed to disable media output, otherwise returns {@code false}.
-     */
-    public boolean shouldDisableMediaOutput(String packageName) {
-        return mInfoMediaManager.shouldDisableMediaOutput(packageName);
-    }
-
-    /**
      * Returns {@code true} if needed to enable volume seekbar, otherwise returns {@code false}.
      */
     public boolean shouldEnableVolumeSeekBar(RoutingSessionInfo sessionInfo) {
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
index 0d5de88..ea10944 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
@@ -33,6 +33,8 @@
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHearingAid;
 import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothUuid;
+import android.bluetooth.le.ScanFilter;
 import android.content.Context;
 import android.media.AudioAttributes;
 import android.media.AudioDeviceAttributes;
@@ -147,7 +149,7 @@
                 HearingAidProfile.DeviceSide.SIDE_RIGHT);
 
         assertThat(mCachedDevice1.getHiSyncId()).isNotEqualTo(HISYNCID1);
-        mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1);
+        mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1, null);
 
         assertThat(mCachedDevice1.getHiSyncId()).isEqualTo(HISYNCID1);
         assertThat(mCachedDevice1.getDeviceMode()).isEqualTo(
@@ -164,12 +166,43 @@
         when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(
                 BluetoothHearingAid.HI_SYNC_ID_INVALID);
 
-        mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1);
+        mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1, null);
 
         verify(mCachedDevice1, never()).setHearingAidInfo(any(HearingAidInfo.class));
     }
 
     /**
+     * Test initHearingAidDeviceIfNeeded, an invalid HiSyncId and hearing aid scan filter, set an
+     * empty hearing aid info on the device.
+     */
+    @Test
+    public void initHearingAidDeviceIfNeeded_hearingAidScanFilter_setHearingAidInfo() {
+        when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(
+                BluetoothHearingAid.HI_SYNC_ID_INVALID);
+        final ScanFilter scanFilter = new ScanFilter.Builder()
+                .setServiceData(BluetoothUuid.HEARING_AID, new byte[]{0}).build();
+
+        mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1, List.of(scanFilter));
+
+        assertThat(mCachedDevice1.isHearingAidDevice()).isTrue();
+    }
+
+    /**
+     * Test initHearingAidDeviceIfNeeded, an invalid HiSyncId and random scan filter, not to set
+     * hearing aid info on the device.
+     */
+    @Test
+    public void initHearingAidDeviceIfNeeded_randomScanFilter_setHearingAidInfo() {
+        when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(
+                BluetoothHearingAid.HI_SYNC_ID_INVALID);
+        final ScanFilter scanFilter = new ScanFilter.Builder().build();
+
+        mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1, List.of(scanFilter));
+
+        assertThat(mCachedDevice1.isHearingAidDevice()).isFalse();
+    }
+
+    /**
      * Test setSubDeviceIfNeeded, a device with same HiSyncId will be set as sub device
      */
     @Test
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
index 45b5de4..866ef9d 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
@@ -1083,51 +1083,4 @@
         assertThat(device.getState()).isEqualTo(STATE_SELECTED);
         assertThat(mInfoMediaManager.getCurrentConnectedDevice()).isEqualTo(device);
     }
-
-    @Test
-    public void shouldDisableMediaOutput_infosIsEmpty_returnsTrue() {
-        mShadowRouter2Manager.setTransferableRoutes(new ArrayList<>());
-
-        assertThat(mInfoMediaManager.shouldDisableMediaOutput("test")).isTrue();
-    }
-
-    @Test
-    public void shouldDisableMediaOutput_infosSizeEqual1_returnsFalse() {
-        final MediaRoute2Info info = mock(MediaRoute2Info.class);
-        final List<MediaRoute2Info> infos = new ArrayList<>();
-        infos.add(info);
-        mShadowRouter2Manager.setTransferableRoutes(infos);
-
-        when(info.getType()).thenReturn(TYPE_REMOTE_SPEAKER);
-
-        assertThat(mInfoMediaManager.shouldDisableMediaOutput("test")).isFalse();
-    }
-
-    @Test
-    public void shouldDisableMediaOutput_infosSizeEqual1AndNotCastDevice_returnsFalse() {
-        final MediaRoute2Info info = mock(MediaRoute2Info.class);
-        final List<MediaRoute2Info> infos = new ArrayList<>();
-        infos.add(info);
-        mShadowRouter2Manager.setTransferableRoutes(infos);
-
-        when(info.getType()).thenReturn(TYPE_BUILTIN_SPEAKER);
-
-        assertThat(mInfoMediaManager.shouldDisableMediaOutput("test")).isFalse();
-    }
-
-
-    @Test
-    public void shouldDisableMediaOutput_infosSizeOverThan1_returnsFalse() {
-        final MediaRoute2Info info = mock(MediaRoute2Info.class);
-        final MediaRoute2Info info2 = mock(MediaRoute2Info.class);
-        final List<MediaRoute2Info> infos = new ArrayList<>();
-        infos.add(info);
-        infos.add(info2);
-        mShadowRouter2Manager.setTransferableRoutes(infos);
-
-        when(info.getType()).thenReturn(TYPE_REMOTE_SPEAKER);
-        when(info2.getType()).thenReturn(TYPE_REMOTE_SPEAKER);
-
-        assertThat(mInfoMediaManager.shouldDisableMediaOutput("test")).isFalse();
-    }
 }
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 7d8b066..368115b 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -320,6 +320,7 @@
 
 
     <uses-permission android:name="android.permission.CONTROL_KEYGUARD" />
+    <uses-permission android:name="android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS" />
     <uses-permission android:name="android.permission.SUSPEND_APPS" />
     <uses-permission android:name="android.permission.OBSERVE_APP_USAGE" />
     <uses-permission android:name="android.permission.READ_CLIPBOARD_IN_BACKGROUND" />
diff --git a/packages/SystemUI/OWNERS b/packages/SystemUI/OWNERS
index 5fdf354..bba926d 100644
--- a/packages/SystemUI/OWNERS
+++ b/packages/SystemUI/OWNERS
@@ -23,6 +23,7 @@
 brycelee@google.com
 brzezinski@google.com
 caitlinshk@google.com
+cameronyee@google.com
 chandruis@google.com
 chrisgollner@google.com
 cinek@google.com
@@ -44,6 +45,7 @@
 jjaggi@google.com
 jonmiranda@google.com
 joshtrask@google.com
+juansmartinez@google.com
 juliacr@google.com
 juliatuttle@google.com
 justinkoh@google.com
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
index 8e79e3c..38b99cc 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
@@ -70,6 +70,9 @@
          * If a new layout change happens while an animation is already in progress, the animation
          * is updated to continue from the current values to the new end state.
          *
+         * A set of [excludedViews] can be passed. If any dependent view from [rootView] matches an
+         * entry in this set, changes to that view will not be animated.
+         *
          * The animator continues to respond to layout changes until [stopAnimating] is called.
          *
          * Successive calls to this method override the previous settings ([interpolator] and
@@ -82,9 +85,16 @@
         fun animate(
             rootView: View,
             interpolator: Interpolator = DEFAULT_INTERPOLATOR,
-            duration: Long = DEFAULT_DURATION
+            duration: Long = DEFAULT_DURATION,
+            excludedViews: Set<View> = emptySet()
         ): Boolean {
-            return animate(rootView, interpolator, duration, ephemeral = false)
+            return animate(
+                rootView,
+                interpolator,
+                duration,
+                ephemeral = false,
+                excludedViews = excludedViews
+            )
         }
 
         /**
@@ -95,16 +105,24 @@
         fun animateNextUpdate(
             rootView: View,
             interpolator: Interpolator = DEFAULT_INTERPOLATOR,
-            duration: Long = DEFAULT_DURATION
+            duration: Long = DEFAULT_DURATION,
+            excludedViews: Set<View> = emptySet()
         ): Boolean {
-            return animate(rootView, interpolator, duration, ephemeral = true)
+            return animate(
+                rootView,
+                interpolator,
+                duration,
+                ephemeral = true,
+                excludedViews = excludedViews
+            )
         }
 
         private fun animate(
             rootView: View,
             interpolator: Interpolator,
             duration: Long,
-            ephemeral: Boolean
+            ephemeral: Boolean,
+            excludedViews: Set<View> = emptySet()
         ): Boolean {
             if (
                 !occupiesSpace(
@@ -119,7 +137,7 @@
             }
 
             val listener = createUpdateListener(interpolator, duration, ephemeral)
-            addListener(rootView, listener, recursive = true)
+            addListener(rootView, listener, recursive = true, excludedViews = excludedViews)
             return true
         }
 
@@ -921,8 +939,11 @@
         private fun addListener(
             view: View,
             listener: View.OnLayoutChangeListener,
-            recursive: Boolean = false
+            recursive: Boolean = false,
+            excludedViews: Set<View> = emptySet()
         ) {
+            if (excludedViews.contains(view)) return
+
             // Make sure that only one listener is active at a time.
             val previousListener = view.getTag(R.id.tag_layout_listener)
             if (previousListener != null && previousListener is View.OnLayoutChangeListener) {
@@ -933,7 +954,12 @@
             view.setTag(R.id.tag_layout_listener, listener)
             if (view is ViewGroup && recursive) {
                 for (i in 0 until view.childCount) {
-                    addListener(view.getChildAt(i), listener, recursive = true)
+                    addListener(
+                        view.getChildAt(i),
+                        listener,
+                        recursive = true,
+                        excludedViews = excludedViews
+                    )
                 }
             }
         }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index ca91b8a..38b751c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -17,15 +17,19 @@
 
 package com.android.systemui.notifications.ui.composable
 
+import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.defaultMinSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
 import androidx.compose.ui.unit.dp
 
 @Composable
@@ -34,10 +38,26 @@
 ) {
     // TODO(b/272779828): implement.
     Column(
-        modifier = modifier.fillMaxWidth().defaultMinSize(minHeight = 300.dp).padding(4.dp),
+        modifier =
+            modifier
+                .fillMaxWidth()
+                .defaultMinSize(minHeight = 300.dp)
+                .clip(RoundedCornerShape(32.dp))
+                .background(MaterialTheme.colorScheme.surface)
+                .padding(16.dp),
     ) {
-        Text("Notifications", modifier = Modifier.align(Alignment.CenterHorizontally))
+        Text(
+            text = "Notifications",
+            modifier = Modifier.align(Alignment.CenterHorizontally),
+            style = MaterialTheme.typography.titleLarge,
+            color = MaterialTheme.colorScheme.onSurface,
+        )
         Spacer(modifier = Modifier.weight(1f))
-        Text("Shelf", modifier = Modifier.align(Alignment.CenterHorizontally))
+        Text(
+            text = "Shelf",
+            modifier = Modifier.align(Alignment.CenterHorizontally),
+            style = MaterialTheme.typography.titleSmall,
+            color = MaterialTheme.colorScheme.onSurface,
+        )
     }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/QuickSettings.kt
index 665d6dd..1bb341c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/QuickSettings.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/QuickSettings.kt
@@ -17,15 +17,19 @@
 
 package com.android.systemui.qs.footer.ui.compose
 
+import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.defaultMinSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
 import androidx.compose.ui.unit.dp
 
 @Composable
@@ -34,10 +38,26 @@
 ) {
     // TODO(b/272780058): implement.
     Column(
-        modifier = modifier.fillMaxWidth().defaultMinSize(minHeight = 300.dp).padding(4.dp),
+        modifier =
+            modifier
+                .fillMaxWidth()
+                .defaultMinSize(minHeight = 300.dp)
+                .clip(RoundedCornerShape(32.dp))
+                .background(MaterialTheme.colorScheme.primary)
+                .padding(16.dp),
     ) {
-        Text("Quick settings", modifier = Modifier.align(Alignment.CenterHorizontally))
+        Text(
+            text = "Quick settings",
+            modifier = Modifier.align(Alignment.CenterHorizontally),
+            style = MaterialTheme.typography.titleLarge,
+            color = MaterialTheme.colorScheme.onPrimary,
+        )
         Spacer(modifier = Modifier.weight(1f))
-        Text("QS footer actions", modifier = Modifier.align(Alignment.CenterHorizontally))
+        Text(
+            text = "QS footer actions",
+            modifier = Modifier.align(Alignment.CenterHorizontally),
+            style = MaterialTheme.typography.titleSmall,
+            color = MaterialTheme.colorScheme.onPrimary,
+        )
     }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index 20e1751..27358f5 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -16,18 +16,19 @@
 
 package com.android.systemui.shade.ui.composable
 
+import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.material3.Button
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.notifications.ui.composable.Notifications
+import com.android.systemui.qs.footer.ui.compose.QuickSettings
 import com.android.systemui.scene.shared.model.Direction
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
@@ -62,12 +63,7 @@
     override fun Content(
         containerName: String,
         modifier: Modifier,
-    ) {
-        ShadeScene(
-            viewModel = viewModel,
-            modifier = modifier,
-        )
-    }
+    ) = ShadeScene(viewModel, modifier)
 
     private fun destinationScenes(
         up: SceneKey,
@@ -84,23 +80,15 @@
     viewModel: ShadeSceneViewModel,
     modifier: Modifier = Modifier,
 ) {
-    // TODO(b/280887022): implement the real UI.
-
-    Box(modifier = modifier) {
-        Column(
-            horizontalAlignment = Alignment.CenterHorizontally,
-            modifier = Modifier.align(Alignment.Center)
-        ) {
-            Text("Shade", style = MaterialTheme.typography.headlineMedium)
-            Row(
-                horizontalArrangement = Arrangement.spacedBy(8.dp),
-            ) {
-                Button(
-                    onClick = { viewModel.onContentClicked() },
-                ) {
-                    Text("Open some content")
-                }
-            }
-        }
+    Column(
+        horizontalAlignment = Alignment.CenterHorizontally,
+        verticalArrangement = Arrangement.spacedBy(16.dp),
+        modifier =
+            Modifier.fillMaxSize()
+                .clickable(onClick = { viewModel.onContentClicked() })
+                .padding(horizontal = 16.dp, vertical = 48.dp)
+    ) {
+        QuickSettings(modifier = modifier.height(160.dp))
+        Notifications(modifier = modifier.weight(1f))
     }
 }
diff --git a/packages/SystemUI/ktfmt_includes.txt b/packages/SystemUI/ktfmt_includes.txt
index 18753fd..006fc09 100644
--- a/packages/SystemUI/ktfmt_includes.txt
+++ b/packages/SystemUI/ktfmt_includes.txt
@@ -422,9 +422,6 @@
 -packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallFlags.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallLogger.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherContainer.kt
--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/model/ConnectivitySlots.kt
--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryStateNotifier.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsController.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt
@@ -696,7 +693,6 @@
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallChronometerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallLoggerTest.kt
--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryStateNotifierTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ClockTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImplTest.kt
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml
index 57b3acd..66c54f2 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml
@@ -21,6 +21,7 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_marginTop="@dimen/keyguard_lock_padding"
+        android:importantForAccessibility="no"
         android:ellipsize="marquee"
         android:focusable="true"
         android:gravity="center"
diff --git a/packages/SystemUI/res/layout/media_smartspace_recommendations.xml b/packages/SystemUI/res/layout/media_smartspace_recommendations.xml
deleted file mode 100644
index 9304ff7..0000000
--- a/packages/SystemUI/res/layout/media_smartspace_recommendations.xml
+++ /dev/null
@@ -1,136 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2019 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License
-  -->
-
-<!-- Layout for media recommendations inside QSPanel carousel -->
-<!-- See media_recommendation_expanded.xml and media_recommendation_collapsed.xml for the
-     constraints. -->
-<com.android.systemui.util.animation.TransitionLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/media_recommendations"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:clipChildren="false"
-    android:clipToPadding="false"
-    android:forceHasOverlappingRendering="false"
-    android:background="@drawable/qs_media_background"
-    android:theme="@style/MediaPlayer">
-
-    <!-- This view just ensures the full media player is a certain height. -->
-    <View
-        android:id="@+id/sizing_view"
-        android:layout_width="match_parent"
-        android:layout_height="@dimen/qs_media_session_height_expanded" />
-
-    <com.android.internal.widget.CachingIconView
-        android:id="@+id/recommendation_card_icon"
-        android:layout_width="@dimen/qs_media_app_icon_size"
-        android:layout_height="@dimen/qs_media_app_icon_size"
-        android:minWidth="@dimen/qs_media_app_icon_size"
-        android:minHeight="@dimen/qs_media_app_icon_size"
-        android:layout_marginStart="@dimen/qs_media_padding"
-        android:layout_marginTop="@dimen/qs_media_rec_icon_top_margin"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent" />
-
-    <FrameLayout
-        android:id="@+id/media_cover1_container"
-        style="@style/MediaPlayer.Recommendation.AlbumContainer"
-        >
-        <ImageView
-            android:id="@+id/media_cover1"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:minWidth="@dimen/qs_media_rec_album_size"
-            android:minHeight="@dimen/qs_media_rec_album_size"
-            app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintBottom_toBottomOf="parent"
-            android:adjustViewBounds="true"
-            android:background="@drawable/bg_smartspace_media_item"
-            style="@style/MediaPlayer.Recommendation.Album"
-            android:clipToOutline="true"
-            android:scaleType="centerCrop"/>
-    </FrameLayout>
-
-    <TextView
-        android:id="@+id/media_title1"
-        style="@style/MediaPlayer.Recommendation.Text.Title"
-        />
-
-    <TextView
-        android:id="@+id/media_subtitle1"
-        style="@style/MediaPlayer.Recommendation.Text.Subtitle"
-        />
-
-    <FrameLayout
-        android:id="@+id/media_cover2_container"
-        style="@style/MediaPlayer.Recommendation.AlbumContainer"
-        >
-        <ImageView
-            android:id="@+id/media_cover2"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:minWidth="@dimen/qs_media_rec_album_size"
-            android:minHeight="@dimen/qs_media_rec_album_size"
-            android:adjustViewBounds="true"
-            android:background="@drawable/bg_smartspace_media_item"
-            style="@style/MediaPlayer.Recommendation.Album"
-            android:clipToOutline="true"
-            android:scaleType="centerCrop"/>
-    </FrameLayout>
-
-    <TextView
-        android:id="@+id/media_title2"
-        style="@style/MediaPlayer.Recommendation.Text.Title"
-        />
-
-    <TextView
-        android:id="@+id/media_subtitle2"
-        style="@style/MediaPlayer.Recommendation.Text.Subtitle"
-        />
-
-    <FrameLayout
-        android:id="@+id/media_cover3_container"
-        style="@style/MediaPlayer.Recommendation.AlbumContainer"
-        >
-        <ImageView
-            android:id="@+id/media_cover3"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:minWidth="@dimen/qs_media_rec_album_size"
-            android:minHeight="@dimen/qs_media_rec_album_size"
-            android:adjustViewBounds="true"
-            android:background="@drawable/bg_smartspace_media_item"
-            style="@style/MediaPlayer.Recommendation.Album"
-            android:clipToOutline="true"
-            android:scaleType="centerCrop"/>
-    </FrameLayout>
-
-    <TextView
-        android:id="@+id/media_title3"
-        style="@style/MediaPlayer.Recommendation.Text.Title"
-        />
-
-    <TextView
-        android:id="@+id/media_subtitle3"
-        style="@style/MediaPlayer.Recommendation.Text.Subtitle"
-        />
-
-    <include
-        layout="@layout/media_long_press_menu" />
-
-</com.android.systemui.util.animation.TransitionLayout>
diff --git a/packages/SystemUI/res/layout/super_notification_shade.xml b/packages/SystemUI/res/layout/super_notification_shade.xml
index 2fde947..a336252 100644
--- a/packages/SystemUI/res/layout/super_notification_shade.xml
+++ b/packages/SystemUI/res/layout/super_notification_shade.xml
@@ -76,6 +76,13 @@
              android:layout_height="match_parent"
              android:visibility="invisible" />
 
+    <!-- Shared container for the notification stack. Can be positioned by either
+         the keyguard_root_view or notification_panel -->
+    <com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
+        android:id="@+id/shared_notification_container"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
     <include layout="@layout/brightness_mirror_container" />
 
     <com.android.systemui.scrim.ScrimView
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 47cd1e7..3366f4f 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -663,9 +663,6 @@
 
     <dimen name="restricted_padlock_pading">4dp</dimen>
 
-    <!-- How far the expanded QS panel peeks from the header in collapsed state. -->
-    <dimen name="qs_peek_height">0dp</dimen>
-
     <!-- Padding between subtitles and the following text in the QSFooter dialog -->
     <dimen name="qs_footer_dialog_subtitle_padding">20dp</dimen>
 
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index 134a7a9..3a2177a 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -211,6 +211,7 @@
     <item type="id" name="keyguard_indication_area" />
     <item type="id" name="keyguard_indication_text" />
     <item type="id" name="keyguard_indication_text_bottom" />
+    <item type="id" name="nssl_guideline" />
     <item type="id" name="lock_icon" />
     <item type="id" name="lock_icon_bg" />
 </resources>
diff --git a/packages/SystemUI/res/xml/media_recommendation_collapsed.xml b/packages/SystemUI/res/xml/media_recommendation_collapsed.xml
deleted file mode 100644
index b7d4b3a..0000000
--- a/packages/SystemUI/res/xml/media_recommendation_collapsed.xml
+++ /dev/null
@@ -1,101 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2020 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License
-  -->
-<ConstraintSet
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto" >
-
-    <Constraint
-        android:id="@+id/sizing_view"
-        android:layout_width="match_parent"
-        android:layout_height="@dimen/qs_media_session_height_collapsed"
-        />
-
-    <!-- Only the constraintBottom and marginBottom are different. The rest of the constraints are
-         the same as the constraints in media_recommendations_expanded.xml. But, due to how
-         ConstraintSets work, all the constraints need to be in the same place. So, the shared
-         constraints can't be put in the shared layout file media_smartspace_recommendations.xml and
-         the constraints are instead duplicated between here and media_recommendations_expanded.xml.
-         Ditto for the other cover containers. -->
-    <Constraint
-        android:id="@+id/media_cover1_container"
-        app:layout_constraintBottom_toBottomOf="parent"
-        android:layout_marginBottom="@dimen/qs_media_padding"
-        style="@style/MediaPlayer.Recommendation.AlbumContainer"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintEnd_toStartOf="@id/media_cover2_container"
-        android:layout_marginEnd="@dimen/qs_media_rec_album_side_margin"
-        app:layout_constraintHorizontal_chainStyle="packed"
-        app:layout_constraintHorizontal_bias="1.0"
-        app:layout_constraintVertical_bias="0.5"
-        />
-
-    <Constraint
-        android:id="@+id/media_title1"
-        android:visibility="gone"
-        />
-
-    <Constraint
-        android:id="@+id/media_subtitle1"
-        android:visibility="gone"
-        />
-
-    <Constraint
-        android:id="@+id/media_cover2_container"
-        app:layout_constraintBottom_toBottomOf="parent"
-        android:layout_marginBottom="@dimen/qs_media_padding"
-        style="@style/MediaPlayer.Recommendation.AlbumContainer"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintStart_toEndOf="@id/media_cover1_container"
-        app:layout_constraintEnd_toStartOf="@id/media_cover3_container"
-        android:layout_marginEnd="@dimen/qs_media_rec_album_side_margin"
-        app:layout_constraintVertical_bias="0.5"
-        />
-
-    <Constraint
-        android:id="@+id/media_title2"
-        android:visibility="gone"
-        />
-
-    <Constraint
-        android:id="@+id/media_subtitle2"
-        android:visibility="gone"
-        />
-
-    <Constraint
-        android:id="@+id/media_cover3_container"
-        app:layout_constraintBottom_toBottomOf="parent"
-        android:layout_marginBottom="@dimen/qs_media_padding"
-        style="@style/MediaPlayer.Recommendation.AlbumContainer"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintStart_toEndOf="@id/media_cover2_container"
-        app:layout_constraintEnd_toEndOf="parent"
-        android:layout_marginEnd="@dimen/qs_media_padding"
-        app:layout_constraintVertical_bias="0.5"
-        />
-
-    <Constraint
-        android:id="@+id/media_title3"
-        android:visibility="gone"
-        />
-
-    <Constraint
-        android:id="@+id/media_subtitle3"
-        android:visibility="gone"
-        />
-
-</ConstraintSet>
diff --git a/packages/SystemUI/res/xml/media_recommendation_expanded.xml b/packages/SystemUI/res/xml/media_recommendation_expanded.xml
deleted file mode 100644
index ce25a7d..0000000
--- a/packages/SystemUI/res/xml/media_recommendation_expanded.xml
+++ /dev/null
@@ -1,123 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2020 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License
-  -->
-<ConstraintSet
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    >
-
-    <Constraint
-        android:id="@+id/sizing_view"
-        android:layout_width="match_parent"
-        android:layout_height="@dimen/qs_media_session_height_expanded"
-        />
-
-    <Constraint
-        android:id="@+id/media_cover1_container"
-        style="@style/MediaPlayer.Recommendation.AlbumContainer"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintBottom_toTopOf="@+id/media_title1"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintEnd_toStartOf="@id/media_cover2_container"
-        android:layout_marginEnd="@dimen/qs_media_rec_album_side_margin"
-        app:layout_constraintHorizontal_chainStyle="packed"
-        app:layout_constraintVertical_chainStyle="packed"
-        app:layout_constraintHorizontal_bias="1.0"
-        app:layout_constraintVertical_bias="0.4"
-        />
-
-    <Constraint
-        android:id="@+id/media_title1"
-        style="@style/MediaPlayer.Recommendation.Text.Title"
-        app:layout_constraintStart_toStartOf="@+id/media_cover1_container"
-        app:layout_constraintEnd_toEndOf="@+id/media_cover1_container"
-        app:layout_constraintTop_toBottomOf="@+id/media_cover1_container"
-        app:layout_constraintBottom_toTopOf="@+id/media_subtitle1"
-        />
-
-    <Constraint
-        android:id="@+id/media_subtitle1"
-        style="@style/MediaPlayer.Recommendation.Text.Subtitle"
-        app:layout_constraintStart_toStartOf="@+id/media_cover1_container"
-        app:layout_constraintEnd_toEndOf="@+id/media_cover1_container"
-        app:layout_constraintTop_toBottomOf="@+id/media_title1"
-        app:layout_constraintBottom_toBottomOf="parent"
-        android:layout_marginBottom="@dimen/qs_media_padding"
-        />
-
-    <Constraint
-        android:id="@+id/media_cover2_container"
-        style="@style/MediaPlayer.Recommendation.AlbumContainer"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintBottom_toTopOf="@id/media_title2"
-        app:layout_constraintStart_toEndOf="@id/media_cover1_container"
-        app:layout_constraintEnd_toStartOf="@id/media_cover3_container"
-        android:layout_marginEnd="@dimen/qs_media_rec_album_side_margin"
-        app:layout_constraintVertical_chainStyle="packed"
-        app:layout_constraintVertical_bias="0.4"
-        />
-
-    <Constraint
-        android:id="@+id/media_title2"
-        style="@style/MediaPlayer.Recommendation.Text.Title"
-        app:layout_constraintStart_toStartOf="@+id/media_cover2_container"
-        app:layout_constraintEnd_toEndOf="@+id/media_cover2_container"
-        app:layout_constraintTop_toBottomOf="@+id/media_cover2_container"
-        app:layout_constraintBottom_toTopOf="@+id/media_subtitle2"
-        />
-
-    <Constraint
-        android:id="@+id/media_subtitle2"
-        style="@style/MediaPlayer.Recommendation.Text.Subtitle"
-        app:layout_constraintStart_toStartOf="@+id/media_cover2_container"
-        app:layout_constraintEnd_toEndOf="@+id/media_cover2_container"
-        app:layout_constraintTop_toBottomOf="@+id/media_title2"
-        app:layout_constraintBottom_toBottomOf="parent"
-        android:layout_marginBottom="@dimen/qs_media_padding"
-        />
-
-    <Constraint
-        android:id="@+id/media_cover3_container"
-        style="@style/MediaPlayer.Recommendation.AlbumContainer"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintBottom_toTopOf="@id/media_title3"
-        app:layout_constraintStart_toEndOf="@id/media_cover2_container"
-        app:layout_constraintEnd_toEndOf="parent"
-        android:layout_marginEnd="@dimen/qs_media_padding"
-        app:layout_constraintVertical_chainStyle="packed"
-        app:layout_constraintVertical_bias="0.4"
-        />
-
-    <Constraint
-        android:id="@+id/media_title3"
-        style="@style/MediaPlayer.Recommendation.Text.Title"
-        app:layout_constraintStart_toStartOf="@+id/media_cover3_container"
-        app:layout_constraintEnd_toEndOf="@+id/media_cover3_container"
-        app:layout_constraintTop_toBottomOf="@+id/media_cover3_container"
-        app:layout_constraintBottom_toTopOf="@+id/media_subtitle3"
-        />
-
-    <Constraint
-        android:id="@+id/media_subtitle3"
-        style="@style/MediaPlayer.Recommendation.Text.Subtitle"
-        app:layout_constraintStart_toStartOf="@+id/media_cover3_container"
-        app:layout_constraintEnd_toEndOf="@+id/media_cover3_container"
-        app:layout_constraintTop_toBottomOf="@+id/media_title3"
-        app:layout_constraintBottom_toBottomOf="parent"
-        android:layout_marginBottom="@dimen/qs_media_padding"
-        />
-
-</ConstraintSet>
diff --git a/packages/SystemUI/res/xml/media_recommendations_view_collapsed.xml b/packages/SystemUI/res/xml/media_recommendations_collapsed.xml
similarity index 100%
rename from packages/SystemUI/res/xml/media_recommendations_view_collapsed.xml
rename to packages/SystemUI/res/xml/media_recommendations_collapsed.xml
diff --git a/packages/SystemUI/res/xml/media_recommendations_view_expanded.xml b/packages/SystemUI/res/xml/media_recommendations_expanded.xml
similarity index 100%
rename from packages/SystemUI/res/xml/media_recommendations_view_expanded.xml
rename to packages/SystemUI/res/xml/media_recommendations_expanded.xml
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockFrame.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardClockFrame.kt
index 50e5466..1cb8e43 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockFrame.kt
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockFrame.kt
@@ -12,12 +12,9 @@
 ) : FrameLayout(context, attrs) {
     private var drawAlpha: Int = 255
 
-    init {
-        setLayerType(View.LAYER_TYPE_SOFTWARE, null)
-    }
-
     protected override fun onSetAlpha(alpha: Int): Boolean {
-        drawAlpha = alpha
+        // Ignore alpha passed from View, prefer to compute it from set values
+        drawAlpha = (255 * this.alpha * transitionAlpha).toInt()
         return true
     }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java
index 2377057..d9b7bde 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java
@@ -69,7 +69,7 @@
                 (long) (125 * KeyguardPatternView.DISAPPEAR_MULTIPLIER_LOCKED),
                 0.6f /* translationScale */,
                 0.45f /* delayScale */, AnimationUtils.loadInterpolator(
-                        mContext, android.R.interpolator.fast_out_linear_in));
+                       mContext, android.R.interpolator.fast_out_linear_in));
         mDisappearYTranslation = getResources().getDimensionPixelSize(
                 R.dimen.disappear_y_translation);
         mYTrans = getResources().getDimensionPixelSize(R.dimen.pin_view_trans_y_entry);
@@ -82,8 +82,10 @@
     }
 
     void onDevicePostureChanged(@DevicePostureInt int posture) {
-        mLastDevicePosture = posture;
-        updateMargins();
+        if (mLastDevicePosture != posture) {
+            mLastDevicePosture = posture;
+            updateMargins();
+        }
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java
index 38c07dc..2bdf46e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java
@@ -104,8 +104,10 @@
     }
 
     void onDevicePostureChanged(@DevicePostureInt int posture) {
-        mLastDevicePosture = posture;
-        updateMargins();
+        if (mLastDevicePosture != posture) {
+            mLastDevicePosture = posture;
+            updateMargins();
+        }
     }
 
     private void updateMargins() {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 5e84a57..35198de 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -164,13 +164,13 @@
 import com.android.systemui.keyguard.domain.interactor.FaceAuthenticationListener;
 import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor;
 import com.android.systemui.keyguard.shared.constants.TrustAgentUiEvent;
-import com.android.systemui.keyguard.shared.model.AcquiredAuthenticationStatus;
-import com.android.systemui.keyguard.shared.model.AuthenticationStatus;
-import com.android.systemui.keyguard.shared.model.DetectionStatus;
-import com.android.systemui.keyguard.shared.model.ErrorAuthenticationStatus;
-import com.android.systemui.keyguard.shared.model.FailedAuthenticationStatus;
-import com.android.systemui.keyguard.shared.model.HelpAuthenticationStatus;
-import com.android.systemui.keyguard.shared.model.SuccessAuthenticationStatus;
+import com.android.systemui.keyguard.shared.model.AcquiredFaceAuthenticationStatus;
+import com.android.systemui.keyguard.shared.model.ErrorFaceAuthenticationStatus;
+import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus;
+import com.android.systemui.keyguard.shared.model.FaceDetectionStatus;
+import com.android.systemui.keyguard.shared.model.FailedFaceAuthenticationStatus;
+import com.android.systemui.keyguard.shared.model.HelpFaceAuthenticationStatus;
+import com.android.systemui.keyguard.shared.model.SuccessFaceAuthenticationStatus;
 import com.android.systemui.keyguard.shared.model.SysUiFaceAuthenticateOptions;
 import com.android.systemui.log.SessionTracker;
 import com.android.systemui.plugins.WeatherData;
@@ -1471,27 +1471,31 @@
     private FaceAuthenticationListener mFaceAuthenticationListener =
             new FaceAuthenticationListener() {
                 @Override
-                public void onAuthenticationStatusChanged(@NonNull AuthenticationStatus status) {
-                    if (status instanceof AcquiredAuthenticationStatus) {
+                public void onAuthenticationStatusChanged(
+                        @NonNull FaceAuthenticationStatus status
+                ) {
+                    if (status instanceof AcquiredFaceAuthenticationStatus) {
                         handleFaceAcquired(
-                                ((AcquiredAuthenticationStatus) status).getAcquiredInfo());
-                    } else if (status instanceof ErrorAuthenticationStatus) {
-                        ErrorAuthenticationStatus error = (ErrorAuthenticationStatus) status;
+                                ((AcquiredFaceAuthenticationStatus) status).getAcquiredInfo());
+                    } else if (status instanceof ErrorFaceAuthenticationStatus) {
+                        ErrorFaceAuthenticationStatus error =
+                                (ErrorFaceAuthenticationStatus) status;
                         handleFaceError(error.getMsgId(), error.getMsg());
-                    } else if (status instanceof FailedAuthenticationStatus) {
+                    } else if (status instanceof FailedFaceAuthenticationStatus) {
                         handleFaceAuthFailed();
-                    } else if (status instanceof HelpAuthenticationStatus) {
-                        HelpAuthenticationStatus helpMsg = (HelpAuthenticationStatus) status;
+                    } else if (status instanceof HelpFaceAuthenticationStatus) {
+                        HelpFaceAuthenticationStatus helpMsg =
+                                (HelpFaceAuthenticationStatus) status;
                         handleFaceHelp(helpMsg.getMsgId(), helpMsg.getMsg());
-                    } else if (status instanceof SuccessAuthenticationStatus) {
+                    } else if (status instanceof SuccessFaceAuthenticationStatus) {
                         FaceManager.AuthenticationResult result =
-                                ((SuccessAuthenticationStatus) status).getSuccessResult();
+                                ((SuccessFaceAuthenticationStatus) status).getSuccessResult();
                         handleFaceAuthenticated(result.getUserId(), result.isStrongBiometric());
                     }
                 }
 
                 @Override
-                public void onDetectionStatusChanged(@NonNull DetectionStatus status) {
+                public void onDetectionStatusChanged(@NonNull FaceDetectionStatus status) {
                     handleFaceAuthenticated(status.getUserId(), status.isStrongBiometric());
                 }
             };
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
index 70b4371..a977966 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
@@ -64,14 +64,6 @@
     val isUnlocked: StateFlow<Boolean>
 
     /**
-     * Whether lock screen bypass is enabled. When enabled, the lock screen will be automatically
-     * dismisses once the authentication challenge is completed. For example, completing a biometric
-     * authentication challenge via face unlock or fingerprint sensor can automatically bypass the
-     * lock screen.
-     */
-    val isBypassEnabled: StateFlow<Boolean>
-
-    /**
      * Whether the auto confirm feature is enabled for the currently-selected user.
      *
      * Note that the length of the PIN is also important to take into consideration, please see
@@ -113,9 +105,6 @@
      */
     suspend fun isLockscreenEnabled(): Boolean
 
-    /** See [isBypassEnabled]. */
-    fun setBypassEnabled(isBypassEnabled: Boolean)
-
     /** Reports an authentication attempt. */
     suspend fun reportAuthenticationAttempt(isSuccessful: Boolean)
 
@@ -157,7 +146,7 @@
     private val lockPatternUtils: LockPatternUtils,
 ) : AuthenticationRepository {
 
-    override val isUnlocked: StateFlow<Boolean> = keyguardRepository.isKeyguardUnlocked
+    override val isUnlocked = keyguardRepository.isKeyguardUnlocked
 
     override suspend fun isLockscreenEnabled(): Boolean {
         return withContext(backgroundDispatcher) {
@@ -166,9 +155,6 @@
         }
     }
 
-    private val _isBypassEnabled = MutableStateFlow(false)
-    override val isBypassEnabled: StateFlow<Boolean> = _isBypassEnabled.asStateFlow()
-
     override val isAutoConfirmEnabled: StateFlow<Boolean> =
         userRepository.selectedUserInfo
             .map { it.id }
@@ -225,10 +211,6 @@
         }
     }
 
-    override fun setBypassEnabled(isBypassEnabled: Boolean) {
-        _isBypassEnabled.value = isBypassEnabled
-    }
-
     override suspend fun reportAuthenticationAttempt(isSuccessful: Boolean) {
         val selectedUserId = userRepository.selectedUserId
         withContext(backgroundDispatcher) {
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
index 3283e40..b482977 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
 import com.android.systemui.user.data.repository.UserRepository
 import com.android.systemui.util.time.SystemClock
 import javax.inject.Inject
@@ -51,6 +52,7 @@
     private val repository: AuthenticationRepository,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
     private val userRepository: UserRepository,
+    private val keyguardRepository: KeyguardRepository,
     private val clock: SystemClock,
 ) {
     /**
@@ -76,14 +78,6 @@
                 initialValue = true,
             )
 
-    /**
-     * Whether lock screen bypass is enabled. When enabled, the lock screen will be automatically
-     * dismisses once the authentication challenge is completed. For example, completing a biometric
-     * authentication challenge via face unlock or fingerprint sensor can automatically bypass the
-     * lock screen.
-     */
-    val isBypassEnabled: StateFlow<Boolean> = repository.isBypassEnabled
-
     /** The current authentication throttling state, only meaningful if [isThrottled] is `true`. */
     val throttling: StateFlow<AuthenticationThrottlingModel> = repository.throttling
 
@@ -156,6 +150,16 @@
     }
 
     /**
+     * Whether lock screen bypass is enabled. When enabled, the lock screen will be automatically
+     * dismisses once the authentication challenge is completed. For example, completing a biometric
+     * authentication challenge via face unlock or fingerprint sensor can automatically bypass the
+     * lock screen.
+     */
+    fun isBypassEnabled(): Boolean {
+        return keyguardRepository.isBypassEnabled()
+    }
+
+    /**
      * Attempts to authenticate the user and unlock the device.
      *
      * If [tryAutoConfirm] is `true`, authentication is attempted if and only if the auth method
@@ -218,11 +222,6 @@
         return authenticationResult.isSuccessful
     }
 
-    /** See [isBypassEnabled]. */
-    fun toggleBypassEnabled() {
-        repository.setBypassEnabled(!repository.isBypassEnabled.value)
-    }
-
     /** Starts refreshing the throttling state every second. */
     private suspend fun startThrottlingCountdown() {
         cancelCountdown()
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index 7a2f244..9df56fc 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -250,7 +250,7 @@
                 .setMessage(messageBody)
                 .setPositiveButton(android.R.string.ok, null)
                 .create();
-        alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
+        alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
         alertDialog.show();
     }
 
@@ -263,7 +263,7 @@
                 .setOnDismissListener(
                         dialog -> animateAway(AuthDialogCallback.DISMISSED_ERROR))
                 .create();
-        alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
+        alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
         alertDialog.show();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt
index 2abdb84..e3e9b3a 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt
@@ -16,10 +16,10 @@
 
 package com.android.systemui.bouncer.domain.interactor
 
+import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
-import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.time.SystemClock
@@ -35,8 +35,8 @@
     private val keyguardStateController: KeyguardStateController,
     private val bouncerRepository: KeyguardBouncerRepository,
     private val biometricSettingsRepository: BiometricSettingsRepository,
-    private val deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
     private val systemClock: SystemClock,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
 ) {
     var receivedDownTouch = false
     val isVisible: Flow<Boolean> = bouncerRepository.alternateBouncerVisible
@@ -78,7 +78,7 @@
             biometricSettingsRepository.isFingerprintEnrolled.value &&
             biometricSettingsRepository.isStrongBiometricAllowed.value &&
             biometricSettingsRepository.isFingerprintEnabledByDevicePolicy.value &&
-            !deviceEntryFingerprintAuthRepository.isLockedOut.value &&
+            !keyguardUpdateMonitor.isFingerprintLockedOut &&
             !keyguardStateController.isUnlocked &&
             !statusBarStateController.isDozing
     }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamLogger.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamLogger.kt
index 0e22406..f3a07fc 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamLogger.kt
@@ -16,15 +16,52 @@
 
 package com.android.systemui.dreams
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.core.LogLevel
-import com.android.systemui.log.dagger.DreamLog
-import javax.inject.Inject
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.core.MessageBuffer
 
 /** Logs dream-related stuff to a {@link LogBuffer}. */
-class DreamLogger @Inject constructor(@DreamLog private val buffer: LogBuffer) {
-    /** Logs a debug message to the buffer. */
-    fun d(tag: String, message: String) {
-        buffer.log(tag, LogLevel.DEBUG, { str1 = message }, { message })
-    }
+class DreamLogger(buffer: MessageBuffer, tag: String) : Logger(buffer, tag) {
+    fun logDreamOverlayEnabled(enabled: Boolean) =
+        d({ "Dream overlay enabled: $bool1" }) { bool1 = enabled }
+
+    fun logIgnoreAddComplication(reason: String, complication: String) =
+        d({ "Ignore adding complication, reason: $str1, complication: $str2" }) {
+            str1 = reason
+            str2 = complication
+        }
+
+    fun logIgnoreRemoveComplication(reason: String, complication: String) =
+        d({ "Ignore removing complication, reason: $str1, complication: $str2" }) {
+            str1 = reason
+            str2 = complication
+        }
+
+    fun logAddComplication(complication: String) =
+        d({ "Add dream complication: $str1" }) { str1 = complication }
+
+    fun logRemoveComplication(complication: String) =
+        d({ "Remove dream complication: $str1" }) { str1 = complication }
+
+    fun logOverlayActive(active: Boolean) = d({ "Dream overlay active: $bool1" }) { bool1 = active }
+
+    fun logLowLightActive(active: Boolean) =
+        d({ "Low light mode active: $bool1" }) { bool1 = active }
+
+    fun logHasAssistantAttention(hasAttention: Boolean) =
+        d({ "Dream overlay has Assistant attention: $bool1" }) { bool1 = hasAttention }
+
+    fun logStatusBarVisible(visible: Boolean) =
+        d({ "Dream overlay status bar visible: $bool1" }) { bool1 = visible }
+
+    fun logAvailableComplicationTypes(types: Int) =
+        d({ "Available complication types: $int1" }) { int1 = types }
+
+    fun logShouldShowComplications(showComplications: Boolean) =
+        d({ "Dream overlay should show complications: $bool1" }) { bool1 = showComplications }
+
+    fun logShowOrHideStatusBarItem(show: Boolean, type: String) =
+        d({ "${if (bool1) "Showing" else "Hiding"} dream status bar item: $int1" }) {
+            bool1 = show
+            str1 = type
+        }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
index 484bf3d..01fb522 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
@@ -36,6 +36,9 @@
 import com.android.systemui.dreams.dagger.DreamOverlayModule
 import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.dagger.DreamLog
 import com.android.systemui.statusbar.BlurUtils
 import com.android.systemui.statusbar.CrossFadeHelper
 import com.android.systemui.statusbar.policy.ConfigurationController
@@ -65,12 +68,14 @@
     private val mDreamInTranslationYDistance: Int,
     @Named(DreamOverlayModule.DREAM_IN_TRANSLATION_Y_DURATION)
     private val mDreamInTranslationYDurationMs: Long,
-    private val mLogger: DreamLogger,
+    @DreamLog logBuffer: LogBuffer,
 ) {
     companion object {
         private const val TAG = "DreamOverlayAnimationsController"
     }
 
+    private val logger = Logger(logBuffer, TAG)
+
     private var mAnimator: Animator? = null
     private lateinit var view: View
 
@@ -179,11 +184,11 @@
                 doOnEnd {
                     mAnimator = null
                     mOverlayStateController.setEntryAnimationsFinished(true)
-                    mLogger.d(TAG, "Dream overlay entry animations finished.")
+                    logger.d("Dream overlay entry animations finished.")
                 }
-                doOnCancel { mLogger.d(TAG, "Dream overlay entry animations canceled.") }
+                doOnCancel { logger.d("Dream overlay entry animations canceled.") }
                 start()
-                mLogger.d(TAG, "Dream overlay entry animations started.")
+                logger.d("Dream overlay entry animations started.")
             }
     }
 
@@ -242,11 +247,11 @@
                 doOnEnd {
                     mAnimator = null
                     mOverlayStateController.setExitAnimationsRunning(false)
-                    mLogger.d(TAG, "Dream overlay exit animations finished.")
+                    logger.d("Dream overlay exit animations finished.")
                 }
-                doOnCancel { mLogger.d(TAG, "Dream overlay exit animations canceled.") }
+                doOnCancel { logger.d("Dream overlay exit animations canceled.") }
                 start()
-                mLogger.d(TAG, "Dream overlay exit animations started.")
+                logger.d("Dream overlay exit animations started.")
             }
         mOverlayStateController.setExitAnimationsRunning(true)
         return mAnimator as AnimatorSet
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
index c2421dc..c9748f9 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
@@ -28,6 +28,8 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
+import com.android.systemui.log.LogBuffer;
+import com.android.systemui.log.dagger.DreamLog;
 import com.android.systemui.statusbar.policy.CallbackController;
 
 import java.util.ArrayList;
@@ -115,10 +117,10 @@
     public DreamOverlayStateController(@Main Executor executor,
             @Named(DREAM_OVERLAY_ENABLED) boolean overlayEnabled,
             FeatureFlags featureFlags,
-            DreamLogger dreamLogger) {
+            @DreamLog LogBuffer logBuffer) {
         mExecutor = executor;
         mOverlayEnabled = overlayEnabled;
-        mLogger = dreamLogger;
+        mLogger = new DreamLogger(logBuffer, TAG);
         mFeatureFlags = featureFlags;
         if (mFeatureFlags.isEnabled(Flags.ALWAYS_SHOW_HOME_CONTROLS_ON_DREAMS)) {
             mSupportedTypes = Complication.COMPLICATION_TYPE_NONE
@@ -126,7 +128,7 @@
         } else {
             mSupportedTypes = Complication.COMPLICATION_TYPE_NONE;
         }
-        mLogger.d(TAG, "Dream overlay enabled: " + mOverlayEnabled);
+        mLogger.logDreamOverlayEnabled(mOverlayEnabled);
     }
 
     /**
@@ -134,14 +136,13 @@
      */
     public void addComplication(Complication complication) {
         if (!mOverlayEnabled) {
-            mLogger.d(TAG,
-                    "Ignoring adding complication due to overlay disabled: " + complication);
+            mLogger.logIgnoreAddComplication("overlay disabled", complication.toString());
             return;
         }
 
         mExecutor.execute(() -> {
             if (mComplications.add(complication)) {
-                mLogger.d(TAG, "Added dream complication: " + complication);
+                mLogger.logAddComplication(complication.toString());
                 mCallbacks.stream().forEach(callback -> callback.onComplicationsChanged());
             }
         });
@@ -152,14 +153,13 @@
      */
     public void removeComplication(Complication complication) {
         if (!mOverlayEnabled) {
-            mLogger.d(TAG,
-                    "Ignoring removing complication due to overlay disabled: " + complication);
+            mLogger.logIgnoreRemoveComplication("overlay disabled", complication.toString());
             return;
         }
 
         mExecutor.execute(() -> {
             if (mComplications.remove(complication)) {
-                mLogger.d(TAG, "Removed dream complication: " + complication);
+                mLogger.logRemoveComplication(complication.toString());
                 mCallbacks.stream().forEach(callback -> callback.onComplicationsChanged());
             }
         });
@@ -305,7 +305,7 @@
      * @param active {@code true} if overlay is active, {@code false} otherwise.
      */
     public void setOverlayActive(boolean active) {
-        mLogger.d(TAG, "Dream overlay active: " + active);
+        mLogger.logOverlayActive(active);
         modifyState(active ? OP_SET_STATE : OP_CLEAR_STATE, STATE_DREAM_OVERLAY_ACTIVE);
     }
 
@@ -314,7 +314,7 @@
      * @param active {@code true} if low light mode is active, {@code false} otherwise.
      */
     public void setLowLightActive(boolean active) {
-        mLogger.d(TAG, "Low light mode active: " + active);
+        mLogger.logLowLightActive(active);
 
         if (isLowLightActive() && !active) {
             // Notify that we're exiting low light only on the transition from active to not active.
@@ -346,7 +346,7 @@
      * @param hasAttention {@code true} if has the user's attention, {@code false} otherwise.
      */
     public void setHasAssistantAttention(boolean hasAttention) {
-        mLogger.d(TAG, "Dream overlay has Assistant attention: " + hasAttention);
+        mLogger.logHasAssistantAttention(hasAttention);
         modifyState(hasAttention ? OP_SET_STATE : OP_CLEAR_STATE, STATE_HAS_ASSISTANT_ATTENTION);
     }
 
@@ -355,7 +355,7 @@
      * @param visible {@code true} if the status bar is visible, {@code false} otherwise.
      */
     public void setDreamOverlayStatusBarVisible(boolean visible) {
-        mLogger.d(TAG, "Dream overlay status bar visible: " + visible);
+        mLogger.logStatusBarVisible(visible);
         modifyState(
                 visible ? OP_SET_STATE : OP_CLEAR_STATE, STATE_DREAM_OVERLAY_STATUS_BAR_VISIBLE);
     }
@@ -373,7 +373,7 @@
      */
     public void setAvailableComplicationTypes(@Complication.ComplicationType int types) {
         mExecutor.execute(() -> {
-            mLogger.d(TAG, "Available complication types: " + types);
+            mLogger.logAvailableComplicationTypes(types);
             mAvailableComplicationTypes = types;
             mCallbacks.forEach(Callback::onAvailableComplicationTypesChanged);
         });
@@ -391,7 +391,7 @@
      */
     public void setShouldShowComplications(boolean shouldShowComplications) {
         mExecutor.execute(() -> {
-            mLogger.d(TAG, "Should show complications: " + shouldShowComplications);
+            mLogger.logShouldShowComplications(shouldShowComplications);
             mShouldShowComplications = shouldShowComplications;
             mCallbacks.forEach(Callback::onAvailableComplicationTypesChanged);
         });
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
index 3a28408..a6401b6 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
@@ -36,6 +36,8 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dreams.DreamOverlayStatusBarItemsProvider.StatusBarItem;
 import com.android.systemui.dreams.dagger.DreamOverlayComponent;
+import com.android.systemui.log.LogBuffer;
+import com.android.systemui.log.dagger.DreamLog;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.CrossFadeHelper;
 import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController;
@@ -161,7 +163,7 @@
             DreamOverlayStatusBarItemsProvider statusBarItemsProvider,
             DreamOverlayStateController dreamOverlayStateController,
             UserTracker userTracker,
-            DreamLogger dreamLogger) {
+            @DreamLog LogBuffer logBuffer) {
         super(view);
         mResources = resources;
         mMainExecutor = mainExecutor;
@@ -177,7 +179,7 @@
         mZenModeController = zenModeController;
         mDreamOverlayStateController = dreamOverlayStateController;
         mUserTracker = userTracker;
-        mLogger = dreamLogger;
+        mLogger = new DreamLogger(logBuffer, TAG);
 
         // Register to receive show/hide updates for the system status bar. Our custom status bar
         // needs to hide when the system status bar is showing to ovoid overlapping status bars.
@@ -346,8 +348,8 @@
             @Nullable String contentDescription) {
         mMainExecutor.execute(() -> {
             if (mIsAttached) {
-                mLogger.d(TAG, (show ? "Showing" : "Hiding") + " dream status bar item: "
-                        + DreamOverlayStatusBarView.getLoggableStatusIconType(iconType));
+                mLogger.logShowOrHideStatusBarItem(
+                        show, DreamOverlayStatusBarView.getLoggableStatusIconType(iconType));
                 mView.showIcon(iconType, show, contentDescription);
             }
         });
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index add32398..0670ec3 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -438,10 +438,6 @@
     // TODO(b/266157412): Tracking Bug
     val MEDIA_RETAIN_SESSIONS = unreleasedFlag(913, "media_retain_sessions")
 
-    // TODO(b/266739309): Tracking Bug
-    @JvmField
-    val MEDIA_RECOMMENDATION_CARD_UPDATE = releasedFlag(914, "media_recommendation_card_update")
-
     // TODO(b/267007629): Tracking Bug
     val MEDIA_RESUME_PROGRESS = releasedFlag(915, "media_resume_progress")
 
diff --git a/packages/SystemUI/src/com/android/systemui/flags/ViewRefactorFlag.kt b/packages/SystemUI/src/com/android/systemui/flags/ViewRefactorFlag.kt
new file mode 100644
index 0000000..eaecda5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/flags/ViewRefactorFlag.kt
@@ -0,0 +1,99 @@
+/*
+ * 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.flags
+
+import android.util.Log
+import com.android.systemui.Dependency
+
+/**
+ * This class promotes best practices for flag guarding System UI view refactors.
+ * * [isEnabled] allows changing an implementation.
+ * * [assertDisabled] allows authors to flag code as being "dead" when the flag gets enabled and
+ *   ensure that it is not being invoked accidentally in the post-flag refactor.
+ * * [expectEnabled] allows authors to guard new code with a "safe" alternative when invoked on
+ *   flag-disabled builds, but with a check that should crash eng builds or tests when the
+ *   expectation is violated.
+ *
+ * The constructors prefer that you provide a [FeatureFlags] instance, but does not require it,
+ * falling back to [Dependency.get]. This fallback should ONLY be used to flag-guard code changes
+ * inside views where injecting flag values after initialization can be error-prone.
+ */
+class ViewRefactorFlag
+private constructor(
+    private val injectedFlags: FeatureFlags?,
+    private val flag: BooleanFlag,
+    private val readFlagValue: (FeatureFlags) -> Boolean
+) {
+    @JvmOverloads
+    constructor(
+        flags: FeatureFlags? = null,
+        flag: UnreleasedFlag
+    ) : this(flags, flag, { it.isEnabled(flag) })
+
+    @JvmOverloads
+    constructor(
+        flags: FeatureFlags? = null,
+        flag: ReleasedFlag
+    ) : this(flags, flag, { it.isEnabled(flag) })
+
+    /** Whether the flag is enabled. Called to switch between an old behavior and a new behavior. */
+    val isEnabled by lazy {
+        @Suppress("DEPRECATION")
+        val featureFlags = injectedFlags ?: Dependency.get(FeatureFlags::class.java)
+        readFlagValue(featureFlags)
+    }
+
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is enabled to ensure that the refactor author catches issues in testing.
+     *
+     * Example usage:
+     * ```
+     * public void setController(NotificationShelfController notificationShelfController) {
+     *     mShelfRefactor.assertDisabled();
+     *     mController = notificationShelfController;
+     * }
+     * ````
+     */
+    fun assertDisabled() = check(!isEnabled) { "Code path not supported when $flag is enabled." }
+
+    /**
+     * Called to ensure code is only run when the flag is enabled. This protects users from the
+     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+     * build to ensure that the refactor author catches issues in testing.
+     *
+     * Example usage:
+     * ```
+     * public void setShelfIcons(NotificationIconContainer icons) {
+     *     if (mShelfRefactor.expectEnabled()) {
+     *         mShelfIcons = icons;
+     *     }
+     * }
+     * ```
+     */
+    fun expectEnabled(): Boolean {
+        if (!isEnabled) {
+            val message = "Code path not supported when $flag is disabled."
+            Log.wtf(TAG, message, Exception(message))
+        }
+        return isEnabled
+    }
+
+    private companion object {
+        private const val TAG = "ViewRefactorFlag"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index f59ad90..23f6fa6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -31,6 +31,9 @@
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
 import com.android.systemui.shade.NotificationShadeWindowView
 import com.android.systemui.statusbar.KeyguardIndicationController
+import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
+import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
 import javax.inject.Inject
 import kotlinx.coroutines.DisposableHandle
 
@@ -40,7 +43,9 @@
 @Inject
 constructor(
     private val keyguardRootView: KeyguardRootView,
+    private val sharedNotificationContainer: SharedNotificationContainer,
     private val keyguardIndicationAreaViewModel: KeyguardIndicationAreaViewModel,
+    private val sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
     private val notificationShadeWindowView: NotificationShadeWindowView,
     private val featureFlags: FeatureFlags,
     private val indicationController: KeyguardIndicationController,
@@ -55,10 +60,28 @@
             notificationShadeWindowView.requireViewById(R.id.notification_panel) as ViewGroup
         bindIndicationArea(notificationPanel)
         bindLockIconView(notificationPanel)
+        setupNotificationStackScrollLayout(notificationPanel)
+
         keyguardLayoutManager.layoutViews()
         keyguardLayoutManagerCommandListener.start()
     }
 
+    fun setupNotificationStackScrollLayout(legacyParent: ViewGroup) {
+        if (featureFlags.isEnabled(Flags.MIGRATE_NSSL)) {
+            // This moves the existing NSSL view to a different parent, as the controller is a
+            // singleton and recreating it has other bad side effects
+            val nssl =
+                legacyParent.requireViewById<View>(R.id.notification_stack_scroller).also {
+                    (it.getParent() as ViewGroup).removeView(it)
+                }
+            sharedNotificationContainer.addNotificationStackScrollLayout(nssl)
+            SharedNotificationContainerBinder.bind(
+                sharedNotificationContainer,
+                sharedNotificationContainerViewModel
+            )
+        }
+    }
+
     fun bindIndicationArea(legacyParent: ViewGroup) {
         indicationAreaHandle?.dispose()
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 9a32e94..e0834bb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -163,9 +163,11 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
 import com.android.systemui.util.DeviceConfigProxy;
+import com.android.systemui.util.kotlin.JavaAdapter;
 import com.android.systemui.util.settings.SecureSettings;
 import com.android.systemui.util.settings.SystemSettings;
 import com.android.systemui.util.time.SystemClock;
+import com.android.systemui.wallpapers.data.repository.WallpaperRepository;
 import com.android.wm.shell.keyguard.KeyguardTransitions;
 
 import dagger.Lazy;
@@ -290,6 +292,8 @@
     public static final String SYS_BOOT_REASON_PROP = "sys.boot.reason.last";
     public static final String REBOOT_MAINLINE_UPDATE = "reboot,mainline_update";
     private final DreamOverlayStateController mDreamOverlayStateController;
+    private final JavaAdapter mJavaAdapter;
+    private final WallpaperRepository mWallpaperRepository;
 
     /** The stream type that the lock sounds are tied to. */
     private int mUiSoundsStreamType;
@@ -1323,6 +1327,8 @@
             KeyguardTransitions keyguardTransitions,
             InteractionJankMonitor interactionJankMonitor,
             DreamOverlayStateController dreamOverlayStateController,
+            JavaAdapter javaAdapter,
+            WallpaperRepository wallpaperRepository,
             Lazy<ShadeController> shadeControllerLazy,
             Lazy<NotificationShadeWindowController> notificationShadeWindowControllerLazy,
             Lazy<ActivityLaunchAnimator> activityLaunchAnimator,
@@ -1384,6 +1390,8 @@
         mScreenOffAnimationController = screenOffAnimationController;
         mInteractionJankMonitor = interactionJankMonitor;
         mDreamOverlayStateController = dreamOverlayStateController;
+        mJavaAdapter = javaAdapter;
+        mWallpaperRepository = wallpaperRepository;
 
         mActivityLaunchAnimator = activityLaunchAnimator;
         mScrimControllerLazy = scrimControllerLazy;
@@ -1487,6 +1495,10 @@
                 com.android.internal.R.anim.lock_screen_behind_enter);
 
         mWorkLockController = new WorkLockActivityController(mContext, mUserTracker);
+
+        mJavaAdapter.alwaysCollectFlow(
+                mWallpaperRepository.getWallpaperSupportsAmbientMode(),
+                this::setWallpaperSupportsAmbientMode);
     }
 
     // TODO(b/273443374) remove, temporary util to get a feature flag
@@ -2970,6 +2982,7 @@
     }
 
     private void onKeyguardExitFinished() {
+        if (DEBUG) Log.d(TAG, "onKeyguardExitFinished()");
         // only play "unlock" noises if not on a call (since the incall UI
         // disables the keyguard)
         if (TelephonyManager.EXTRA_STATE_IDLE.equals(mPhoneState)) {
@@ -3191,13 +3204,14 @@
                 flags |= StatusBarManager.DISABLE_RECENT;
             }
 
-            if (mPowerGestureIntercepted) {
+            if (mPowerGestureIntercepted && mOccluded && isSecure()) {
                 flags |= StatusBarManager.DISABLE_RECENT;
             }
 
             if (DEBUG) {
                 Log.d(TAG, "adjustStatusBarLocked: mShowing=" + mShowing + " mOccluded=" + mOccluded
                         + " isSecure=" + isSecure() + " force=" + forceHideHomeRecentsButtons
+                        + " mPowerGestureIntercepted=" + mPowerGestureIntercepted
                         +  " --> flags=0x" + Integer.toHexString(flags));
             }
 
@@ -3425,6 +3439,7 @@
         pw.print("  mPendingLock: "); pw.println(mPendingLock);
         pw.print("  wakeAndUnlocking: "); pw.println(mWakeAndUnlocking);
         pw.print("  mPendingPinLock: "); pw.println(mPendingPinLock);
+        pw.print("  mPowerGestureIntercepted: "); pw.println(mPowerGestureIntercepted);
     }
 
     /**
@@ -3464,7 +3479,7 @@
      * In case it does support it, we have to fade in the incoming app, otherwise we'll reveal it
      * with the light reveal scrim.
      */
-    public void setWallpaperSupportsAmbientMode(boolean supportsAmbientMode) {
+    private void setWallpaperSupportsAmbientMode(boolean supportsAmbientMode) {
         mWallpaperSupportsAmbientMode = supportsAmbientMode;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index 1487408..a5ac7c7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -67,9 +67,11 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
 import com.android.systemui.util.DeviceConfigProxy;
+import com.android.systemui.util.kotlin.JavaAdapter;
 import com.android.systemui.util.settings.SecureSettings;
 import com.android.systemui.util.settings.SystemSettings;
 import com.android.systemui.util.time.SystemClock;
+import com.android.systemui.wallpapers.data.repository.WallpaperRepository;
 import com.android.wm.shell.keyguard.KeyguardTransitions;
 
 import dagger.Lazy;
@@ -131,6 +133,8 @@
             KeyguardTransitions keyguardTransitions,
             InteractionJankMonitor interactionJankMonitor,
             DreamOverlayStateController dreamOverlayStateController,
+            JavaAdapter javaAdapter,
+            WallpaperRepository wallpaperRepository,
             Lazy<ShadeController> shadeController,
             Lazy<NotificationShadeWindowController> notificationShadeWindowController,
             Lazy<ActivityLaunchAnimator> activityLaunchAnimator,
@@ -172,6 +176,8 @@
                 keyguardTransitions,
                 interactionJankMonitor,
                 dreamOverlayStateController,
+                javaAdapter,
+                wallpaperRepository,
                 shadeController,
                 notificationShadeWindowController,
                 activityLaunchAnimator,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
index 0b6c7c4..ff3e77c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
@@ -325,6 +325,9 @@
 private class StrongAuthTracker(private val userRepository: UserRepository, context: Context?) :
     LockPatternUtils.StrongAuthTracker(context) {
 
+    private val selectedUserId =
+        userRepository.selectedUserInfo.map { it.id }.distinctUntilChanged()
+
     // Backing field for onStrongAuthRequiredChanged
     private val _authFlags =
         MutableStateFlow(AuthenticationFlags(currentUserId, getStrongAuthForUser(currentUserId)))
@@ -336,15 +339,12 @@
         )
 
     val currentUserAuthFlags: Flow<AuthenticationFlags> =
-        userRepository.selectedUserInfo
-            .map { it.id }
-            .distinctUntilChanged()
-            .flatMapLatest { userId ->
-                _authFlags
-                    .map { AuthenticationFlags(userId, getStrongAuthForUser(userId)) }
-                    .onEach { Log.d(TAG, "currentUser authFlags changed, new value: $it") }
-                    .onStart { emit(AuthenticationFlags(userId, getStrongAuthForUser(userId))) }
-            }
+        selectedUserId.flatMapLatest { userId ->
+            _authFlags
+                .map { AuthenticationFlags(userId, getStrongAuthForUser(userId)) }
+                .onEach { Log.d(TAG, "currentUser authFlags changed, new value: $it") }
+                .onStart { emit(AuthenticationFlags(userId, getStrongAuthForUser(userId))) }
+        }
 
     /** isStrongBiometricAllowed for the current user. */
     val isStrongBiometricAllowed: Flow<Boolean> =
@@ -352,16 +352,17 @@
 
     /** isNonStrongBiometricAllowed for the current user. */
     val isNonStrongBiometricAllowed: Flow<Boolean> =
-        userRepository.selectedUserInfo
-            .map { it.id }
-            .distinctUntilChanged()
+        selectedUserId
             .flatMapLatest { userId ->
                 _nonStrongBiometricAllowed
                     .filter { it.first == userId }
                     .map { it.second }
-                    .onEach { Log.d(TAG, "isNonStrongBiometricAllowed changed for current user") }
+                    .onEach {
+                        Log.d(TAG, "isNonStrongBiometricAllowed changed for current user: $it")
+                    }
                     .onStart { emit(isNonStrongBiometricAllowedAfterIdleTimeout(userId)) }
             }
+            .and(isStrongBiometricAllowed)
 
     private val currentUserId
         get() = userRepository.getSelectedUserInfo().id
@@ -387,3 +388,6 @@
 
 private fun DevicePolicyManager.isNotActive(userId: Int, policy: Int): Boolean =
     (getKeyguardDisabledFeatures(null, userId) and policy) == 0
+
+private fun Flow<Boolean>.and(anotherFlow: Flow<Boolean>): Flow<Boolean> =
+    this.combine(anotherFlow) { a, b -> a && b }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
index a3d1abe..6edf40f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
@@ -38,13 +38,13 @@
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AcquiredAuthenticationStatus
-import com.android.systemui.keyguard.shared.model.AuthenticationStatus
-import com.android.systemui.keyguard.shared.model.DetectionStatus
-import com.android.systemui.keyguard.shared.model.ErrorAuthenticationStatus
-import com.android.systemui.keyguard.shared.model.FailedAuthenticationStatus
-import com.android.systemui.keyguard.shared.model.HelpAuthenticationStatus
-import com.android.systemui.keyguard.shared.model.SuccessAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.AcquiredFaceAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.ErrorFaceAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FaceDetectionStatus
+import com.android.systemui.keyguard.shared.model.FailedFaceAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.HelpFaceAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.SuccessFaceAuthenticationStatus
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.log.FaceAuthenticationLogger
 import com.android.systemui.log.SessionTracker
@@ -88,10 +88,10 @@
     val canRunFaceAuth: StateFlow<Boolean>
 
     /** Provide the current status of face authentication. */
-    val authenticationStatus: Flow<AuthenticationStatus>
+    val authenticationStatus: Flow<FaceAuthenticationStatus>
 
     /** Provide the current status of face detection. */
-    val detectionStatus: Flow<DetectionStatus>
+    val detectionStatus: Flow<FaceDetectionStatus>
 
     /** Current state of whether face authentication is locked out or not. */
     val isLockedOut: StateFlow<Boolean>
@@ -151,13 +151,13 @@
     private var cancelNotReceivedHandlerJob: Job? = null
     private var halErrorRetryJob: Job? = null
 
-    private val _authenticationStatus: MutableStateFlow<AuthenticationStatus?> =
+    private val _authenticationStatus: MutableStateFlow<FaceAuthenticationStatus?> =
         MutableStateFlow(null)
-    override val authenticationStatus: Flow<AuthenticationStatus>
+    override val authenticationStatus: Flow<FaceAuthenticationStatus>
         get() = _authenticationStatus.filterNotNull()
 
-    private val _detectionStatus = MutableStateFlow<DetectionStatus?>(null)
-    override val detectionStatus: Flow<DetectionStatus>
+    private val _detectionStatus = MutableStateFlow<FaceDetectionStatus?>(null)
+    override val detectionStatus: Flow<FaceDetectionStatus>
         get() = _detectionStatus.filterNotNull()
 
     private val _isLockedOut = MutableStateFlow(false)
@@ -396,18 +396,18 @@
     private val faceAuthCallback =
         object : FaceManager.AuthenticationCallback() {
             override fun onAuthenticationFailed() {
-                _authenticationStatus.value = FailedAuthenticationStatus
+                _authenticationStatus.value = FailedFaceAuthenticationStatus
                 _isAuthenticated.value = false
                 faceAuthLogger.authenticationFailed()
                 onFaceAuthRequestCompleted()
             }
 
             override fun onAuthenticationAcquired(acquireInfo: Int) {
-                _authenticationStatus.value = AcquiredAuthenticationStatus(acquireInfo)
+                _authenticationStatus.value = AcquiredFaceAuthenticationStatus(acquireInfo)
             }
 
             override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
-                val errorStatus = ErrorAuthenticationStatus(errorCode, errString.toString())
+                val errorStatus = ErrorFaceAuthenticationStatus(errorCode, errString.toString())
                 if (errorStatus.isLockoutError()) {
                     _isLockedOut.value = true
                 }
@@ -433,11 +433,11 @@
                 if (faceAcquiredInfoIgnoreList.contains(code)) {
                     return
                 }
-                _authenticationStatus.value = HelpAuthenticationStatus(code, helpStr.toString())
+                _authenticationStatus.value = HelpFaceAuthenticationStatus(code, helpStr.toString())
             }
 
             override fun onAuthenticationSucceeded(result: FaceManager.AuthenticationResult) {
-                _authenticationStatus.value = SuccessAuthenticationStatus(result)
+                _authenticationStatus.value = SuccessFaceAuthenticationStatus(result)
                 _isAuthenticated.value = true
                 faceAuthLogger.faceAuthSuccess(result)
                 onFaceAuthRequestCompleted()
@@ -482,7 +482,7 @@
     private val detectionCallback =
         FaceManager.FaceDetectionCallback { sensorId, userId, isStrong ->
             faceAuthLogger.faceDetected()
-            _detectionStatus.value = DetectionStatus(sensorId, userId, isStrong)
+            _detectionStatus.value = FaceDetectionStatus(sensorId, userId, isStrong)
         }
 
     private var cancellationInProgress = false
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt
index 52234b3..9bec300 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt
@@ -21,27 +21,29 @@
 import android.hardware.biometrics.BiometricSourceType
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitorCallback
-import com.android.systemui.Dumpable
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dump.DumpManager
-import java.io.PrintWriter
+import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.stateIn
 
 /** Encapsulates state about device entry fingerprint auth mechanism. */
 interface DeviceEntryFingerprintAuthRepository {
     /** Whether the device entry fingerprint auth is locked out. */
-    val isLockedOut: StateFlow<Boolean>
+    val isLockedOut: Flow<Boolean>
 
     /**
      * Whether the fingerprint sensor is currently listening, this doesn't mean that the user is
@@ -53,6 +55,9 @@
      * Fingerprint sensor type present on the device, null if fingerprint sensor is not available.
      */
     val availableFpSensorType: Flow<BiometricType?>
+
+    /** Provide the current status of fingerprint authentication. */
+    val authenticationStatus: Flow<FingerprintAuthenticationStatus>
 }
 
 /**
@@ -69,16 +74,7 @@
     val authController: AuthController,
     val keyguardUpdateMonitor: KeyguardUpdateMonitor,
     @Application scope: CoroutineScope,
-    dumpManager: DumpManager,
-) : DeviceEntryFingerprintAuthRepository, Dumpable {
-
-    init {
-        dumpManager.registerDumpable(this)
-    }
-
-    override fun dump(pw: PrintWriter, args: Array<String?>) {
-        pw.println("isLockedOut=${isLockedOut.value}")
-    }
+) : DeviceEntryFingerprintAuthRepository {
 
     override val availableFpSensorType: Flow<BiometricType?>
         get() {
@@ -114,7 +110,7 @@
         else if (authController.isRearFpsSupported) BiometricType.REAR_FINGERPRINT else null
     }
 
-    override val isLockedOut: StateFlow<Boolean> =
+    override val isLockedOut: Flow<Boolean> =
         conflatedCallbackFlow {
                 val sendLockoutUpdate =
                     fun() {
@@ -138,7 +134,7 @@
                 sendLockoutUpdate()
                 awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
             }
-            .stateIn(scope, started = SharingStarted.Eagerly, initialValue = false)
+            .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
 
     override val isRunning: Flow<Boolean>
         get() = conflatedCallbackFlow {
@@ -166,6 +162,93 @@
             awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
         }
 
+    override val authenticationStatus: Flow<FingerprintAuthenticationStatus>
+        get() = conflatedCallbackFlow {
+            val callback =
+                object : KeyguardUpdateMonitorCallback() {
+                    override fun onBiometricAuthenticated(
+                        userId: Int,
+                        biometricSourceType: BiometricSourceType,
+                        isStrongBiometric: Boolean,
+                    ) {
+
+                        sendUpdateIfFingerprint(
+                            biometricSourceType,
+                            SuccessFingerprintAuthenticationStatus(
+                                userId,
+                                isStrongBiometric,
+                            ),
+                        )
+                    }
+
+                    override fun onBiometricError(
+                        msgId: Int,
+                        errString: String?,
+                        biometricSourceType: BiometricSourceType,
+                    ) {
+                        sendUpdateIfFingerprint(
+                            biometricSourceType,
+                            ErrorFingerprintAuthenticationStatus(
+                                msgId,
+                                errString,
+                            ),
+                        )
+                    }
+
+                    override fun onBiometricHelp(
+                        msgId: Int,
+                        helpString: String?,
+                        biometricSourceType: BiometricSourceType,
+                    ) {
+                        sendUpdateIfFingerprint(
+                            biometricSourceType,
+                            HelpFingerprintAuthenticationStatus(
+                                msgId,
+                                helpString,
+                            ),
+                        )
+                    }
+
+                    override fun onBiometricAuthFailed(
+                        biometricSourceType: BiometricSourceType,
+                    ) {
+                        sendUpdateIfFingerprint(
+                            biometricSourceType,
+                            FailFingerprintAuthenticationStatus,
+                        )
+                    }
+
+                    override fun onBiometricAcquired(
+                        biometricSourceType: BiometricSourceType,
+                        acquireInfo: Int,
+                    ) {
+                        sendUpdateIfFingerprint(
+                            biometricSourceType,
+                            AcquiredFingerprintAuthenticationStatus(
+                                acquireInfo,
+                            ),
+                        )
+                    }
+
+                    private fun sendUpdateIfFingerprint(
+                        biometricSourceType: BiometricSourceType,
+                        authenticationStatus: FingerprintAuthenticationStatus
+                    ) {
+                        if (biometricSourceType != BiometricSourceType.FINGERPRINT) {
+                            return
+                        }
+
+                        trySendWithFailureLogging(
+                            authenticationStatus,
+                            TAG,
+                            "new fingerprint authentication status"
+                        )
+                    }
+                }
+            keyguardUpdateMonitor.registerCallback(callback)
+            awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
+        }
+
     companion object {
         const val TAG = "DeviceEntryFingerprintAuthRepositoryImpl"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index d119920..7475c42 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -31,26 +31,31 @@
 import com.android.systemui.doze.DozeTransitionCallback
 import com.android.systemui.doze.DozeTransitionListener
 import com.android.systemui.dreams.DreamOverlayCallbackController
+import com.android.systemui.keyguard.ScreenLifecycle
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
 import com.android.systemui.keyguard.shared.model.DozeStateModel
 import com.android.systemui.keyguard.shared.model.DozeTransitionModel
+import com.android.systemui.keyguard.shared.model.ScreenModel
 import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.keyguard.shared.model.WakefulnessModel
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.phone.BiometricUnlockController
 import com.android.systemui.statusbar.phone.BiometricUnlockController.WakeAndUnlockMode
 import com.android.systemui.statusbar.phone.DozeParameters
+import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flowOn
@@ -147,6 +152,9 @@
     /** Observable for device wake/sleep state */
     val wakefulness: StateFlow<WakefulnessModel>
 
+    /** Observable for device screen state */
+    val screenModel: StateFlow<ScreenModel>
+
     /** Observable for biometric unlock modes */
     val biometricUnlockState: Flow<BiometricUnlockModel>
 
@@ -162,6 +170,9 @@
     /** Whether quick settings or quick-quick settings is visible. */
     val isQuickSettingsVisible: Flow<Boolean>
 
+    /** Receive an event for doze time tick */
+    val dozeTimeTick: Flow<Unit>
+
     /**
      * Returns `true` if the keyguard is showing; `false` otherwise.
      *
@@ -171,6 +182,14 @@
      */
     fun isKeyguardShowing(): Boolean
 
+    /**
+     * Whether lock screen bypass is enabled. When enabled, the lock screen will be automatically
+     * dismissed once the authentication challenge is completed. For example, completing a biometric
+     * authentication challenge via face unlock or fingerprint sensor can automatically bypass the
+     * lock screen.
+     */
+    fun isBypassEnabled(): Boolean
+
     /** Sets whether the bottom area UI should animate the transition out of doze state. */
     fun setAnimateDozingTransitions(animate: Boolean)
 
@@ -195,6 +214,8 @@
     fun setIsDozing(isDozing: Boolean)
 
     fun setIsActiveDreamLockscreenHosted(isLockscreenHosted: Boolean)
+
+    fun dozeTimeTick()
 }
 
 /** Encapsulates application state for the keyguard. */
@@ -204,8 +225,10 @@
 constructor(
     statusBarStateController: StatusBarStateController,
     wakefulnessLifecycle: WakefulnessLifecycle,
+    screenLifecycle: ScreenLifecycle,
     biometricUnlockController: BiometricUnlockController,
     private val keyguardStateController: KeyguardStateController,
+    private val keyguardBypassController: KeyguardBypassController,
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
     private val dozeTransitionListener: DozeTransitionListener,
     private val dozeParameters: DozeParameters,
@@ -252,23 +275,17 @@
     override val isAodAvailable: Flow<Boolean> =
         conflatedCallbackFlow {
                 val callback =
-                    object : DozeParameters.Callback {
-                        override fun onAlwaysOnChange() {
-                            trySendWithFailureLogging(
-                                dozeParameters.getAlwaysOn(),
-                                TAG,
-                                "updated isAodAvailable"
-                            )
-                        }
+                    DozeParameters.Callback {
+                        trySendWithFailureLogging(
+                            dozeParameters.alwaysOn,
+                            TAG,
+                            "updated isAodAvailable"
+                        )
                     }
 
                 dozeParameters.addCallback(callback)
                 // Adding the callback does not send an initial update.
-                trySendWithFailureLogging(
-                    dozeParameters.getAlwaysOn(),
-                    TAG,
-                    "initial isAodAvailable"
-                )
+                trySendWithFailureLogging(dozeParameters.alwaysOn, TAG, "initial isAodAvailable")
 
                 awaitClose { dozeParameters.removeCallback(callback) }
             }
@@ -366,6 +383,13 @@
         _isDozing.value = isDozing
     }
 
+    private val _dozeTimeTick = MutableSharedFlow<Unit>()
+    override val dozeTimeTick = _dozeTimeTick.asSharedFlow()
+
+    override fun dozeTimeTick() {
+        _dozeTimeTick.tryEmit(Unit)
+    }
+
     private val _lastDozeTapToWakePosition = MutableStateFlow<Point?>(null)
     override val lastDozeTapToWakePosition = _lastDozeTapToWakePosition.asStateFlow()
 
@@ -464,6 +488,10 @@
         return keyguardStateController.isShowing
     }
 
+    override fun isBypassEnabled(): Boolean {
+        return keyguardBypassController.bypassEnabled
+    }
+
     override val statusBarState: Flow<StatusBarState> = conflatedCallbackFlow {
         val callback =
             object : StatusBarStateController.StateListener {
@@ -551,6 +579,42 @@
                 initialValue = WakefulnessModel.fromWakefulnessLifecycle(wakefulnessLifecycle),
             )
 
+    override val screenModel: StateFlow<ScreenModel> =
+        conflatedCallbackFlow {
+                val observer =
+                    object : ScreenLifecycle.Observer {
+                        override fun onScreenTurningOn() {
+                            dispatchNewState()
+                        }
+                        override fun onScreenTurnedOn() {
+                            dispatchNewState()
+                        }
+                        override fun onScreenTurningOff() {
+                            dispatchNewState()
+                        }
+                        override fun onScreenTurnedOff() {
+                            dispatchNewState()
+                        }
+
+                        private fun dispatchNewState() {
+                            trySendWithFailureLogging(
+                                ScreenModel.fromScreenLifecycle(screenLifecycle),
+                                TAG,
+                                "updated screen state",
+                            )
+                        }
+                    }
+
+                screenLifecycle.addObserver(observer)
+                awaitClose { screenLifecycle.removeObserver(observer) }
+            }
+            .stateIn(
+                scope,
+                // Use Eagerly so that we're always listening and never miss an event.
+                SharingStarted.Eagerly,
+                initialValue = ScreenModel.fromScreenLifecycle(screenLifecycle),
+            )
+
     override val fingerprintSensorLocation: Flow<Point?> = conflatedCallbackFlow {
         fun sendFpLocation() {
             trySendWithFailureLogging(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt
index abe59b7..27e3a74 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt
@@ -18,8 +18,8 @@
 
 import com.android.keyguard.FaceAuthUiEvent
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.shared.model.AuthenticationStatus
-import com.android.systemui.keyguard.shared.model.DetectionStatus
+import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FaceDetectionStatus
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -40,10 +40,10 @@
     private val _canRunFaceAuth = MutableStateFlow(false)
     override val canRunFaceAuth: StateFlow<Boolean> = _canRunFaceAuth
 
-    override val authenticationStatus: Flow<AuthenticationStatus>
+    override val authenticationStatus: Flow<FaceAuthenticationStatus>
         get() = emptyFlow()
 
-    override val detectionStatus: Flow<DetectionStatus>
+    override val detectionStatus: Flow<FaceDetectionStatus>
         get() = emptyFlow()
 
     private val _isLockedOut = MutableStateFlow(false)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractor.kt
new file mode 100644
index 0000000..c849b84
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractor.kt
@@ -0,0 +1,138 @@
+/*
+ *  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.keyguard.domain.interactor
+
+import android.content.res.Resources
+import android.hardware.biometrics.BiometricSourceType
+import android.hardware.biometrics.BiometricSourceType.FINGERPRINT
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED
+import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository
+import com.android.systemui.biometrics.shared.model.FingerprintSensorType
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.util.IndicationHelper
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.filterNot
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+
+/**
+ * BiometricMessage business logic. Filters biometric error/acquired/fail/success events for
+ * authentication events that should never surface a message to the user at the current device
+ * state.
+ */
+@ExperimentalCoroutinesApi
+@SysUISingleton
+class BiometricMessageInteractor
+@Inject
+constructor(
+    @Main private val resources: Resources,
+    private val fingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
+    private val fingerprintPropertyRepository: FingerprintPropertyRepository,
+    private val indicationHelper: IndicationHelper,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+) {
+    val fingerprintErrorMessage: Flow<BiometricMessage> =
+        fingerprintAuthRepository.authenticationStatus
+            .filter {
+                it is ErrorFingerprintAuthenticationStatus &&
+                    !indicationHelper.shouldSuppressErrorMsg(FINGERPRINT, it.msgId)
+            }
+            .map {
+                val errorStatus = it as ErrorFingerprintAuthenticationStatus
+                BiometricMessage(
+                    FINGERPRINT,
+                    BiometricMessageType.ERROR,
+                    errorStatus.msgId,
+                    errorStatus.msg,
+                )
+            }
+
+    val fingerprintHelpMessage: Flow<BiometricMessage> =
+        fingerprintAuthRepository.authenticationStatus
+            .filter { it is HelpFingerprintAuthenticationStatus }
+            .filterNot { isPrimaryAuthRequired() }
+            .map {
+                val helpStatus = it as HelpFingerprintAuthenticationStatus
+                BiometricMessage(
+                    FINGERPRINT,
+                    BiometricMessageType.HELP,
+                    helpStatus.msgId,
+                    helpStatus.msg,
+                )
+            }
+
+    val fingerprintFailMessage: Flow<BiometricMessage> =
+        isUdfps().flatMapLatest { isUdfps ->
+            fingerprintAuthRepository.authenticationStatus
+                .filter { it is FailFingerprintAuthenticationStatus }
+                .filterNot { isPrimaryAuthRequired() }
+                .map {
+                    BiometricMessage(
+                        FINGERPRINT,
+                        BiometricMessageType.FAIL,
+                        BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED,
+                        if (isUdfps) {
+                            resources.getString(
+                                com.android.internal.R.string.fingerprint_udfps_error_not_match
+                            )
+                        } else {
+                            resources.getString(
+                                com.android.internal.R.string.fingerprint_error_not_match
+                            )
+                        },
+                    )
+                }
+        }
+
+    private fun isUdfps() =
+        fingerprintPropertyRepository.sensorType.map {
+            it == FingerprintSensorType.UDFPS_OPTICAL ||
+                it == FingerprintSensorType.UDFPS_ULTRASONIC
+        }
+
+    private fun isPrimaryAuthRequired(): Boolean {
+        // Only checking if unlocking with Biometric is allowed (no matter strong or non-strong
+        // as long as primary auth, i.e. PIN/pattern/password, is required), so it's ok to
+        // pass true for isStrongBiometric to isUnlockingWithBiometricAllowed() to bypass the
+        // check of whether non-strong biometric is allowed since strong biometrics can still be
+        // used.
+        return !keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(true /* isStrongBiometric */)
+    }
+}
+
+data class BiometricMessage(
+    val source: BiometricSourceType,
+    val type: BiometricMessageType,
+    val id: Int,
+    val message: String?,
+)
+
+enum class BiometricMessageType {
+    HELP,
+    ERROR,
+    FAIL,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DozeInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DozeInteractor.kt
index 2efcd0c..0c898be 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DozeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DozeInteractor.kt
@@ -35,4 +35,8 @@
     fun setLastTapToWakePosition(position: Point) {
         keyguardRepository.setLastDozeTapToWakePosition(position)
     }
+
+    fun dozeTimeTick() {
+        keyguardRepository.dozeTimeTick()
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt
index 74ef7a5..141b130 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt
@@ -16,8 +16,8 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
-import com.android.systemui.keyguard.shared.model.AuthenticationStatus
-import com.android.systemui.keyguard.shared.model.DetectionStatus
+import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FaceDetectionStatus
 import kotlinx.coroutines.flow.Flow
 
 /**
@@ -27,10 +27,10 @@
 interface KeyguardFaceAuthInteractor {
 
     /** Current authentication status */
-    val authenticationStatus: Flow<AuthenticationStatus>
+    val authenticationStatus: Flow<FaceAuthenticationStatus>
 
     /** Current detection status */
-    val detectionStatus: Flow<DetectionStatus>
+    val detectionStatus: Flow<FaceDetectionStatus>
 
     /** Can face auth be run right now */
     fun canFaceAuthRun(): Boolean
@@ -72,8 +72,8 @@
  */
 interface FaceAuthenticationListener {
     /** Receive face authentication status updates */
-    fun onAuthenticationStatusChanged(status: AuthenticationStatus)
+    fun onAuthenticationStatusChanged(status: FaceAuthenticationStatus)
 
     /** Receive status updates whenever face detection runs */
-    fun onDetectionStatusChanged(status: DetectionStatus)
+    fun onDetectionStatusChanged(status: FaceDetectionStatus)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index 7fae752..1553525 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -70,6 +70,8 @@
     val dozeAmount: Flow<Float> = repository.linearDozeAmount
     /** Whether the system is in doze mode. */
     val isDozing: Flow<Boolean> = repository.isDozing
+    /** Receive an event for doze time tick */
+    val dozeTimeTick: Flow<Unit> = repository.dozeTimeTick
     /** Whether Always-on Display mode is available. */
     val isAodAvailable: Flow<Boolean> = repository.isAodAvailable
     /** Doze transition information. */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt
index 5005b6c..10dd900 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt
@@ -17,8 +17,8 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.shared.model.AuthenticationStatus
-import com.android.systemui.keyguard.shared.model.DetectionStatus
+import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FaceDetectionStatus
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.emptyFlow
@@ -31,9 +31,9 @@
  */
 @SysUISingleton
 class NoopKeyguardFaceAuthInteractor @Inject constructor() : KeyguardFaceAuthInteractor {
-    override val authenticationStatus: Flow<AuthenticationStatus>
+    override val authenticationStatus: Flow<FaceAuthenticationStatus>
         get() = emptyFlow()
-    override val detectionStatus: Flow<DetectionStatus>
+    override val detectionStatus: Flow<FaceDetectionStatus>
         get() = emptyFlow()
 
     override fun canFaceAuthRun(): Boolean = false
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt
index d467225..6e7a20b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt
@@ -30,8 +30,8 @@
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository
-import com.android.systemui.keyguard.shared.model.AuthenticationStatus
-import com.android.systemui.keyguard.shared.model.ErrorAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.ErrorFaceAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.log.FaceAuthenticationLogger
 import com.android.systemui.util.kotlin.pairwise
@@ -165,10 +165,10 @@
         repository.cancel()
     }
 
-    private val _authenticationStatusOverride = MutableStateFlow<AuthenticationStatus?>(null)
+    private val faceAuthenticationStatusOverride = MutableStateFlow<FaceAuthenticationStatus?>(null)
     /** Provide the status of face authentication */
     override val authenticationStatus =
-        merge(_authenticationStatusOverride.filterNotNull(), repository.authenticationStatus)
+        merge(faceAuthenticationStatusOverride.filterNotNull(), repository.authenticationStatus)
 
     /** Provide the status of face detection */
     override val detectionStatus = repository.detectionStatus
@@ -176,13 +176,13 @@
     private fun runFaceAuth(uiEvent: FaceAuthUiEvent, fallbackToDetect: Boolean) {
         if (featureFlags.isEnabled(Flags.FACE_AUTH_REFACTOR)) {
             if (repository.isLockedOut.value) {
-                _authenticationStatusOverride.value =
-                    ErrorAuthenticationStatus(
+                faceAuthenticationStatusOverride.value =
+                    ErrorFaceAuthenticationStatus(
                         BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT,
                         context.resources.getString(R.string.keyguard_face_unlock_unavailable)
                     )
             } else {
-                _authenticationStatusOverride.value = null
+                faceAuthenticationStatusOverride.value = null
                 applicationScope.launch {
                     faceAuthenticationLogger.authRequested(uiEvent)
                     repository.authenticate(uiEvent, fallbackToDetection = fallbackToDetect)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt
index b354cfd..0f6d82e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt
@@ -23,33 +23,35 @@
  * Authentication status provided by
  * [com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository]
  */
-sealed class AuthenticationStatus
+sealed class FaceAuthenticationStatus
 
 /** Success authentication status. */
-data class SuccessAuthenticationStatus(val successResult: FaceManager.AuthenticationResult) :
-    AuthenticationStatus()
+data class SuccessFaceAuthenticationStatus(val successResult: FaceManager.AuthenticationResult) :
+    FaceAuthenticationStatus()
 
 /** Face authentication help message. */
-data class HelpAuthenticationStatus(val msgId: Int, val msg: String?) : AuthenticationStatus()
+data class HelpFaceAuthenticationStatus(val msgId: Int, val msg: String?) :
+    FaceAuthenticationStatus()
 
 /** Face acquired message. */
-data class AcquiredAuthenticationStatus(val acquiredInfo: Int) : AuthenticationStatus()
+data class AcquiredFaceAuthenticationStatus(val acquiredInfo: Int) : FaceAuthenticationStatus()
 
 /** Face authentication failed message. */
-object FailedAuthenticationStatus : AuthenticationStatus()
+object FailedFaceAuthenticationStatus : FaceAuthenticationStatus()
 
 /** Face authentication error message */
-data class ErrorAuthenticationStatus(
+data class ErrorFaceAuthenticationStatus(
     val msgId: Int,
     val msg: String? = null,
     // present to break equality check if the same error occurs repeatedly.
     val createdAt: Long = elapsedRealtime()
-) : AuthenticationStatus() {
+) : FaceAuthenticationStatus() {
     /**
      * Method that checks if [msgId] is a lockout error. A lockout error means that face
      * authentication is locked out.
      */
-    fun isLockoutError() = msgId == FaceManager.FACE_ERROR_LOCKOUT_PERMANENT
+    fun isLockoutError() =
+        msgId == FaceManager.FACE_ERROR_LOCKOUT_PERMANENT || msgId == FaceManager.FACE_ERROR_LOCKOUT
 
     /**
      * Method that checks if [msgId] is a cancellation error. This means that face authentication
@@ -64,4 +66,4 @@
 }
 
 /** Face detection success message. */
-data class DetectionStatus(val sensorId: Int, val userId: Int, val isStrongBiometric: Boolean)
+data class FaceDetectionStatus(val sensorId: Int, val userId: Int, val isStrongBiometric: Boolean)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt
new file mode 100644
index 0000000..7fc6016
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.keyguard.shared.model
+
+import android.os.SystemClock.elapsedRealtime
+
+/**
+ * Fingerprint authentication status provided by
+ * [com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository]
+ */
+sealed class FingerprintAuthenticationStatus
+
+/** Fingerprint authentication success status. */
+data class SuccessFingerprintAuthenticationStatus(
+    val userId: Int,
+    val isStrongBiometric: Boolean,
+) : FingerprintAuthenticationStatus()
+
+/** Fingerprint authentication help message. */
+data class HelpFingerprintAuthenticationStatus(
+    val msgId: Int,
+    val msg: String?,
+) : FingerprintAuthenticationStatus()
+
+/** Fingerprint acquired message. */
+data class AcquiredFingerprintAuthenticationStatus(val acquiredInfo: Int) :
+    FingerprintAuthenticationStatus()
+
+/** Fingerprint authentication failed message. */
+object FailFingerprintAuthenticationStatus : FingerprintAuthenticationStatus()
+
+/** Fingerprint authentication error message */
+data class ErrorFingerprintAuthenticationStatus(
+    val msgId: Int,
+    val msg: String? = null,
+    // present to break equality check if the same error occurs repeatedly.
+    val createdAt: Long = elapsedRealtime(),
+) : FingerprintAuthenticationStatus()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/ScreenModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/ScreenModel.kt
new file mode 100644
index 0000000..80a1b75
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/ScreenModel.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.keyguard.shared.model
+
+import com.android.systemui.keyguard.ScreenLifecycle
+
+/** Model device screen lifecycle states. */
+data class ScreenModel(
+    val state: ScreenState,
+) {
+    companion object {
+        fun fromScreenLifecycle(screenLifecycle: ScreenLifecycle): ScreenModel {
+            return ScreenModel(ScreenState.fromScreenLifecycleInt(screenLifecycle.getScreenState()))
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/ScreenState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/ScreenState.kt
new file mode 100644
index 0000000..fe5d935
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/ScreenState.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.shared.model
+
+import com.android.systemui.keyguard.ScreenLifecycle
+
+enum class ScreenState {
+    /** Screen is fully off. */
+    SCREEN_OFF,
+    /** Signal that the screen is turning on. */
+    SCREEN_TURNING_ON,
+    /** Screen is fully on. */
+    SCREEN_ON,
+    /** Signal that the screen is turning off. */
+    SCREEN_TURNING_OFF;
+
+    companion object {
+        fun fromScreenLifecycleInt(value: Int): ScreenState {
+            return when (value) {
+                ScreenLifecycle.SCREEN_OFF -> SCREEN_OFF
+                ScreenLifecycle.SCREEN_TURNING_ON -> SCREEN_TURNING_ON
+                ScreenLifecycle.SCREEN_ON -> SCREEN_ON
+                ScreenLifecycle.SCREEN_TURNING_OFF -> SCREEN_TURNING_OFF
+                else -> throw IllegalArgumentException("Invalid screen value: $value")
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsAodFingerprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsAodFingerprintViewBinder.kt
index 728dd39..9872d97 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsAodFingerprintViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsAodFingerprintViewBinder.kt
@@ -37,6 +37,7 @@
         view: LottieAnimationView,
         viewModel: UdfpsAodViewModel,
     ) {
+        view.alpha = 0f
         view.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.STARTED) {
                 launch {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsBackgroundViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsBackgroundViewBinder.kt
index 26ef468..0113628 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsBackgroundViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsBackgroundViewBinder.kt
@@ -38,6 +38,7 @@
         view: ImageView,
         viewModel: BackgroundViewModel,
     ) {
+        view.alpha = 0f
         view.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.STARTED) {
                 launch {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsFingerprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsFingerprintViewBinder.kt
index 0ab8e52..bab04f2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsFingerprintViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsFingerprintViewBinder.kt
@@ -42,6 +42,7 @@
         view: LottieAnimationView,
         viewModel: FingerprintViewModel,
     ) {
+        view.alpha = 0f
         view.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.STARTED) {
                 launch {
diff --git a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt
index 68cdfb6..373f705 100644
--- a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt
@@ -4,7 +4,7 @@
 import android.hardware.face.FaceSensorPropertiesInternal
 import com.android.keyguard.FaceAuthUiEvent
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.shared.model.ErrorAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.ErrorFaceAuthenticationStatus
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.log.core.LogLevel.DEBUG
 import com.android.systemui.log.dagger.FaceAuthLog
@@ -240,7 +240,7 @@
         )
     }
 
-    fun hardwareError(errorStatus: ErrorAuthenticationStatus) {
+    fun hardwareError(errorStatus: ErrorFaceAuthenticationStatus) {
         logBuffer.log(
             TAG,
             DEBUG,
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt
index 0b33904..258284e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt
@@ -31,15 +31,12 @@
 private const val TAG = "RecommendationViewHolder"
 
 /** ViewHolder for a Smartspace media recommendation. */
-class RecommendationViewHolder private constructor(itemView: View, updatedView: Boolean) {
+class RecommendationViewHolder private constructor(itemView: View) {
 
     val recommendations = itemView as TransitionLayout
 
     // Recommendation screen
-    lateinit var cardIcon: ImageView
-    lateinit var mediaAppIcons: List<CachingIconView>
-    lateinit var mediaProgressBars: List<SeekBar>
-    lateinit var cardTitle: TextView
+    val cardTitle: TextView = itemView.requireViewById(R.id.media_rec_title)
 
     val mediaCoverContainers =
         listOf<ViewGroup>(
@@ -47,53 +44,25 @@
             itemView.requireViewById(R.id.media_cover2_container),
             itemView.requireViewById(R.id.media_cover3_container)
         )
+    val mediaAppIcons: List<CachingIconView> =
+        mediaCoverContainers.map { it.requireViewById(R.id.media_rec_app_icon) }
     val mediaTitles: List<TextView> =
-        if (updatedView) {
-            mediaCoverContainers.map { it.requireViewById(R.id.media_title) }
-        } else {
-            listOf(
-                itemView.requireViewById(R.id.media_title1),
-                itemView.requireViewById(R.id.media_title2),
-                itemView.requireViewById(R.id.media_title3)
-            )
-        }
+        mediaCoverContainers.map { it.requireViewById(R.id.media_title) }
     val mediaSubtitles: List<TextView> =
-        if (updatedView) {
-            mediaCoverContainers.map { it.requireViewById(R.id.media_subtitle) }
-        } else {
-            listOf(
-                itemView.requireViewById(R.id.media_subtitle1),
-                itemView.requireViewById(R.id.media_subtitle2),
-                itemView.requireViewById(R.id.media_subtitle3)
-            )
+        mediaCoverContainers.map { it.requireViewById(R.id.media_subtitle) }
+    val mediaProgressBars: List<SeekBar> =
+        mediaCoverContainers.map {
+            it.requireViewById<SeekBar?>(R.id.media_progress_bar).apply {
+                // Media playback is in the direction of tape, not time, so it stays LTR
+                layoutDirection = View.LAYOUT_DIRECTION_LTR
+            }
         }
 
     val mediaCoverItems: List<ImageView> =
-        if (updatedView) {
-            mediaCoverContainers.map { it.requireViewById(R.id.media_cover) }
-        } else {
-            listOf(
-                itemView.requireViewById(R.id.media_cover1),
-                itemView.requireViewById(R.id.media_cover2),
-                itemView.requireViewById(R.id.media_cover3)
-            )
-        }
+        mediaCoverContainers.map { it.requireViewById(R.id.media_cover) }
     val gutsViewHolder = GutsViewHolder(itemView)
 
     init {
-        if (updatedView) {
-            mediaAppIcons = mediaCoverContainers.map { it.requireViewById(R.id.media_rec_app_icon) }
-            cardTitle = itemView.requireViewById(R.id.media_rec_title)
-            mediaProgressBars =
-                mediaCoverContainers.map {
-                    it.requireViewById<SeekBar?>(R.id.media_progress_bar).apply {
-                        // Media playback is in the direction of tape, not time, so it stays LTR
-                        layoutDirection = View.LAYOUT_DIRECTION_LTR
-                    }
-                }
-        } else {
-            cardIcon = itemView.requireViewById<ImageView>(R.id.recommendation_card_icon)
-        }
         (recommendations.background as IlluminationDrawable).let { background ->
             mediaCoverContainers.forEach { background.registerLightSource(it) }
             background.registerLightSource(gutsViewHolder.cancel)
@@ -114,63 +83,31 @@
          * @param parent Parent of inflated view.
          */
         @JvmStatic
-        fun create(
-            inflater: LayoutInflater,
-            parent: ViewGroup,
-            updatedView: Boolean,
-        ): RecommendationViewHolder {
+        fun create(inflater: LayoutInflater, parent: ViewGroup): RecommendationViewHolder {
             val itemView =
-                if (updatedView) {
-                    inflater.inflate(
-                        R.layout.media_recommendations,
-                        parent,
-                        false /* attachToRoot */
-                    )
-                } else {
-                    inflater.inflate(
-                        R.layout.media_smartspace_recommendations,
-                        parent,
-                        false /* attachToRoot */
-                    )
-                }
+                inflater.inflate(R.layout.media_recommendations, parent, false /* attachToRoot */)
             // Because this media view (a TransitionLayout) is used to measure and layout the views
             // in various states before being attached to its parent, we can't depend on the default
             // LAYOUT_DIRECTION_INHERIT to correctly resolve the ltr direction.
             itemView.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
-            return RecommendationViewHolder(itemView, updatedView)
+            return RecommendationViewHolder(itemView)
         }
 
         // Res Ids for the control components on the recommendation view.
         val controlsIds =
             setOf(
-                R.id.recommendation_card_icon,
                 R.id.media_rec_title,
-                R.id.media_cover1,
-                R.id.media_cover2,
-                R.id.media_cover3,
                 R.id.media_cover,
                 R.id.media_cover1_container,
                 R.id.media_cover2_container,
                 R.id.media_cover3_container,
-                R.id.media_title1,
-                R.id.media_title2,
-                R.id.media_title3,
                 R.id.media_title,
-                R.id.media_subtitle1,
-                R.id.media_subtitle2,
-                R.id.media_subtitle3,
                 R.id.media_subtitle,
             )
 
         val mediaTitlesAndSubtitlesIds =
             setOf(
-                R.id.media_title1,
-                R.id.media_title2,
-                R.id.media_title3,
                 R.id.media_title,
-                R.id.media_subtitle1,
-                R.id.media_subtitle2,
-                R.id.media_subtitle3,
                 R.id.media_subtitle,
             )
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt
index 70b5e75..398dcf2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt
@@ -744,11 +744,7 @@
 
             val newRecs = mediaControlPanelFactory.get()
             newRecs.attachRecommendation(
-                RecommendationViewHolder.create(
-                    LayoutInflater.from(context),
-                    mediaContent,
-                    mediaFlags.isRecommendationCardUpdateEnabled()
-                )
+                RecommendationViewHolder.create(LayoutInflater.from(context), mediaContent)
             )
             newRecs.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
             val lp =
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
index a978b92..a12bc2c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
@@ -784,14 +784,7 @@
             contentDescription =
                     mRecommendationViewHolder.getGutsViewHolder().getGutsText().getText();
         } else if (data != null) {
-            if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
-                contentDescription = mContext.getString(
-                        R.string.controls_media_smartspace_rec_header);
-            } else {
-                contentDescription = mContext.getString(
-                        R.string.controls_media_smartspace_rec_description,
-                        data.getAppName(mContext));
-            }
+            contentDescription = mContext.getString(R.string.controls_media_smartspace_rec_header);
         } else {
             contentDescription = null;
         }
@@ -1377,10 +1370,6 @@
         PackageManager packageManager = mContext.getPackageManager();
         // Set up media source app's logo.
         Drawable icon = packageManager.getApplicationIcon(applicationInfo);
-        if (!mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
-            ImageView headerLogoImageView = mRecommendationViewHolder.getCardIcon();
-            headerLogoImageView.setImageDrawable(icon);
-        }
         fetchAndUpdateRecommendationColors(icon);
 
         // Set up media rec card's tap action if applicable.
@@ -1401,16 +1390,7 @@
 
             // Set up media item cover.
             ImageView mediaCoverImageView = mediaCoverItems.get(itemIndex);
-            if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
-                bindRecommendationArtwork(
-                        recommendation,
-                        data.getPackageName(),
-                        itemIndex
-                );
-            } else {
-                mediaCoverImageView.post(
-                        () -> mediaCoverImageView.setImageIcon(recommendation.getIcon()));
-            }
+            bindRecommendationArtwork(recommendation, data.getPackageName(), itemIndex);
 
             // Set up the media item's click listener if applicable.
             ViewGroup mediaCoverContainer = mediaCoverContainers.get(itemIndex);
@@ -1455,21 +1435,18 @@
             subtitleView.setText(subtitle);
 
             // Set up progress bar
-            if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
-                SeekBar mediaProgressBar =
-                        mRecommendationViewHolder.getMediaProgressBars().get(itemIndex);
-                TextView mediaSubtitle =
-                        mRecommendationViewHolder.getMediaSubtitles().get(itemIndex);
-                // show progress bar if the recommended album is played.
-                Double progress = MediaDataUtils.getDescriptionProgress(recommendation.getExtras());
-                if (progress == null || progress <= 0.0) {
-                    mediaProgressBar.setVisibility(View.GONE);
-                    mediaSubtitle.setVisibility(View.VISIBLE);
-                } else {
-                    mediaProgressBar.setProgress((int) (progress * 100));
-                    mediaProgressBar.setVisibility(View.VISIBLE);
-                    mediaSubtitle.setVisibility(View.GONE);
-                }
+            SeekBar mediaProgressBar =
+                    mRecommendationViewHolder.getMediaProgressBars().get(itemIndex);
+            TextView mediaSubtitle = mRecommendationViewHolder.getMediaSubtitles().get(itemIndex);
+            // show progress bar if the recommended album is played.
+            Double progress = MediaDataUtils.getDescriptionProgress(recommendation.getExtras());
+            if (progress == null || progress <= 0.0) {
+                mediaProgressBar.setVisibility(View.GONE);
+                mediaSubtitle.setVisibility(View.VISIBLE);
+            } else {
+                mediaProgressBar.setProgress((int) (progress * 100));
+                mediaProgressBar.setVisibility(View.VISIBLE);
+                mediaSubtitle.setVisibility(View.GONE);
             }
         }
         mSmartspaceMediaItemsCount = NUM_REQUIRED_RECOMMENDATIONS;
@@ -1588,9 +1565,7 @@
         int textPrimaryColor = MediaColorSchemesKt.textPrimaryFromScheme(colorScheme);
         int textSecondaryColor = MediaColorSchemesKt.textSecondaryFromScheme(colorScheme);
 
-        if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
-            mRecommendationViewHolder.getCardTitle().setTextColor(textPrimaryColor);
-        }
+        mRecommendationViewHolder.getCardTitle().setTextColor(textPrimaryColor);
 
         mRecommendationViewHolder.getRecommendations()
                 .setBackgroundTintList(ColorStateList.valueOf(backgroundColor));
@@ -1598,12 +1573,9 @@
                 (title) -> title.setTextColor(textPrimaryColor));
         mRecommendationViewHolder.getMediaSubtitles().forEach(
                 (subtitle) -> subtitle.setTextColor(textSecondaryColor));
-        if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
-            mRecommendationViewHolder.getMediaProgressBars().forEach(
-                    (progressBar) -> progressBar.setProgressTintList(
-                            ColorStateList.valueOf(textPrimaryColor))
-            );
-        }
+        mRecommendationViewHolder.getMediaProgressBars().forEach(
+                (progressBar) -> progressBar.setProgressTintList(
+                        ColorStateList.valueOf(textPrimaryColor)));
 
         mRecommendationViewHolder.getGutsViewHolder().setColors(colorScheme);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
index 4bca778..1dd969f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
@@ -655,13 +655,8 @@
                 expandedLayout.load(context, R.xml.media_session_expanded)
             }
             TYPE.RECOMMENDATION -> {
-                if (mediaFlags.isRecommendationCardUpdateEnabled()) {
-                    collapsedLayout.load(context, R.xml.media_recommendations_view_collapsed)
-                    expandedLayout.load(context, R.xml.media_recommendations_view_expanded)
-                } else {
-                    collapsedLayout.load(context, R.xml.media_recommendation_collapsed)
-                    expandedLayout.load(context, R.xml.media_recommendation_expanded)
-                }
+                collapsedLayout.load(context, R.xml.media_recommendations_collapsed)
+                expandedLayout.load(context, R.xml.media_recommendations_expanded)
             }
         }
         refreshState()
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
index 01f047c..09aef88 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
@@ -49,10 +49,6 @@
      */
     fun isRetainingPlayersEnabled() = featureFlags.isEnabled(Flags.MEDIA_RETAIN_SESSIONS)
 
-    /** Check whether we show the updated recommendation card. */
-    fun isRecommendationCardUpdateEnabled() =
-        featureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)
-
     /** Check whether to get progress information for resume players */
     fun isResumeProgressEnabled() = featureFlags.isEnabled(Flags.MEDIA_RESUME_PROGRESS)
 
diff --git a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt
index eb1ca66..809edc0 100644
--- a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt
@@ -70,6 +70,19 @@
         }
     }
 
+    /**
+     * Wakes up the device if dreaming with a screensaver.
+     *
+     * @param why a string explaining why we're waking the device for debugging purposes. Should be
+     *   in SCREAMING_SNAKE_CASE.
+     * @param wakeReason the PowerManager-based reason why we're waking the device.
+     */
+    fun wakeUpIfDreaming(why: String, @PowerManager.WakeReason wakeReason: Int) {
+        if (statusBarStateController.isDreaming) {
+            repository.wakeUp(why, wakeReason)
+        }
+    }
+
     companion object {
         private const val FSI_WAKE_WHY = "full_screen_intent"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
index 83b373d..856a92e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
@@ -58,6 +58,7 @@
     private final BrightnessSliderController mBrightnessSliderController;
     private final BrightnessMirrorHandler mBrightnessMirrorHandler;
     private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+    private boolean mListening;
 
     private View.OnTouchListener mTileLayoutTouchListener = new View.OnTouchListener() {
         @Override
@@ -159,12 +160,15 @@
     public void setListening(boolean listening, boolean expanded) {
         setListening(listening && expanded);
 
-        // Set the listening as soon as the QS fragment starts listening regardless of the
-        //expansion, so it will update the current brightness before the slider is visible.
-        if (listening) {
-            mBrightnessController.registerCallbacks();
-        } else {
-            mBrightnessController.unregisterCallbacks();
+        if (listening != mListening) {
+            mListening = listening;
+            // Set the listening as soon as the QS fragment starts listening regardless of the
+            //expansion, so it will update the current brightness before the slider is visible.
+            if (listening) {
+                mBrightnessController.registerCallbacks();
+            } else {
+                mBrightnessController.unregisterCallbacks();
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt
index 285ff74..b23c4ec 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt
@@ -77,7 +77,7 @@
             authenticationInteractor.isUnlocked
                 .map { isUnlocked ->
                     val currentSceneKey = sceneInteractor.currentScene(CONTAINER_NAME).value.key
-                    val isBypassEnabled = authenticationInteractor.isBypassEnabled.value
+                    val isBypassEnabled = authenticationInteractor.isBypassEnabled()
                     when {
                         isUnlocked ->
                             when (currentSceneKey) {
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
index 5aa5fee..f9324a9 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
@@ -16,19 +16,25 @@
 
 package com.android.systemui.scene.ui.view
 
+import android.view.Gravity
+import android.view.View
 import android.view.ViewGroup
+import android.widget.FrameLayout
 import androidx.activity.OnBackPressedDispatcher
 import androidx.activity.OnBackPressedDispatcherOwner
 import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
+import androidx.core.view.isVisible
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.R
 import com.android.systemui.compose.ComposeFacade
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.scene.shared.model.Scene
 import com.android.systemui.scene.shared.model.SceneContainerConfig
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
+import java.time.Instant
 import kotlinx.coroutines.launch
 
 object SceneWindowRootViewBinder {
@@ -77,6 +83,9 @@
                         )
                     )
 
+                    val legacyView = view.requireViewById<View>(R.id.legacy_window_root)
+                    view.addView(createVisibilityToggleView(legacyView))
+
                     launch {
                         viewModel.isVisible.collect { isVisible ->
                             onVisibilityChangedInternal(isVisible)
@@ -89,4 +98,28 @@
             }
         }
     }
+
+    private var clickCount = 0
+    private var lastClick = Instant.now()
+
+    /**
+     * A temporary UI to toggle on/off the visibility of the given [otherView]. It is toggled by
+     * tapping 5 times in quick succession on the device camera (top center).
+     */
+    // TODO(b/291321285): Remove this when the Flexiglass UI is mature enough to turn off legacy
+    //  SysUI altogether.
+    private fun createVisibilityToggleView(otherView: View): View {
+        val toggleView = View(otherView.context)
+        toggleView.layoutParams = FrameLayout.LayoutParams(200, 200, Gravity.CENTER_HORIZONTAL)
+        toggleView.setOnClickListener {
+            val now = Instant.now()
+            clickCount = if (now.minusSeconds(2) > lastClick) 1 else clickCount + 1
+            if (clickCount == 5) {
+                otherView.isVisible = !otherView.isVisible
+                clickCount = 0
+            }
+            lastClick = now
+        }
+        return toggleView
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java
index 9594ba3..6af9b73 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java
@@ -22,7 +22,6 @@
 
 import android.animation.ValueAnimator;
 import android.annotation.NonNull;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.database.ContentObserver;
 import android.hardware.display.BrightnessInfo;
@@ -31,10 +30,10 @@
 import android.os.AsyncTask;
 import android.os.Handler;
 import android.os.HandlerExecutor;
+import android.os.Looper;
 import android.os.Message;
 import android.os.PowerManager;
 import android.os.RemoteException;
-import android.os.ServiceManager;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
@@ -43,6 +42,8 @@
 import android.util.Log;
 import android.util.MathUtils;
 
+import androidx.annotation.Nullable;
+
 import com.android.internal.display.BrightnessSynchronizer;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
@@ -52,10 +53,13 @@
 import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.policy.BrightnessMirrorController;
+import com.android.systemui.util.settings.SecureSettings;
 
 import java.util.concurrent.Executor;
 
-import javax.inject.Inject;
+import dagger.assisted.Assisted;
+import dagger.assisted.AssistedFactory;
+import dagger.assisted.AssistedInject;
 
 public class BrightnessController implements ToggleSlider.Listener, MirroredBrightnessController {
     private static final String TAG = "CentralSurfaces.BrightnessController";
@@ -75,8 +79,11 @@
     private final DisplayManager mDisplayManager;
     private final UserTracker mUserTracker;
     private final DisplayTracker mDisplayTracker;
+    @Nullable
     private final IVrManager mVrManager;
 
+    private final SecureSettings mSecureSettings;
+
     private final Executor mMainExecutor;
     private final Handler mBackgroundHandler;
     private final BrightnessObserver mBrightnessObserver;
@@ -106,6 +113,8 @@
     /** ContentObserver to watch brightness */
     private class BrightnessObserver extends ContentObserver {
 
+        private boolean mObserving = false;
+
         BrightnessObserver(Handler handler) {
             super(handler);
         }
@@ -124,19 +133,17 @@
         }
 
         public void startObserving() {
-            final ContentResolver cr = mContext.getContentResolver();
-            cr.unregisterContentObserver(this);
-            cr.registerContentObserver(
-                    BRIGHTNESS_MODE_URI,
-                    false, this, UserHandle.USER_ALL);
-            mDisplayTracker.addBrightnessChangeCallback(mBrightnessListener,
-                    new HandlerExecutor(mHandler));
+            if (!mObserving) {
+                mObserving = true;
+                mSecureSettings.registerContentObserverForUser(
+                        BRIGHTNESS_MODE_URI,
+                        false, this, UserHandle.USER_ALL);
+            }
         }
 
         public void stopObserving() {
-            final ContentResolver cr = mContext.getContentResolver();
-            cr.unregisterContentObserver(this);
-            mDisplayTracker.removeCallback(mBrightnessListener);
+            mSecureSettings.unregisterContentObserver(this);
+            mObserving = false;
         }
 
     }
@@ -159,6 +166,8 @@
             }
 
             mBrightnessObserver.startObserving();
+            mDisplayTracker.addBrightnessChangeCallback(mBrightnessListener,
+                    new HandlerExecutor(mMainHandler));
             mUserTracker.addCallback(mUserChangedCallback, mMainExecutor);
 
             // Update the slider and mode before attaching the listener so we don't
@@ -166,7 +175,7 @@
             mUpdateModeRunnable.run();
             mUpdateSliderRunnable.run();
 
-            mHandler.sendEmptyMessage(MSG_ATTACH_LISTENER);
+            mMainHandler.sendEmptyMessage(MSG_ATTACH_LISTENER);
         }
     };
 
@@ -187,9 +196,10 @@
             }
 
             mBrightnessObserver.stopObserving();
+            mDisplayTracker.removeCallback(mBrightnessListener);
             mUserTracker.removeCallback(mUserChangedCallback);
 
-            mHandler.sendEmptyMessage(MSG_DETACH_LISTENER);
+            mMainHandler.sendEmptyMessage(MSG_DETACH_LISTENER);
         }
     };
 
@@ -225,7 +235,7 @@
             mBrightnessMin = info.brightnessMinimum;
             // Value is passed as intbits, since this is what the message takes.
             final int valueAsIntBits = Float.floatToIntBits(info.brightness);
-            mHandler.obtainMessage(MSG_UPDATE_SLIDER, valueAsIntBits,
+            mMainHandler.obtainMessage(MSG_UPDATE_SLIDER, valueAsIntBits,
                     inVrMode ? 1 : 0).sendToTarget();
         }
     };
@@ -233,14 +243,14 @@
     private final IVrStateCallbacks mVrStateCallbacks = new IVrStateCallbacks.Stub() {
         @Override
         public void onVrStateChanged(boolean enabled) {
-            mHandler.obtainMessage(MSG_VR_MODE_CHANGED, enabled ? 1 : 0, 0)
+            mMainHandler.obtainMessage(MSG_VR_MODE_CHANGED, enabled ? 1 : 0, 0)
                     .sendToTarget();
         }
     };
 
-    private final Handler mHandler = new Handler() {
+    private final Handler.Callback mHandlerCallback = new Handler.Callback() {
         @Override
-        public void handleMessage(Message msg) {
+        public boolean handleMessage(Message msg) {
             mExternalChange = true;
             try {
                 switch (msg.what) {
@@ -257,14 +267,18 @@
                         updateVrMode(msg.arg1 != 0);
                         break;
                     default:
-                        super.handleMessage(msg);
+                        return false;
+
                 }
             } finally {
                 mExternalChange = false;
             }
+            return true;
         }
     };
 
+    private final Handler mMainHandler;
+
     private final UserTracker.Callback mUserChangedCallback =
             new UserTracker.Callback() {
                 @Override
@@ -274,12 +288,17 @@
                 }
             };
 
+    @AssistedInject
     public BrightnessController(
             Context context,
-            ToggleSlider control,
+            @Assisted ToggleSlider control,
             UserTracker userTracker,
             DisplayTracker displayTracker,
+            DisplayManager displayManager,
+            SecureSettings secureSettings,
+            @Nullable IVrManager iVrManager,
             @Main Executor mainExecutor,
+            @Main Looper mainLooper,
             @Background Handler bgHandler) {
         mContext = context;
         mControl = control;
@@ -288,22 +307,23 @@
         mBackgroundHandler = bgHandler;
         mUserTracker = userTracker;
         mDisplayTracker = displayTracker;
-        mBrightnessObserver = new BrightnessObserver(mHandler);
-
+        mSecureSettings = secureSettings;
         mDisplayId = mContext.getDisplayId();
-        PowerManager pm = context.getSystemService(PowerManager.class);
+        mDisplayManager = displayManager;
+        mVrManager = iVrManager;
 
-        mDisplayManager = context.getSystemService(DisplayManager.class);
-        mVrManager = IVrManager.Stub.asInterface(ServiceManager.getService(
-                Context.VR_SERVICE));
+        mMainHandler = new Handler(mainLooper, mHandlerCallback);
+        mBrightnessObserver = new BrightnessObserver(mMainHandler);
     }
 
     public void registerCallbacks() {
+        mBackgroundHandler.removeCallbacks(mStartListeningRunnable);
         mBackgroundHandler.post(mStartListeningRunnable);
     }
 
     /** Unregister all call backs, both to and from the controller */
     public void unregisterCallbacks() {
+        mBackgroundHandler.removeCallbacks(mStopListeningRunnable);
         mBackgroundHandler.post(mStopListeningRunnable);
         mControlValueInitialized = false;
     }
@@ -418,38 +438,12 @@
         mSliderAnimator.start();
     }
 
+
+
     /** Factory for creating a {@link BrightnessController}. */
-    public static class Factory {
-        private final Context mContext;
-        private final UserTracker mUserTracker;
-        private final DisplayTracker mDisplayTracker;
-        private final Executor mMainExecutor;
-        private final Handler mBackgroundHandler;
-
-        @Inject
-        public Factory(
-                Context context,
-                UserTracker userTracker,
-                DisplayTracker displayTracker,
-                @Main Executor mainExecutor,
-                @Background Handler bgHandler) {
-            mContext = context;
-            mUserTracker = userTracker;
-            mDisplayTracker = displayTracker;
-            mMainExecutor = mainExecutor;
-            mBackgroundHandler = bgHandler;
-        }
-
+    @AssistedFactory
+    public interface Factory {
         /** Create a {@link BrightnessController} */
-        public BrightnessController create(ToggleSlider toggleSlider) {
-            return new BrightnessController(
-                    mContext,
-                    toggleSlider,
-                    mUserTracker,
-                    mDisplayTracker,
-                    mMainExecutor,
-                    mBackgroundHandler);
-        }
+        BrightnessController create(ToggleSlider toggleSlider);
     }
-
 }
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
index 182e456..38b1f14 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
@@ -23,7 +23,6 @@
 import android.app.Activity;
 import android.graphics.Rect;
 import android.os.Bundle;
-import android.os.Handler;
 import android.view.Gravity;
 import android.view.KeyEvent;
 import android.view.View;
@@ -37,10 +36,7 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.systemui.R;
-import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.settings.DisplayTracker;
-import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 
@@ -56,26 +52,21 @@
 
     private BrightnessController mBrightnessController;
     private final BrightnessSliderController.Factory mToggleSliderFactory;
-    private final UserTracker mUserTracker;
-    private final DisplayTracker mDisplayTracker;
+    private final BrightnessController.Factory mBrightnessControllerFactory;
     private final DelayableExecutor mMainExecutor;
-    private final Handler mBackgroundHandler;
     private final AccessibilityManagerWrapper mAccessibilityMgr;
     private Runnable mCancelTimeoutRunnable;
 
     @Inject
     public BrightnessDialog(
-            UserTracker userTracker,
-            DisplayTracker displayTracker,
-            BrightnessSliderController.Factory factory,
+            BrightnessSliderController.Factory brightnessSliderfactory,
+            BrightnessController.Factory brightnessControllerFactory,
             @Main DelayableExecutor mainExecutor,
-            @Background Handler bgHandler,
-            AccessibilityManagerWrapper accessibilityMgr) {
-        mUserTracker = userTracker;
-        mDisplayTracker = displayTracker;
-        mToggleSliderFactory = factory;
+            AccessibilityManagerWrapper accessibilityMgr
+    ) {
+        mToggleSliderFactory = brightnessSliderfactory;
+        mBrightnessControllerFactory = brightnessControllerFactory;
         mMainExecutor = mainExecutor;
-        mBackgroundHandler = bgHandler;
         mAccessibilityMgr = accessibilityMgr;
     }
 
@@ -121,8 +112,7 @@
         controller.init();
         frame.addView(controller.getRootView(), MATCH_PARENT, WRAP_CONTENT);
 
-        mBrightnessController = new BrightnessController(
-                this, controller, mUserTracker, mDisplayTracker, mMainExecutor, mBackgroundHandler);
+        mBrightnessController = mBrightnessControllerFactory.create(controller);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/shade/LockscreenHostedDreamGestureListener.kt b/packages/SystemUI/src/com/android/systemui/shade/LockscreenHostedDreamGestureListener.kt
new file mode 100644
index 0000000..45fc68a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/LockscreenHostedDreamGestureListener.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.shade
+
+import android.os.PowerManager
+import android.view.GestureDetector
+import android.view.MotionEvent
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.plugins.FalsingManager.LOW_PENALTY
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.statusbar.StatusBarState
+import javax.inject.Inject
+
+/**
+ * This gestureListener will wake up by tap when the device is dreaming but not dozing, and the
+ * selected screensaver is hosted in lockscreen. Tap is gated by the falsing manager.
+ *
+ * Touches go through the [NotificationShadeWindowViewController].
+ */
+@SysUISingleton
+class LockscreenHostedDreamGestureListener
+@Inject
+constructor(
+    private val falsingManager: FalsingManager,
+    private val powerInteractor: PowerInteractor,
+    private val statusBarStateController: StatusBarStateController,
+    private val primaryBouncerInteractor: PrimaryBouncerInteractor,
+    private val keyguardRepository: KeyguardRepository,
+    private val shadeLogger: ShadeLogger,
+) : GestureDetector.SimpleOnGestureListener() {
+    private val TAG = this::class.simpleName
+
+    override fun onSingleTapUp(e: MotionEvent): Boolean {
+        if (shouldHandleMotionEvent()) {
+            if (!falsingManager.isFalseTap(LOW_PENALTY)) {
+                shadeLogger.d("$TAG#onSingleTapUp tap handled, requesting wakeUpIfDreaming")
+                powerInteractor.wakeUpIfDreaming(
+                    "DREAMING_SINGLE_TAP",
+                    PowerManager.WAKE_REASON_TAP
+                )
+            } else {
+                shadeLogger.d("$TAG#onSingleTapUp false tap ignored")
+            }
+            return true
+        }
+        return false
+    }
+
+    private fun shouldHandleMotionEvent(): Boolean {
+        return keyguardRepository.isActiveDreamLockscreenHosted.value &&
+            statusBarStateController.state == StatusBarState.KEYGUARD &&
+            !primaryBouncerInteractor.isBouncerShowing()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index ea15035..9399d48 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -168,7 +168,6 @@
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.NotificationShelfController;
 import com.android.systemui.statusbar.PulseExpansionHandler;
-import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.VibratorHelper;
@@ -224,6 +223,8 @@
 import com.android.systemui.util.time.SystemClock;
 import com.android.wm.shell.animation.FlingAnimationUtils;
 
+import kotlin.Unit;
+
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -234,8 +235,6 @@
 import javax.inject.Inject;
 import javax.inject.Provider;
 
-import kotlin.Unit;
-
 import kotlinx.coroutines.CoroutineDispatcher;
 
 @SysUISingleton
@@ -440,8 +439,6 @@
     private final FalsingCollector mFalsingCollector;
     private final ShadeHeadsUpTrackerImpl mShadeHeadsUpTracker = new ShadeHeadsUpTrackerImpl();
     private final ShadeFoldAnimator mShadeFoldAnimator = new ShadeFoldAnimatorImpl();
-    private final ShadeNotificationPresenterImpl mShadeNotificationPresenter =
-            new ShadeNotificationPresenterImpl();
 
     private boolean mShowIconsWhenExpanded;
     private int mIndicationBottomPadding;
@@ -1473,7 +1470,7 @@
                 // so we should not add a padding for them
                 stackScrollerPadding = 0;
             } else {
-                stackScrollerPadding = mQsController.getUnlockedStackScrollerPadding();
+                stackScrollerPadding = mQsController.getHeaderHeight();
             }
         } else {
             stackScrollerPadding = mClockPositionResult.stackScrollerPaddingExpanded;
@@ -1520,7 +1517,7 @@
                 userSwitcherPreferredY,
                 darkAmount, mOverStretchAmount,
                 bypassEnabled,
-                mQsController.getUnlockedStackScrollerPadding(),
+                mQsController.getHeaderHeight(),
                 mQsController.computeExpansionFraction(),
                 mDisplayTopInset,
                 mSplitShadeEnabled,
@@ -3323,23 +3320,6 @@
         ).printTableData(ipw);
     }
 
-    private final class ShadeNotificationPresenterImpl implements ShadeNotificationPresenter{
-        @Override
-        public RemoteInputController.Delegate createRemoteInputDelegate() {
-            return mNotificationStackScrollLayoutController.createDelegate();
-        }
-
-        @Override
-        public boolean hasPulsingNotifications() {
-            return mNotificationListContainer.hasPulsingNotifications();
-        }
-    }
-
-    @Override
-    public ShadeNotificationPresenter getShadeNotificationPresenter() {
-        return mShadeNotificationPresenter;
-    }
-
     @Override
     public void initDependencies(
             CentralSurfaces centralSurfaces,
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index bb2be66..b328c55 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -543,7 +543,6 @@
                 state.forceUserActivity,
                 state.launchingActivityFromNotification,
                 state.mediaBackdropShowing,
-                state.wallpaperSupportsAmbientMode,
                 state.windowNotTouchable,
                 state.componentsForcingTopUi,
                 state.forceOpenTokens,
@@ -734,12 +733,6 @@
         apply(mCurrentState);
     }
 
-    @Override
-    public void setWallpaperSupportsAmbientMode(boolean supportsAmbientMode) {
-        mCurrentState.wallpaperSupportsAmbientMode = supportsAmbientMode;
-        apply(mCurrentState);
-    }
-
     /**
      * @param state The {@link StatusBarStateController} of the status bar.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt
index 7812f07..d252943 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt
@@ -46,7 +46,6 @@
     @JvmField var forceUserActivity: Boolean = false,
     @JvmField var launchingActivityFromNotification: Boolean = false,
     @JvmField var mediaBackdropShowing: Boolean = false,
-    @JvmField var wallpaperSupportsAmbientMode: Boolean = false,
     @JvmField var windowNotTouchable: Boolean = false,
     @JvmField var componentsForcingTopUi: MutableSet<String> = mutableSetOf(),
     @JvmField var forceOpenTokens: MutableSet<Any> = mutableSetOf(),
@@ -84,7 +83,6 @@
             forceUserActivity.toString(),
             launchingActivityFromNotification.toString(),
             mediaBackdropShowing.toString(),
-            wallpaperSupportsAmbientMode.toString(),
             windowNotTouchable.toString(),
             componentsForcingTopUi.toString(),
             forceOpenTokens.toString(),
@@ -124,7 +122,6 @@
             forceUserActivity: Boolean,
             launchingActivity: Boolean,
             backdropShowing: Boolean,
-            wallpaperSupportsAmbientMode: Boolean,
             notTouchable: Boolean,
             componentsForcingTopUi: MutableSet<String>,
             forceOpenTokens: MutableSet<Any>,
@@ -153,7 +150,6 @@
                 this.forceUserActivity = forceUserActivity
                 this.launchingActivityFromNotification = launchingActivity
                 this.mediaBackdropShowing = backdropShowing
-                this.wallpaperSupportsAmbientMode = wallpaperSupportsAmbientMode
                 this.windowNotTouchable = notTouchable
                 this.componentsForcingTopUi.clear()
                 this.componentsForcingTopUi.addAll(componentsForcingTopUi)
@@ -200,7 +196,6 @@
                 "forceUserActivity",
                 "launchingActivity",
                 "backdropShowing",
-                "wallpaperSupportsAmbientMode",
                 "notTouchable",
                 "componentsForcingTopUi",
                 "forceOpenTokens",
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index 5c41d57..108ea68 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.shade;
 
+import static com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED;
 import static com.android.systemui.flags.Flags.TRACKPAD_GESTURE_COMMON;
 import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
 
@@ -46,6 +47,7 @@
 import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.compose.ComposeFacade;
+import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
@@ -72,7 +74,6 @@
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.PhoneStatusBarViewController;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
-import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent;
 import com.android.systemui.statusbar.window.StatusBarWindowStateController;
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
 import com.android.systemui.util.time.SystemClock;
@@ -87,7 +88,7 @@
 /**
  * Controller for {@link NotificationShadeWindowView}.
  */
-@CentralSurfacesComponent.CentralSurfacesScope
+@SysUISingleton
 public class NotificationShadeWindowViewController {
     private static final String TAG = "NotifShadeWindowVC";
     private final FalsingCollector mFalsingCollector;
@@ -102,9 +103,12 @@
     private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
     private final AmbientState mAmbientState;
     private final PulsingGestureListener mPulsingGestureListener;
+    private final LockscreenHostedDreamGestureListener mLockscreenHostedDreamGestureListener;
     private final NotificationInsetsController mNotificationInsetsController;
     private final boolean mIsTrackpadCommonEnabled;
+    private final FeatureFlags mFeatureFlags;
     private GestureDetector mPulsingWakeupGestureHandler;
+    private GestureDetector mDreamingWakeupGestureHandler;
     private View mBrightnessMirror;
     private boolean mTouchActive;
     private boolean mTouchCancelled;
@@ -156,6 +160,7 @@
             NotificationInsetsController notificationInsetsController,
             AmbientState ambientState,
             PulsingGestureListener pulsingGestureListener,
+            LockscreenHostedDreamGestureListener lockscreenHostedDreamGestureListener,
             KeyguardBouncerViewModel keyguardBouncerViewModel,
             KeyguardBouncerComponent.Factory keyguardBouncerComponentFactory,
             KeyguardMessageAreaController.Factory messageAreaControllerFactory,
@@ -187,8 +192,10 @@
         mKeyguardUnlockAnimationController = keyguardUnlockAnimationController;
         mAmbientState = ambientState;
         mPulsingGestureListener = pulsingGestureListener;
+        mLockscreenHostedDreamGestureListener = lockscreenHostedDreamGestureListener;
         mNotificationInsetsController = notificationInsetsController;
         mIsTrackpadCommonEnabled = featureFlags.isEnabled(TRACKPAD_GESTURE_COMMON);
+        mFeatureFlags = featureFlags;
 
         // This view is not part of the newly inflated expanded status bar.
         mBrightnessMirror = mView.findViewById(R.id.brightness_mirror_container);
@@ -237,7 +244,10 @@
         mStackScrollLayout = mView.findViewById(R.id.notification_stack_scroller);
         mPulsingWakeupGestureHandler = new GestureDetector(mView.getContext(),
                 mPulsingGestureListener);
-
+        if (mFeatureFlags.isEnabled(LOCKSCREEN_WALLPAPER_DREAM_ENABLED)) {
+            mDreamingWakeupGestureHandler = new GestureDetector(mView.getContext(),
+                    mLockscreenHostedDreamGestureListener);
+        }
         mView.setLayoutInsetsController(mNotificationInsetsController);
         mView.setInteractionEventHandler(new NotificationShadeWindowView.InteractionEventHandler() {
             @Override
@@ -291,6 +301,10 @@
 
                 mFalsingCollector.onTouchEvent(ev);
                 mPulsingWakeupGestureHandler.onTouchEvent(ev);
+                if (mDreamingWakeupGestureHandler != null
+                        && mDreamingWakeupGestureHandler.onTouchEvent(ev)) {
+                    return true;
+                }
                 if (mStatusBarKeyguardViewManager.dispatchTouchEvent(ev)) {
                     return true;
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
index fba0120..5c1dd56 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
@@ -29,6 +29,8 @@
 import com.android.systemui.R
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.fragments.FragmentService
 import com.android.systemui.navigationbar.NavigationModeController
 import com.android.systemui.plugins.qs.QS
@@ -36,6 +38,7 @@
 import com.android.systemui.recents.OverviewProxyService
 import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener
 import com.android.systemui.shared.system.QuickStepContract
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.util.LargeScreenUtils
 import com.android.systemui.util.ViewController
 import com.android.systemui.util.concurrency.DelayableExecutor
@@ -54,7 +57,10 @@
         private val shadeHeaderController: ShadeHeaderController,
         private val shadeExpansionStateManager: ShadeExpansionStateManager,
         private val fragmentService: FragmentService,
-        @Main private val delayableExecutor: DelayableExecutor
+        @Main private val delayableExecutor: DelayableExecutor,
+        private val featureFlags: FeatureFlags,
+        private val
+            notificationStackScrollLayoutController: NotificationStackScrollLayoutController,
 ) : ViewController<NotificationsQuickSettingsContainer>(view), QSContainerController {
 
     private var qsExpanded = false
@@ -118,6 +124,9 @@
             isGestureNavigation = QuickStepContract.isGesturalMode(mode)
         }
         isGestureNavigation = QuickStepContract.isGesturalMode(currentMode)
+
+        mView.setStackScroller(notificationStackScrollLayoutController.getView())
+        mView.setMigratingNSSL(featureFlags.isEnabled(Flags.MIGRATE_NSSL))
     }
 
     public override fun onViewAttached() {
@@ -254,14 +263,17 @@
     }
 
     private fun setNotificationsConstraints(constraintSet: ConstraintSet) {
+        if (featureFlags.isEnabled(Flags.MIGRATE_NSSL)) {
+            return
+        }
         val startConstraintId = if (splitShadeEnabled) R.id.qs_edge_guideline else PARENT_ID
+        val nsslId = R.id.notification_stack_scroller
         constraintSet.apply {
-            connect(R.id.notification_stack_scroller, START, startConstraintId, START)
-            setMargin(R.id.notification_stack_scroller, START,
-                    if (splitShadeEnabled) 0 else panelMarginHorizontal)
-            setMargin(R.id.notification_stack_scroller, END, panelMarginHorizontal)
-            setMargin(R.id.notification_stack_scroller, TOP, topMargin)
-            setMargin(R.id.notification_stack_scroller, BOTTOM, notificationsBottomMargin)
+            connect(nsslId, START, startConstraintId, START)
+            setMargin(nsslId, START, if (splitShadeEnabled) 0 else panelMarginHorizontal)
+            setMargin(nsslId, END, panelMarginHorizontal)
+            setMargin(nsslId, TOP, topMargin)
+            setMargin(nsslId, BOTTOM, notificationsBottomMargin)
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java
index e5b84bd..3b3df50 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java
@@ -23,6 +23,7 @@
 import android.graphics.Rect;
 import android.util.AttributeSet;
 import android.view.View;
+import android.view.ViewGroup.MarginLayoutParams;
 import android.view.WindowInsets;
 
 import androidx.annotation.Nullable;
@@ -56,6 +57,7 @@
     private QS mQs;
     private View mQSContainer;
     private int mLastQSPaddingBottom;
+    private boolean mIsMigratingNSSL;
 
     /**
      *  These are used to compute the bounding box containing the shade and the notification scrim,
@@ -75,10 +77,13 @@
     protected void onFinishInflate() {
         super.onFinishInflate();
         mQsFrame = findViewById(R.id.qs_frame);
-        mStackScroller = findViewById(R.id.notification_stack_scroller);
         mKeyguardStatusBar = findViewById(R.id.keyguard_header);
     }
 
+    void setStackScroller(View stackScroller) {
+        mStackScroller = stackScroller;
+    }
+
     @Override
     public void onFragmentViewCreated(String tag, Fragment fragment) {
         mQs = (QS) fragment;
@@ -108,7 +113,7 @@
     }
 
     public void setNotificationsMarginBottom(int margin) {
-        LayoutParams params = (LayoutParams) mStackScroller.getLayoutParams();
+        MarginLayoutParams params = (MarginLayoutParams) mStackScroller.getLayoutParams();
         params.bottomMargin = margin;
         mStackScroller.setLayoutParams(params);
     }
@@ -173,8 +178,15 @@
         super.dispatchDraw(canvas);
     }
 
+    void setMigratingNSSL(boolean isMigrating) {
+        mIsMigratingNSSL = isMigrating;
+    }
+
     @Override
     protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
+        if (mIsMigratingNSSL) {
+            return super.drawChild(canvas, child, drawingTime);
+        }
         int layoutIndex = mLayoutDrawingOrder.indexOf(child);
         if (layoutIndex >= 0) {
             return super.drawChild(canvas, mDrawingOrderedChildren.get(layoutIndex), drawingTime);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
index 025c4611..baac57c 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
@@ -233,7 +233,6 @@
     private int mMaxExpansionHeight;
     /** Expansion fraction of the notification shade */
     private float mShadeExpandedFraction;
-    private int mPeekHeight;
     private float mLastOverscroll;
     private boolean mExpansionFromOverscroll;
     private boolean mExpansionEnabledPolicy = true;
@@ -429,7 +428,6 @@
         final ViewConfiguration configuration = ViewConfiguration.get(this.mPanelView.getContext());
         mTouchSlop = configuration.getScaledTouchSlop();
         mSlopMultiplier = configuration.getScaledAmbiguousGestureMultiplier();
-        mPeekHeight = mResources.getDimensionPixelSize(R.dimen.qs_peek_height);
         mStatusBarMinHeight = SystemBarUtils.getStatusBarHeight(mPanelView.getContext());
         mScrimCornerRadius = mResources.getDimensionPixelSize(
                 R.dimen.notification_scrim_corner_radius);
@@ -500,12 +498,7 @@
     }
 
     int getHeaderHeight() {
-        return mQs.getHeader().getHeight();
-    }
-
-    /** Returns the padding of the stackscroller when unlocked */
-    int getUnlockedStackScrollerPadding() {
-        return (mQs != null ? mQs.getHeader().getHeight() : 0) + mPeekHeight;
+        return isQsFragmentCreated() ? mQs.getHeader().getHeight() : 0;
     }
 
     private boolean isRemoteInputActiveWithKeyboardUp() {
@@ -2090,8 +2083,6 @@
         ipw.println(mMaxExpansionHeight);
         ipw.print("mShadeExpandedFraction=");
         ipw.println(mShadeExpandedFraction);
-        ipw.print("mPeekHeight=");
-        ipw.println(mPeekHeight);
         ipw.print("mLastOverscroll=");
         ipw.println(mLastOverscroll);
         ipw.print("mExpansionFromOverscroll=");
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
index 6e78357..be21df1 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
@@ -44,6 +44,7 @@
 import com.android.systemui.statusbar.notification.row.dagger.NotificationShelfComponent
 import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinderWrapperControllerImpl
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
+import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
 import com.android.systemui.statusbar.phone.KeyguardBottomAreaView
 import com.android.systemui.statusbar.phone.StatusBarLocation
 import com.android.systemui.statusbar.phone.StatusIconContainer
@@ -185,6 +186,14 @@
             return notificationShadeWindowView.findViewById(R.id.keyguard_root_view)
         }
 
+        @Provides
+        @SysUISingleton
+        fun providesSharedNotificationContainer(
+            notificationShadeWindowView: NotificationShadeWindowView,
+        ): SharedNotificationContainer {
+            return notificationShadeWindowView.findViewById(R.id.shared_notification_container)
+        }
+
         // TODO(b/277762009): Only allow this view's controller to inject the view. See above.
         @Provides
         @SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt
index 8d5c30b..2532bad 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt
@@ -18,6 +18,7 @@
 import android.view.ViewPropertyAnimator
 import com.android.systemui.statusbar.GestureRecorder
 import com.android.systemui.statusbar.NotificationShelfController
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.phone.CentralSurfaces
 import com.android.systemui.statusbar.phone.HeadsUpManagerPhone
 
@@ -63,6 +64,9 @@
     /** Animates the view from its current alpha to zero then runs the runnable. */
     fun fadeOut(startDelayMs: Long, durationMs: Long, endAction: Runnable): ViewPropertyAnimator
 
+    /** Returns the NSSL controller. */
+    val notificationStackScrollLayoutController: NotificationStackScrollLayoutController
+
     /** Set whether the bouncer is showing. */
     fun setBouncerShowing(bouncerShowing: Boolean)
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt
index 9aa5eb0..d5b5c87 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt
@@ -19,9 +19,7 @@
 import android.view.ViewGroup
 import android.view.ViewTreeObserver
 import com.android.systemui.keyguard.shared.model.WakefulnessModel
-import com.android.systemui.statusbar.RemoteInputController
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController
 import com.android.systemui.statusbar.phone.KeyguardStatusBarView
 import com.android.systemui.statusbar.phone.KeyguardStatusBarViewController
@@ -141,9 +139,6 @@
     /** Returns the StatusBarState. */
     val barState: Int
 
-    /** Returns the NSSL controller. */
-    val notificationStackScrollLayoutController: NotificationStackScrollLayoutController
-
     /** Sets the amount of progress in the status bar launch animation. */
     fun applyLaunchAnimationProgress(linearProgress: Float)
 
@@ -261,9 +256,6 @@
     /** Returns the ShadeFoldAnimator. */
     val shadeFoldAnimator: ShadeFoldAnimator
 
-    /** Returns the ShadeNotificationPresenter. */
-    val shadeNotificationPresenter: ShadeNotificationPresenter
-
     companion object {
         /**
          * Returns a multiplicative factor to use when determining the falsing threshold for touches
@@ -325,16 +317,7 @@
     fun cancelFoldToAodAnimation()
 
     /** Returns the main view of the shade. */
-    val view: ViewGroup
-}
-
-/** Handles the shade's interactions with StatusBarNotificationPresenter. */
-interface ShadeNotificationPresenter {
-    /** Returns a new delegate for some view controller pieces of the remote input process. */
-    fun createRemoteInputDelegate(): RemoteInputController.Delegate
-
-    /** Returns whether the screen has temporarily woken up to display notifications. */
-    fun hasPulsingNotifications(): Boolean
+    val view: ViewGroup?
 }
 
 /**
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt
new file mode 100644
index 0000000..287ac52
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt
@@ -0,0 +1,111 @@
+/*
+ * 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.shade
+
+import android.view.MotionEvent
+import android.view.ViewGroup
+import android.view.ViewTreeObserver
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.phone.HeadsUpAppearanceController
+import java.util.function.Consumer
+import javax.inject.Inject
+
+/** Empty implementation of ShadeViewController for variants with no shade. */
+class ShadeViewControllerEmptyImpl @Inject constructor() : ShadeViewController {
+    override fun expand(animate: Boolean) {}
+    override fun expandToQs() {}
+    override fun expandToNotifications() {}
+    override val isExpandingOrCollapsing: Boolean = false
+    override val isExpanded: Boolean = false
+    override val isPanelExpanded: Boolean = false
+    override val isShadeFullyExpanded: Boolean = false
+    override fun collapse(delayed: Boolean, speedUpFactor: Float) {}
+    override fun collapse(animate: Boolean, delayed: Boolean, speedUpFactor: Float) {}
+    override fun collapseWithDuration(animationDuration: Int) {}
+    override fun instantCollapse() {}
+    override fun animateCollapseQs(fullyCollapse: Boolean) {}
+    override fun canBeCollapsed(): Boolean = false
+    override val isCollapsing: Boolean = false
+    override val isFullyCollapsed: Boolean = false
+    override val isTracking: Boolean = false
+    override val isViewEnabled: Boolean = false
+    override fun setOpenCloseListener(openCloseListener: OpenCloseListener) {}
+    override fun shouldHideStatusBarIconsWhenExpanded() = false
+    override fun blockExpansionForCurrentTouch() {}
+    override fun setTrackingStartedListener(trackingStartedListener: TrackingStartedListener) {}
+    override fun disableHeader(state1: Int, state2: Int, animated: Boolean) {}
+    override fun startExpandLatencyTracking() {}
+    override fun startBouncerPreHideAnimation() {}
+    override fun dozeTimeTick() {}
+    override fun resetViews(animate: Boolean) {}
+    override val barState: Int = 0
+    override fun applyLaunchAnimationProgress(linearProgress: Float) {}
+    override fun closeUserSwitcherIfOpen(): Boolean {
+        return false
+    }
+    override fun onBackPressed() {}
+    override fun setIsLaunchAnimationRunning(running: Boolean) {}
+    override fun setAlpha(alpha: Int, animate: Boolean) {}
+    override fun setAlphaChangeAnimationEndAction(r: Runnable) {}
+    override fun setPulsing(pulsing: Boolean) {}
+    override fun setQsScrimEnabled(qsScrimEnabled: Boolean) {}
+    override fun setAmbientIndicationTop(ambientIndicationTop: Int, ambientTextVisible: Boolean) {}
+    override fun updateSystemUiStateFlags() {}
+    override fun updateTouchableRegion() {}
+    override fun addOnGlobalLayoutListener(listener: ViewTreeObserver.OnGlobalLayoutListener) {}
+    override fun removeOnGlobalLayoutListener(listener: ViewTreeObserver.OnGlobalLayoutListener) {}
+    override fun postToView(action: Runnable): Boolean {
+        return false
+    }
+    override fun transitionToExpandedShade(delay: Long) {}
+    override val isUnlockHintRunning: Boolean = false
+
+    override fun resetViewGroupFade() {}
+    override fun setKeyguardTransitionProgress(keyguardAlpha: Float, keyguardTranslationY: Int) {}
+    override fun setOverStretchAmount(amount: Float) {}
+    override fun setKeyguardStatusBarAlpha(alpha: Float) {}
+    override fun showAodUi() {}
+    override fun isFullyExpanded(): Boolean {
+        return false
+    }
+    override fun handleExternalTouch(event: MotionEvent): Boolean {
+        return false
+    }
+    override fun startTrackingExpansionFromStatusBar() {}
+    override val shadeHeadsUpTracker = ShadeHeadsUpTrackerEmptyImpl()
+    override val shadeFoldAnimator = ShadeFoldAnimatorEmptyImpl()
+}
+
+class ShadeHeadsUpTrackerEmptyImpl : ShadeHeadsUpTracker {
+    override fun addTrackingHeadsUpListener(listener: Consumer<ExpandableNotificationRow>) {}
+    override fun removeTrackingHeadsUpListener(listener: Consumer<ExpandableNotificationRow>) {}
+    override fun setHeadsUpAppearanceController(
+        headsUpAppearanceController: HeadsUpAppearanceController?
+    ) {}
+    override val trackedHeadsUpNotification: ExpandableNotificationRow? = null
+}
+
+class ShadeFoldAnimatorEmptyImpl : ShadeFoldAnimator {
+    override fun prepareFoldToAodAnimation() {}
+    override fun startFoldToAodAnimation(
+        startAction: Runnable,
+        endAction: Runnable,
+        cancelAction: Runnable,
+    ) {}
+    override fun cancelFoldToAodAnimation() {}
+    override val view: ViewGroup? = null
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LegacyNotificationShelfControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/LegacyNotificationShelfControllerImpl.java
index 4ec5f46..7a989cf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LegacyNotificationShelfControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LegacyNotificationShelfControllerImpl.java
@@ -19,7 +19,6 @@
 import android.view.View;
 
 import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationViewController;
 import com.android.systemui.statusbar.notification.row.dagger.NotificationRowScope;
 import com.android.systemui.statusbar.notification.stack.AmbientState;
@@ -52,7 +51,6 @@
         mActivatableNotificationViewController = activatableNotificationViewController;
         mKeyguardBypassController = keyguardBypassController;
         mStatusBarStateController = statusBarStateController;
-        mView.setSensitiveRevealAnimEnabled(featureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM));
         mOnAttachStateChangeListener = new View.OnAttachStateChangeListener() {
             @Override
             public void onViewAttachedToWindow(View v) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
index 47a4641..5ac542b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
@@ -112,9 +112,6 @@
     /** Sets the state of whether heads up is showing or not. */
     default void setHeadsUpShowing(boolean showing) {}
 
-    /** Sets whether the wallpaper supports ambient mode or not. */
-    default void setWallpaperSupportsAmbientMode(boolean supportsAmbientMode) {}
-
     /** Gets whether the wallpaper is showing or not. */
     default boolean isShowingWallpaper() {
         return false;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index 25a1dc6..3f37c60 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -24,7 +24,6 @@
 import android.graphics.Rect;
 import android.util.AttributeSet;
 import android.util.IndentingPrintWriter;
-import android.util.Log;
 import android.util.MathUtils;
 import android.view.View;
 import android.view.ViewGroup;
@@ -40,6 +39,8 @@
 import com.android.internal.policy.SystemBarUtils;
 import com.android.systemui.R;
 import com.android.systemui.animation.ShadeInterpolation;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.flags.ViewRefactorFlag;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator;
 import com.android.systemui.statusbar.notification.NotificationUtils;
@@ -95,8 +96,10 @@
     private float mCornerAnimationDistance;
     private NotificationShelfController mController;
     private float mActualWidth = -1;
-    private boolean mSensitiveRevealAnimEnabled;
-    private boolean mShelfRefactorFlagEnabled;
+    private final ViewRefactorFlag mSensitiveRevealAnim =
+            new ViewRefactorFlag(Flags.SENSITIVE_REVEAL_ANIM);
+    private final ViewRefactorFlag mShelfRefactor =
+            new ViewRefactorFlag(Flags.NOTIFICATION_SHELF_REFACTOR);
     private boolean mCanModifyColorOfNotifications;
     private boolean mCanInteract;
     private NotificationStackScrollLayout mHostLayout;
@@ -130,7 +133,7 @@
 
     public void bind(AmbientState ambientState,
                      NotificationStackScrollLayoutController hostLayoutController) {
-        assertRefactorFlagDisabled();
+        mShelfRefactor.assertDisabled();
         mAmbientState = ambientState;
         mHostLayoutController = hostLayoutController;
         hostLayoutController.setOnNotificationRemovedListener((child, isTransferInProgress) -> {
@@ -140,7 +143,7 @@
 
     public void bind(AmbientState ambientState, NotificationStackScrollLayout hostLayout,
             NotificationRoundnessManager roundnessManager) {
-        if (!checkRefactorFlagEnabled()) return;
+        if (!mShelfRefactor.expectEnabled()) return;
         mAmbientState = ambientState;
         mHostLayout = hostLayout;
         mRoundnessManager = roundnessManager;
@@ -268,7 +271,7 @@
         }
 
         final float stackEnd = ambientState.getStackY() + ambientState.getStackHeight();
-        if (mSensitiveRevealAnimEnabled && viewState.hidden) {
+        if (mSensitiveRevealAnim.isEnabled() && viewState.hidden) {
             // if the shelf is hidden, position it at the end of the stack (plus the clip
             // padding), such that when it appears animated, it will smoothly move in from the
             // bottom, without jump cutting any notifications
@@ -279,7 +282,7 @@
     }
 
     private int getSpeedBumpIndex() {
-        if (mShelfRefactorFlagEnabled) {
+        if (mShelfRefactor.isEnabled()) {
             return mHostLayout.getSpeedBumpIndex();
         } else {
             return mHostLayoutController.getSpeedBumpIndex();
@@ -413,7 +416,7 @@
                     expandingAnimated, isLastChild, shelfClipStart);
 
             // TODO(b/172289889) scale mPaddingBetweenElements with expansion amount
-            if ((!mSensitiveRevealAnimEnabled && ((isLastChild && !child.isInShelf())
+            if ((!mSensitiveRevealAnim.isEnabled() && ((isLastChild && !child.isInShelf())
                     || backgroundForceHidden)) || aboveShelf) {
                 notificationClipEnd = shelfStart + getIntrinsicHeight();
             } else {
@@ -462,7 +465,7 @@
                 // if the shelf is visible, but if the shelf is hidden, it causes incorrect curling.
                 // notificationClipEnd handles the discrepancy between a visible and hidden shelf,
                 // so we use that when on the keyguard (and while animating away) to reduce curling.
-                final float keyguardSafeShelfStart = !mSensitiveRevealAnimEnabled
+                final float keyguardSafeShelfStart = !mSensitiveRevealAnim.isEnabled()
                         && mAmbientState.isOnKeyguard() ? notificationClipEnd : shelfStart;
                 updateCornerRoundnessOnScroll(anv, viewStart, keyguardSafeShelfStart);
             }
@@ -504,7 +507,7 @@
     }
 
     private ExpandableView getHostLayoutChildAt(int index) {
-        if (mShelfRefactorFlagEnabled) {
+        if (mShelfRefactor.isEnabled()) {
             return (ExpandableView) mHostLayout.getChildAt(index);
         } else {
             return mHostLayoutController.getChildAt(index);
@@ -512,7 +515,7 @@
     }
 
     private int getHostLayoutChildCount() {
-        if (mShelfRefactorFlagEnabled) {
+        if (mShelfRefactor.isEnabled()) {
             return mHostLayout.getChildCount();
         } else {
             return mHostLayoutController.getChildCount();
@@ -520,7 +523,7 @@
     }
 
     private boolean canModifyColorOfNotifications() {
-        if (mShelfRefactorFlagEnabled) {
+        if (mShelfRefactor.isEnabled()) {
             return mCanModifyColorOfNotifications && mAmbientState.isShadeExpanded();
         } else {
             return mController.canModifyColorOfNotifications();
@@ -583,7 +586,7 @@
     }
 
     private boolean isViewAffectedBySwipe(ExpandableView expandableView) {
-        if (!mShelfRefactorFlagEnabled) {
+        if (!mShelfRefactor.isEnabled()) {
             return mHostLayoutController.isViewAffectedBySwipe(expandableView);
         } else {
             return mRoundnessManager.isViewAffectedBySwipe(expandableView);
@@ -607,7 +610,7 @@
     }
 
     private View getHostLayoutTransientView(int index) {
-        if (mShelfRefactorFlagEnabled) {
+        if (mShelfRefactor.isEnabled()) {
             return mHostLayout.getTransientView(index);
         } else {
             return mHostLayoutController.getTransientView(index);
@@ -615,7 +618,7 @@
     }
 
     private int getHostLayoutTransientViewCount() {
-        if (mShelfRefactorFlagEnabled) {
+        if (mShelfRefactor.isEnabled()) {
             return mHostLayout.getTransientViewCount();
         } else {
             return mHostLayoutController.getTransientViewCount();
@@ -961,7 +964,7 @@
 
     @Override
     public void onStateChanged(int newState) {
-        assertRefactorFlagDisabled();
+        mShelfRefactor.assertDisabled();
         mStatusBarState = newState;
         updateInteractiveness();
     }
@@ -975,7 +978,7 @@
     }
 
     private boolean canInteract() {
-        if (mShelfRefactorFlagEnabled) {
+        if (mShelfRefactor.isEnabled()) {
             return mCanInteract;
         } else {
             return mStatusBarState == StatusBarState.KEYGUARD;
@@ -1018,32 +1021,18 @@
         return false;
     }
 
-    private void assertRefactorFlagDisabled() {
-        if (mShelfRefactorFlagEnabled) {
-            NotificationShelfController.throwIllegalFlagStateError(false);
-        }
-    }
-
-    private boolean checkRefactorFlagEnabled() {
-        if (!mShelfRefactorFlagEnabled) {
-            Log.wtf(TAG,
-                    "Code path not supported when Flags.NOTIFICATION_SHELF_REFACTOR is disabled.");
-        }
-        return mShelfRefactorFlagEnabled;
-    }
-
     public void setController(NotificationShelfController notificationShelfController) {
-        assertRefactorFlagDisabled();
+        mShelfRefactor.assertDisabled();
         mController = notificationShelfController;
     }
 
     public void setCanModifyColorOfNotifications(boolean canModifyColorOfNotifications) {
-        if (!checkRefactorFlagEnabled()) return;
+        if (!mShelfRefactor.expectEnabled()) return;
         mCanModifyColorOfNotifications = canModifyColorOfNotifications;
     }
 
     public void setCanInteract(boolean canInteract) {
-        if (!checkRefactorFlagEnabled()) return;
+        if (!mShelfRefactor.expectEnabled()) return;
         mCanInteract = canInteract;
         updateInteractiveness();
     }
@@ -1053,27 +1042,15 @@
     }
 
     private int getIndexOfViewInHostLayout(ExpandableView child) {
-        if (mShelfRefactorFlagEnabled) {
+        if (mShelfRefactor.isEnabled()) {
             return mHostLayout.indexOfChild(child);
         } else {
             return mHostLayoutController.indexOfChild(child);
         }
     }
 
-    /**
-     * Set whether the sensitive reveal animation feature flag is enabled
-     * @param enabled true if enabled
-     */
-    public void setSensitiveRevealAnimEnabled(boolean enabled) {
-        mSensitiveRevealAnimEnabled = enabled;
-    }
-
-    public void setRefactorFlagEnabled(boolean enabled) {
-        mShelfRefactorFlagEnabled = enabled;
-    }
-
     public void requestRoundnessResetFor(ExpandableView child) {
-        if (!checkRefactorFlagEnabled()) return;
+        if (!mShelfRefactor.expectEnabled()) return;
         child.requestRoundnessReset(SHELF_SCROLL);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.kt
index 1619dda..8a3e217 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.kt
@@ -16,11 +16,8 @@
 
 package com.android.systemui.statusbar
 
-import android.util.Log
 import android.view.View
 import android.view.View.OnClickListener
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.statusbar.notification.row.ExpandableView
 import com.android.systemui.statusbar.notification.stack.AmbientState
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
@@ -49,29 +46,4 @@
 
     /** @see View.setOnClickListener */
     fun setOnClickListener(listener: OnClickListener)
-
-    companion object {
-        @JvmStatic
-        fun assertRefactorFlagDisabled(featureFlags: FeatureFlags) {
-            if (featureFlags.isEnabled(Flags.NOTIFICATION_SHELF_REFACTOR)) {
-                throwIllegalFlagStateError(expected = false)
-            }
-        }
-
-        @JvmStatic
-        fun checkRefactorFlagEnabled(featureFlags: FeatureFlags): Boolean =
-            featureFlags.isEnabled(Flags.NOTIFICATION_SHELF_REFACTOR).also { enabled ->
-                if (!enabled) {
-                    Log.wtf("NotifShelf", getErrorMessage(expected = true))
-                }
-            }
-
-        @JvmStatic
-        fun throwIllegalFlagStateError(expected: Boolean): Nothing =
-            error(getErrorMessage(expected))
-
-        private fun getErrorMessage(expected: Boolean): String =
-            "Code path not supported when Flags.NOTIFICATION_SHELF_REFACTOR is " +
-                if (expected) "disabled" else "enabled"
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
index 1cf9c1e..1c5aa3c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
@@ -3,10 +3,10 @@
 import android.util.FloatProperty
 import android.view.View
 import androidx.annotation.FloatRange
-import com.android.systemui.Dependency
 import com.android.systemui.R
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
+import com.android.systemui.flags.ViewRefactorFlag
 import com.android.systemui.statusbar.notification.stack.AnimationProperties
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator
 import kotlin.math.abs
@@ -46,14 +46,14 @@
     @JvmDefault
     val topCornerRadius: Float
         get() =
-            if (roundableState.newHeadsUpAnimFlagEnabled) roundableState.topCornerRadius
+            if (roundableState.newHeadsUpAnim.isEnabled) roundableState.topCornerRadius
             else topRoundness * maxRadius
 
     /** Current bottom corner in pixel, based on [bottomRoundness] and [maxRadius] */
     @JvmDefault
     val bottomCornerRadius: Float
         get() =
-            if (roundableState.newHeadsUpAnimFlagEnabled) roundableState.bottomCornerRadius
+            if (roundableState.newHeadsUpAnim.isEnabled) roundableState.bottomCornerRadius
             else bottomRoundness * maxRadius
 
     /** Get and update the current radii */
@@ -335,13 +335,12 @@
     internal val targetView: View,
     private val roundable: Roundable,
     maxRadius: Float,
-    private val featureFlags: FeatureFlags = Dependency.get(FeatureFlags::class.java)
+    featureFlags: FeatureFlags? = null
 ) {
     internal var maxRadius = maxRadius
         private set
 
-    internal val newHeadsUpAnimFlagEnabled
-        get() = featureFlags.isEnabled(Flags.IMPROVED_HUN_ANIMATIONS)
+    internal val newHeadsUpAnim = ViewRefactorFlag(featureFlags, Flags.IMPROVED_HUN_ANIMATIONS)
 
     /** Animatable for top roundness */
     private val topAnimatable = topAnimatable(roundable)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DreamCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DreamCoordinator.kt
new file mode 100644
index 0000000..d268e35
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DreamCoordinator.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.collection.coordinator
+
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.SysuiStatusBarStateController
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * Filter out notifications on the lockscreen if the lockscreen hosted dream is active. If the user
+ * stops dreaming, pulls the shade down or unlocks the device, then the notifications are unhidden.
+ */
+@CoordinatorScope
+class DreamCoordinator
+@Inject
+constructor(
+    private val statusBarStateController: SysuiStatusBarStateController,
+    @Application private val scope: CoroutineScope,
+    private val keyguardRepository: KeyguardRepository,
+) : Coordinator {
+    private var isOnKeyguard = false
+    private var isLockscreenHostedDream = false
+
+    override fun attach(pipeline: NotifPipeline) {
+        pipeline.addPreGroupFilter(filter)
+        statusBarStateController.addCallback(statusBarStateListener)
+        scope.launch { attachFilterOnDreamingStateChange() }
+        recordStatusBarState(statusBarStateController.state)
+    }
+
+    private val filter =
+        object : NotifFilter("LockscreenHostedDreamFilter") {
+            var isFiltering = false
+            override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean {
+                return isFiltering
+            }
+            inline fun update(msg: () -> String) {
+                val wasFiltering = isFiltering
+                isFiltering = isLockscreenHostedDream && isOnKeyguard
+                if (wasFiltering != isFiltering) {
+                    invalidateList(msg())
+                }
+            }
+        }
+
+    private val statusBarStateListener =
+        object : StatusBarStateController.StateListener {
+            override fun onStateChanged(newState: Int) {
+                recordStatusBarState(newState)
+            }
+        }
+
+    private suspend fun attachFilterOnDreamingStateChange() {
+        keyguardRepository.isActiveDreamLockscreenHosted.collect { isDreaming ->
+            recordDreamingState(isDreaming)
+        }
+    }
+
+    private fun recordStatusBarState(newState: Int) {
+        isOnKeyguard = newState == StatusBarState.KEYGUARD
+        filter.update { "recordStatusBarState: " + StatusBarState.toString(newState) }
+    }
+
+    private fun recordDreamingState(isDreaming: Boolean) {
+        isLockscreenHostedDream = isDreaming
+        filter.update { "recordLockscreenHostedDreamState: $isDreaming" }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
index e5953cf..0ccab9e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
@@ -15,6 +15,8 @@
  */
 package com.android.systemui.statusbar.notification.collection.coordinator
 
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED
 import com.android.systemui.statusbar.notification.collection.NotifPipeline
 import com.android.systemui.statusbar.notification.collection.PipelineDumpable
 import com.android.systemui.statusbar.notification.collection.PipelineDumper
@@ -31,32 +33,34 @@
 
 @CoordinatorScope
 class NotifCoordinatorsImpl @Inject constructor(
-        sectionStyleProvider: SectionStyleProvider,
-        dataStoreCoordinator: DataStoreCoordinator,
-        hideLocallyDismissedNotifsCoordinator: HideLocallyDismissedNotifsCoordinator,
-        hideNotifsForOtherUsersCoordinator: HideNotifsForOtherUsersCoordinator,
-        keyguardCoordinator: KeyguardCoordinator,
-        rankingCoordinator: RankingCoordinator,
-        appOpsCoordinator: AppOpsCoordinator,
-        deviceProvisionedCoordinator: DeviceProvisionedCoordinator,
-        bubbleCoordinator: BubbleCoordinator,
-        headsUpCoordinator: HeadsUpCoordinator,
-        gutsCoordinator: GutsCoordinator,
-        conversationCoordinator: ConversationCoordinator,
-        debugModeCoordinator: DebugModeCoordinator,
-        groupCountCoordinator: GroupCountCoordinator,
-        groupWhenCoordinator: GroupWhenCoordinator,
-        mediaCoordinator: MediaCoordinator,
-        preparationCoordinator: PreparationCoordinator,
-        remoteInputCoordinator: RemoteInputCoordinator,
-        rowAppearanceCoordinator: RowAppearanceCoordinator,
-        stackCoordinator: StackCoordinator,
-        shadeEventCoordinator: ShadeEventCoordinator,
-        smartspaceDedupingCoordinator: SmartspaceDedupingCoordinator,
-        viewConfigCoordinator: ViewConfigCoordinator,
-        visualStabilityCoordinator: VisualStabilityCoordinator,
-        sensitiveContentCoordinator: SensitiveContentCoordinator,
-        dismissibilityCoordinator: DismissibilityCoordinator,
+    sectionStyleProvider: SectionStyleProvider,
+    featureFlags: FeatureFlags,
+    dataStoreCoordinator: DataStoreCoordinator,
+    hideLocallyDismissedNotifsCoordinator: HideLocallyDismissedNotifsCoordinator,
+    hideNotifsForOtherUsersCoordinator: HideNotifsForOtherUsersCoordinator,
+    keyguardCoordinator: KeyguardCoordinator,
+    rankingCoordinator: RankingCoordinator,
+    appOpsCoordinator: AppOpsCoordinator,
+    deviceProvisionedCoordinator: DeviceProvisionedCoordinator,
+    bubbleCoordinator: BubbleCoordinator,
+    headsUpCoordinator: HeadsUpCoordinator,
+    gutsCoordinator: GutsCoordinator,
+    conversationCoordinator: ConversationCoordinator,
+    debugModeCoordinator: DebugModeCoordinator,
+    groupCountCoordinator: GroupCountCoordinator,
+    groupWhenCoordinator: GroupWhenCoordinator,
+    mediaCoordinator: MediaCoordinator,
+    preparationCoordinator: PreparationCoordinator,
+    remoteInputCoordinator: RemoteInputCoordinator,
+    rowAppearanceCoordinator: RowAppearanceCoordinator,
+    stackCoordinator: StackCoordinator,
+    shadeEventCoordinator: ShadeEventCoordinator,
+    smartspaceDedupingCoordinator: SmartspaceDedupingCoordinator,
+    viewConfigCoordinator: ViewConfigCoordinator,
+    visualStabilityCoordinator: VisualStabilityCoordinator,
+    sensitiveContentCoordinator: SensitiveContentCoordinator,
+    dismissibilityCoordinator: DismissibilityCoordinator,
+    dreamCoordinator: DreamCoordinator,
 ) : NotifCoordinators {
 
     private val mCoreCoordinators: MutableList<CoreCoordinator> = ArrayList()
@@ -96,6 +100,10 @@
         mCoordinators.add(remoteInputCoordinator)
         mCoordinators.add(dismissibilityCoordinator)
 
+        if (featureFlags.isEnabled(LOCKSCREEN_WALLPAPER_DREAM_ENABLED)) {
+            mCoordinators.add(dreamCoordinator)
+        }
+
         // Manually add Ordered Sections
         mOrderedSections.add(headsUpCoordinator.sectioner) // HeadsUp
         mOrderedSections.add(appOpsCoordinator.sectioner) // ForegroundService
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
index 1896080..9ecf50e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
@@ -16,7 +16,6 @@
 
 package com.android.systemui.statusbar.notification.collection.inflation;
 
-import static com.android.systemui.flags.Flags.NOTIFICATION_INLINE_REPLY_ANIMATION;
 import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED;
 import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED;
 import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC;
@@ -176,8 +175,6 @@
         entry.setRow(row);
         mNotifBindPipeline.manageRow(entry, row);
         mPresenter.onBindRow(row);
-        row.setInlineReplyAnimationFlagEnabled(
-                mFeatureFlags.isEnabled(NOTIFICATION_INLINE_REPLY_ANIMATION));
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
index 908c11a..36a8e98 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
@@ -566,7 +566,7 @@
 
     @Override
     public float getTopCornerRadius() {
-        if (isNewHeadsUpAnimFlagEnabled()) {
+        if (mImprovedHunAnimation.isEnabled()) {
             return super.getTopCornerRadius();
         }
 
@@ -576,7 +576,7 @@
 
     @Override
     public float getBottomCornerRadius() {
-        if (isNewHeadsUpAnimFlagEnabled()) {
+        if (mImprovedHunAnimation.isEnabled()) {
             return super.getBottomCornerRadius();
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index b34c281..42b99a1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -77,6 +77,7 @@
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
+import com.android.systemui.flags.ViewRefactorFlag;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
@@ -275,7 +276,8 @@
     private OnExpandClickListener mOnExpandClickListener;
     private View.OnClickListener mOnFeedbackClickListener;
     private Path mExpandingClipPath;
-    private boolean mIsInlineReplyAnimationFlagEnabled = false;
+    private final ViewRefactorFlag mInlineReplyAnimation =
+            new ViewRefactorFlag(Flags.NOTIFICATION_INLINE_REPLY_ANIMATION);
 
     // Listener will be called when receiving a long click event.
     // Use #setLongPressPosition to optionally assign positional data with the long press.
@@ -3121,10 +3123,6 @@
         return showingLayout != null && showingLayout.requireRowToHaveOverlappingRendering();
     }
 
-    public void setInlineReplyAnimationFlagEnabled(boolean isEnabled) {
-        mIsInlineReplyAnimationFlagEnabled = isEnabled;
-    }
-
     @Override
     public void setActualHeight(int height, boolean notifyListeners) {
         boolean changed = height != getActualHeight();
@@ -3144,7 +3142,7 @@
         }
         int contentHeight = Math.max(getMinHeight(), height);
         for (NotificationContentView l : mLayouts) {
-            if (mIsInlineReplyAnimationFlagEnabled) {
+            if (mInlineReplyAnimation.isEnabled()) {
                 l.setContentHeight(height);
             } else {
                 l.setContentHeight(contentHeight);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
index 7f23c1b..c8f13a6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
@@ -28,10 +28,9 @@
 import android.view.View;
 import android.view.ViewOutlineProvider;
 
-import com.android.systemui.Dependency;
 import com.android.systemui.R;
-import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
+import com.android.systemui.flags.ViewRefactorFlag;
 import com.android.systemui.statusbar.notification.RoundableState;
 import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer;
 import com.android.systemui.util.DumpUtilsKt;
@@ -50,7 +49,8 @@
     private float mOutlineAlpha = -1f;
     private boolean mAlwaysRoundBothCorners;
     private Path mTmpPath = new Path();
-    private final FeatureFlags mFeatureFlags;
+    protected final ViewRefactorFlag mImprovedHunAnimation =
+            new ViewRefactorFlag(Flags.IMPROVED_HUN_ANIMATIONS);
 
     /**
      * {@code false} if the children views of the {@link ExpandableOutlineView} are translated when
@@ -126,7 +126,7 @@
             return EMPTY_PATH;
         }
         float bottomRadius = mAlwaysRoundBothCorners ? getMaxRadius() : getBottomCornerRadius();
-        if (!isNewHeadsUpAnimFlagEnabled() && (topRadius + bottomRadius > height)) {
+        if (!mImprovedHunAnimation.isEnabled() && (topRadius + bottomRadius > height)) {
             float overShoot = topRadius + bottomRadius - height;
             float currentTopRoundness = getTopRoundness();
             float currentBottomRoundness = getBottomRoundness();
@@ -167,7 +167,6 @@
         super(context, attrs);
         setOutlineProvider(mProvider);
         initDimens();
-        mFeatureFlags = Dependency.get(FeatureFlags.class);
     }
 
     @Override
@@ -376,8 +375,4 @@
         });
     }
 
-    // TODO(b/290365128) replace with ViewRefactorFlag
-    protected boolean isNewHeadsUpAnimFlagEnabled() {
-        return mFeatureFlags.isEnabled(Flags.IMPROVED_HUN_ANIMATIONS);
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java
index b2a3780..867e08b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java
@@ -18,6 +18,7 @@
 
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 
 import dagger.Binds;
 import dagger.Module;
@@ -58,9 +59,13 @@
     @ElementsIntoSet
     @Named(NOTIF_REMOTEVIEWS_FACTORIES)
     static Set<NotifRemoteViewsFactory> provideNotifRemoteViewsFactories(
-            FeatureFlags featureFlags
+            FeatureFlags featureFlags,
+            PrecomputedTextViewFactory precomputedTextViewFactory
     ) {
         final Set<NotifRemoteViewsFactory> replacementFactories = new HashSet<>();
+        if (featureFlags.isEnabled(Flags.PRECOMPUTED_TEXT)) {
+            replacementFactories.add(precomputedTextViewFactory);
+        }
         return replacementFactories;
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedImageFloatingTextView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedImageFloatingTextView.kt
new file mode 100644
index 0000000..57c82dc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedImageFloatingTextView.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.row
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.RemoteViews
+import com.android.internal.widget.ImageFloatingTextView
+
+/** Precomputed version of [ImageFloatingTextView] */
+@RemoteViews.RemoteView
+class PrecomputedImageFloatingTextView
+@JvmOverloads
+constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
+    ImageFloatingTextView(context, attrs, defStyleAttr), TextPrecomputer {
+
+    override fun setTextAsync(text: CharSequence?): Runnable = precompute(this, text)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedTextView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedTextView.kt
new file mode 100644
index 0000000..8508b1f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedTextView.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.row
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.RemoteViews
+import android.widget.TextView
+
+/**
+ * A user interface element that uses the PrecomputedText API to display text in a notification,
+ * with the help of RemoteViews.
+ */
+@RemoteViews.RemoteView
+class PrecomputedTextView
+@JvmOverloads
+constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
+    TextView(context, attrs, defStyleAttr), TextPrecomputer {
+
+    override fun setTextAsync(text: CharSequence?): Runnable = precompute(this, text)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedTextViewFactory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedTextViewFactory.kt
new file mode 100644
index 0000000..b002330
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedTextViewFactory.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.row
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import android.widget.TextView
+import com.android.internal.widget.ImageFloatingTextView
+import javax.inject.Inject
+
+class PrecomputedTextViewFactory @Inject constructor() : NotifRemoteViewsFactory {
+    override fun instantiate(
+        parent: View?,
+        name: String,
+        context: Context,
+        attrs: AttributeSet
+    ): View? {
+        return when (name) {
+            TextView::class.java.name,
+            TextView::class.java.simpleName -> PrecomputedTextView(context, attrs)
+            ImageFloatingTextView::class.java.name ->
+                PrecomputedImageFloatingTextView(context, attrs)
+            else -> null
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/TextPrecomputer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/TextPrecomputer.kt
new file mode 100644
index 0000000..49f4a33
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/TextPrecomputer.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.row
+
+import android.text.PrecomputedText
+import android.text.Spannable
+import android.util.Log
+import android.widget.TextView
+
+internal interface TextPrecomputer {
+    /**
+     * Creates PrecomputedText from given text and returns a runnable which sets precomputed text to
+     * the textview on main thread.
+     *
+     * @param text text to be converted to PrecomputedText
+     * @return Runnable that sets precomputed text on the main thread
+     */
+    fun precompute(
+        textView: TextView,
+        text: CharSequence?,
+        logException: Boolean = true
+    ): Runnable {
+        val precomputedText: Spannable? =
+            text?.let { PrecomputedText.create(it, textView.textMetricsParams) }
+
+        return Runnable {
+            try {
+                textView.text = precomputedText
+            } catch (exception: IllegalArgumentException) {
+                if (logException) {
+                    Log.wtf(
+                        /* tag = */ TAG,
+                        /* msg = */ "PrecomputedText setText failed for TextView:$textView",
+                        /* tr = */ exception
+                    )
+                }
+                textView.text = text
+            }
+        }
+    }
+
+    private companion object {
+        private const val TAG = "TextPrecomputer"
+    }
+}
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 23a58d2..22a87a7 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
@@ -21,7 +21,6 @@
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.statusbar.LegacyNotificationShelfControllerImpl
@@ -66,7 +65,7 @@
     override fun setOnClickListener(listener: View.OnClickListener) = unsupported
 
     private val unsupported: Nothing
-        get() = NotificationShelfController.throwIllegalFlagStateError(expected = true)
+        get() = error("Code path not supported when NOTIFICATION_SHELF_REFACTOR is disabled")
 }
 
 /** Binds a [NotificationShelf] to its [view model][NotificationShelfViewModel]. */
@@ -80,8 +79,6 @@
     ) {
         ActivatableNotificationViewBinder.bind(viewModel, shelf, falsingManager)
         shelf.apply {
-            setRefactorFlagEnabled(true)
-            setSensitiveRevealAnimEnabled(featureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM))
             // TODO(278765923): Replace with eventual NotificationIconContainerViewBinder#bind()
             notificationIconAreaController.setShelfIcons(shelfIcons)
             repeatWhenAttached {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index b0f3f59..95e74f2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -29,7 +29,6 @@
 import com.android.systemui.R;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator;
 import com.android.systemui.statusbar.NotificationShelf;
 import com.android.systemui.statusbar.StatusBarState;
@@ -57,7 +56,6 @@
     private final SectionProvider mSectionProvider;
     private final BypassController mBypassController;
     private final LargeScreenShadeInterpolator mLargeScreenShadeInterpolator;
-    private final FeatureFlags mFeatureFlags;
     /**
      *  Used to read bouncer states.
      */
@@ -261,13 +259,12 @@
             @NonNull SectionProvider sectionProvider,
             @NonNull BypassController bypassController,
             @Nullable StatusBarKeyguardViewManager statusBarKeyguardViewManager,
-            @NonNull LargeScreenShadeInterpolator largeScreenShadeInterpolator,
-            @NonNull FeatureFlags featureFlags) {
+            @NonNull LargeScreenShadeInterpolator largeScreenShadeInterpolator
+    ) {
         mSectionProvider = sectionProvider;
         mBypassController = bypassController;
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
         mLargeScreenShadeInterpolator = largeScreenShadeInterpolator;
-        mFeatureFlags = featureFlags;
         reload(context);
         dumpManager.registerDumpable(this);
     }
@@ -753,10 +750,6 @@
         return mLargeScreenShadeInterpolator;
     }
 
-    public FeatureFlags getFeatureFlags() {
-        return mFeatureFlags;
-    }
-
     @Override
     public void dump(PrintWriter pw, String[] args) {
         pw.println("mTopPadding=" + mTopPadding);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index c1ceb3c..d71bc2f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -89,6 +89,7 @@
 import com.android.systemui.R;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
+import com.android.systemui.flags.ViewRefactorFlag;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
 import com.android.systemui.shade.ShadeController;
@@ -198,7 +199,8 @@
     private Set<Integer> mDebugTextUsedYPositions;
     private final boolean mDebugRemoveAnimation;
     private final boolean mSensitiveRevealAnimEndabled;
-    private boolean mAnimatedInsets;
+    private final ViewRefactorFlag mAnimatedInsets;
+    private final ViewRefactorFlag mShelfRefactor;
 
     private int mContentHeight;
     private float mIntrinsicContentHeight;
@@ -621,7 +623,9 @@
         mDebugLines = featureFlags.isEnabled(Flags.NSSL_DEBUG_LINES);
         mDebugRemoveAnimation = featureFlags.isEnabled(Flags.NSSL_DEBUG_REMOVE_ANIMATION);
         mSensitiveRevealAnimEndabled = featureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM);
-        setAnimatedInsetsEnabled(featureFlags.isEnabled(Flags.ANIMATED_NOTIFICATION_SHADE_INSETS));
+        mAnimatedInsets =
+                new ViewRefactorFlag(featureFlags, Flags.ANIMATED_NOTIFICATION_SHADE_INSETS);
+        mShelfRefactor = new ViewRefactorFlag(featureFlags, Flags.NOTIFICATION_SHELF_REFACTOR);
         mSectionsManager = Dependency.get(NotificationSectionsManager.class);
         mScreenOffAnimationController =
                 Dependency.get(ScreenOffAnimationController.class);
@@ -660,7 +664,7 @@
         mGroupMembershipManager = Dependency.get(GroupMembershipManager.class);
         mGroupExpansionManager = Dependency.get(GroupExpansionManager.class);
         setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
-        if (mAnimatedInsets) {
+        if (mAnimatedInsets.isEnabled()) {
             setWindowInsetsAnimationCallback(mInsetsCallback);
         }
     }
@@ -730,11 +734,6 @@
     }
 
     @VisibleForTesting
-    void setAnimatedInsetsEnabled(boolean enabled) {
-        mAnimatedInsets = enabled;
-    }
-
-    @VisibleForTesting
     public void updateFooter() {
         if (mFooterView == null) {
             return;
@@ -1773,7 +1772,7 @@
             return;
         }
         mForcedScroll = v;
-        if (mAnimatedInsets) {
+        if (mAnimatedInsets.isEnabled()) {
             updateForcedScroll();
         } else {
             scrollTo(v);
@@ -1822,7 +1821,7 @@
 
     @Override
     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
-        if (!mAnimatedInsets) {
+        if (!mAnimatedInsets.isEnabled()) {
             mBottomInset = insets.getInsets(WindowInsets.Type.ime()).bottom;
         }
         mWaterfallTopInset = 0;
@@ -1830,11 +1829,11 @@
         if (cutout != null) {
             mWaterfallTopInset = cutout.getWaterfallInsets().top;
         }
-        if (mAnimatedInsets && !mIsInsetAnimationRunning) {
+        if (mAnimatedInsets.isEnabled() && !mIsInsetAnimationRunning) {
             // update bottom inset e.g. after rotation
             updateBottomInset(insets);
         }
-        if (!mAnimatedInsets) {
+        if (!mAnimatedInsets.isEnabled()) {
             int range = getScrollRange();
             if (mOwnScrollY > range) {
                 // HACK: We're repeatedly getting staggered insets here while the IME is
@@ -2714,7 +2713,7 @@
      * @param listener callback for notification removed
      */
     public void setOnNotificationRemovedListener(OnNotificationRemovedListener listener) {
-        NotificationShelfController.assertRefactorFlagDisabled(mAmbientState.getFeatureFlags());
+        mShelfRefactor.assertDisabled();
         mOnNotificationRemovedListener = listener;
     }
 
@@ -2727,7 +2726,7 @@
         if (!mChildTransferInProgress) {
             onViewRemovedInternal(expandableView, this);
         }
-        if (mAmbientState.getFeatureFlags().isEnabled(Flags.NOTIFICATION_SHELF_REFACTOR)) {
+        if (mShelfRefactor.isEnabled()) {
             mShelf.requestRoundnessResetFor(expandableView);
         } else {
             if (mOnNotificationRemovedListener != null) {
@@ -4943,18 +4942,12 @@
 
     @Nullable
     public ExpandableView getShelf() {
-        if (NotificationShelfController.checkRefactorFlagEnabled(mAmbientState.getFeatureFlags())) {
-            return mShelf;
-        } else {
-            return null;
-        }
+        if (!mShelfRefactor.expectEnabled()) return null;
+        return mShelf;
     }
 
     public void setShelf(NotificationShelf shelf) {
-        if (!NotificationShelfController.checkRefactorFlagEnabled(
-                mAmbientState.getFeatureFlags())) {
-            return;
-        }
+        if (!mShelfRefactor.expectEnabled()) return;
         int index = -1;
         if (mShelf != null) {
             index = indexOfChild(mShelf);
@@ -4968,7 +4961,7 @@
     }
 
     public void setShelfController(NotificationShelfController notificationShelfController) {
-        NotificationShelfController.assertRefactorFlagDisabled(mAmbientState.getFeatureFlags());
+        mShelfRefactor.assertDisabled();
         int index = -1;
         if (mShelf != null) {
             index = indexOfChild(mShelf);
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 ef7375a..4668aa4 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
@@ -65,6 +65,7 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
+import com.android.systemui.flags.ViewRefactorFlag;
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.shared.model.KeyguardState;
@@ -205,6 +206,7 @@
     private boolean mIsInTransitionToAod = false;
 
     private final FeatureFlags mFeatureFlags;
+    private final ViewRefactorFlag mShelfRefactor;
     private final NotificationTargetsHelper mNotificationTargetsHelper;
     private final SecureSettings mSecureSettings;
     private final NotificationDismissibilityProvider mDismissibilityProvider;
@@ -718,6 +720,7 @@
         mShadeController = shadeController;
         mNotifIconAreaController = notifIconAreaController;
         mFeatureFlags = featureFlags;
+        mShelfRefactor = new ViewRefactorFlag(featureFlags, Flags.NOTIFICATION_SHELF_REFACTOR);
         mNotificationTargetsHelper = notificationTargetsHelper;
         mSecureSettings = secureSettings;
         mDismissibilityProvider = dismissibilityProvider;
@@ -1432,7 +1435,7 @@
     }
 
     public void setShelfController(NotificationShelfController notificationShelfController) {
-        NotificationShelfController.assertRefactorFlagDisabled(mFeatureFlags);
+        mShelfRefactor.assertDisabled();
         mView.setShelfController(notificationShelfController);
     }
 
@@ -1645,12 +1648,12 @@
     }
 
     public void setShelf(NotificationShelf shelf) {
-        if (!NotificationShelfController.checkRefactorFlagEnabled(mFeatureFlags)) return;
+        if (!mShelfRefactor.expectEnabled()) return;
         mView.setShelf(shelf);
     }
 
     public int getShelfHeight() {
-        if (!NotificationShelfController.checkRefactorFlagEnabled(mFeatureFlags)) {
+        if (!mShelfRefactor.expectEnabled()) {
             return 0;
         }
         ExpandableView shelf = mView.getShelf();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt
new file mode 100644
index 0000000..874450b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.stack.domain.interactor
+
+import android.content.Context
+import com.android.systemui.R
+import com.android.systemui.common.ui.data.repository.ConfigurationRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+
+/** Encapsulates business-logic specifically related to the shared notification stack container. */
+class SharedNotificationContainerInteractor
+@Inject
+constructor(
+    configurationRepository: ConfigurationRepository,
+    private val context: Context,
+) {
+    val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> =
+        configurationRepository.onAnyConfigurationChange
+            .onStart { emit(Unit) }
+            .map { _ ->
+                with(context.resources) {
+                    ConfigurationBasedDimensions(
+                        useSplitShade = getBoolean(R.bool.config_use_split_notification_shade),
+                        useLargeScreenHeader =
+                            getBoolean(R.bool.config_use_large_screen_shade_header),
+                        marginHorizontal =
+                            getDimensionPixelSize(R.dimen.notification_panel_margin_horizontal),
+                        marginBottom =
+                            getDimensionPixelSize(R.dimen.notification_panel_margin_bottom),
+                        marginTop = getDimensionPixelSize(R.dimen.notification_panel_margin_top),
+                        marginTopLargeScreen =
+                            getDimensionPixelSize(R.dimen.large_screen_shade_header_height),
+                    )
+                }
+            }
+            .distinctUntilChanged()
+
+    data class ConfigurationBasedDimensions(
+        val useSplitShade: Boolean,
+        val useLargeScreenHeader: Boolean,
+        val marginHorizontal: Int,
+        val marginBottom: Int,
+        val marginTop: Int,
+        val marginTopLargeScreen: Int,
+    )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt
new file mode 100644
index 0000000..688843d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.stack.ui.view
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
+import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
+import androidx.constraintlayout.widget.ConstraintSet.END
+import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
+import androidx.constraintlayout.widget.ConstraintSet.START
+import androidx.constraintlayout.widget.ConstraintSet.TOP
+import androidx.constraintlayout.widget.ConstraintSet.VERTICAL
+import com.android.systemui.R
+
+/**
+ * Container for the stack scroller, so that the bounds can be externally specified, such as from
+ * the keyguard or shade scenes.
+ */
+class SharedNotificationContainer(
+    context: Context,
+    private val attrs: AttributeSet?,
+) :
+    ConstraintLayout(
+        context,
+        attrs,
+    ) {
+
+    private val baseConstraintSet = ConstraintSet()
+
+    init {
+        baseConstraintSet.apply {
+            create(R.id.nssl_guideline, VERTICAL)
+            setGuidelinePercent(R.id.nssl_guideline, 0.5f)
+        }
+        baseConstraintSet.applyTo(this)
+    }
+
+    fun addNotificationStackScrollLayout(nssl: View) {
+        addView(nssl)
+    }
+
+    fun updateConstraints(
+        useSplitShade: Boolean,
+        marginStart: Int,
+        marginTop: Int,
+        marginEnd: Int,
+        marginBottom: Int
+    ) {
+        val constraintSet = ConstraintSet()
+        constraintSet.clone(baseConstraintSet)
+
+        val startConstraintId =
+            if (useSplitShade) {
+                R.id.nssl_guideline
+            } else {
+                PARENT_ID
+            }
+        val nsslId = R.id.notification_stack_scroller
+        constraintSet.apply {
+            connect(nsslId, START, startConstraintId, START)
+            connect(nsslId, END, PARENT_ID, END)
+            connect(nsslId, BOTTOM, PARENT_ID, BOTTOM)
+            connect(nsslId, TOP, PARENT_ID, TOP)
+            setMargin(nsslId, START, marginStart)
+            setMargin(nsslId, END, marginEnd)
+            setMargin(nsslId, TOP, marginTop)
+            setMargin(nsslId, BOTTOM, marginBottom)
+        }
+        constraintSet.applyTo(this)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
new file mode 100644
index 0000000..fb1d55d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.stack.ui.viewbinder
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
+import kotlinx.coroutines.launch
+
+/** Binds the shared notification container to its view-model. */
+object SharedNotificationContainerBinder {
+
+    @JvmStatic
+    fun bind(
+        view: SharedNotificationContainer,
+        viewModel: SharedNotificationContainerViewModel,
+    ) {
+        view.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                launch {
+                    viewModel.configurationBasedDimensions.collect {
+                        view.updateConstraints(
+                            useSplitShade = it.useSplitShade,
+                            marginStart = it.marginStart,
+                            marginTop = it.marginTop,
+                            marginEnd = it.marginEnd,
+                            marginBottom = it.marginBottom,
+                        )
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
new file mode 100644
index 0000000..b2e5ac1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.stack.ui.viewmodel
+
+import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+
+/** View-model for the shared notification container */
+class SharedNotificationContainerViewModel
+@Inject
+constructor(
+    interactor: SharedNotificationContainerInteractor,
+) {
+    val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> =
+        interactor.configurationBasedDimensions
+            .map {
+                ConfigurationBasedDimensions(
+                    marginStart = if (it.useSplitShade) 0 else it.marginHorizontal,
+                    marginEnd = it.marginHorizontal,
+                    marginBottom = it.marginBottom,
+                    marginTop =
+                        if (it.useLargeScreenHeader) it.marginTopLargeScreen else it.marginTop,
+                    useSplitShade = it.useSplitShade,
+                )
+            }
+            .distinctUntilChanged()
+
+    data class ConfigurationBasedDimensions(
+        val marginStart: Int,
+        val marginTop: Int,
+        val marginEnd: Int,
+        val marginBottom: Int,
+        val useSplitShade: Boolean,
+    )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index 66d217b..dc021fe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -160,6 +160,7 @@
     private KeyguardViewController mKeyguardViewController;
     private DozeScrimController mDozeScrimController;
     private KeyguardViewMediator mKeyguardViewMediator;
+    private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     private PendingAuthenticated mPendingAuthenticated = null;
     private boolean mHasScreenTurnedOnSinceAuthenticating;
     private boolean mFadedAwayAfterWakeAndUnlock;
@@ -280,7 +281,8 @@
             LatencyTracker latencyTracker,
             ScreenOffAnimationController screenOffAnimationController,
             VibratorHelper vibrator,
-            SystemClock systemClock
+            SystemClock systemClock,
+            StatusBarKeyguardViewManager statusBarKeyguardViewManager
     ) {
         mPowerManager = powerManager;
         mUpdateMonitor = keyguardUpdateMonitor;
@@ -308,6 +310,7 @@
         mVibratorHelper = vibrator;
         mLogger = biometricUnlockLogger;
         mSystemClock = systemClock;
+        mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
 
         dumpManager.registerDumpable(this);
     }
@@ -449,8 +452,19 @@
         // During wake and unlock, we need to draw black before waking up to avoid abrupt
         // brightness changes due to display state transitions.
         Runnable wakeUp = ()-> {
-            if (!wasDeviceInteractive || mUpdateMonitor.isDreaming()) {
+            // Check to see if we are still locked when we are waking and unlocking from dream.
+            // This runnable should be executed after unlock. If that's true, we could be not
+            // dreaming, but still locked. In this case, we should attempt to authenticate instead
+            // of waking up.
+            if (mode == MODE_WAKE_AND_UNLOCK_FROM_DREAM
+                    && !mKeyguardStateController.isUnlocked()
+                    && !mUpdateMonitor.isDreaming()) {
+                // Post wakeUp runnable is called from a callback in keyguard.
+                mHandler.post(() -> mKeyguardViewController.notifyKeyguardAuthenticated(
+                        false /* primaryAuth */));
+            } else if (!wasDeviceInteractive || mUpdateMonitor.isDreaming()) {
                 mLogger.i("bio wakelock: Authenticated, waking up...");
+
                 mPowerManager.wakeUp(
                         mSystemClock.uptimeMillis(),
                         PowerManager.WAKE_REASON_BIOMETRIC,
@@ -462,7 +476,7 @@
             Trace.endSection();
         };
 
-        if (mMode != MODE_NONE) {
+        if (mMode != MODE_NONE && mMode != MODE_WAKE_AND_UNLOCK_FROM_DREAM) {
             wakeUp.run();
         }
         switch (mMode) {
@@ -484,6 +498,10 @@
                 Trace.endSection();
                 break;
             case MODE_WAKE_AND_UNLOCK_FROM_DREAM:
+                // In the case of waking and unlocking from dream, waking up is delayed until after
+                // unlock is complete to avoid conflicts during each sequence's transitions.
+                mStatusBarKeyguardViewManager.addAfterKeyguardGoneRunnable(wakeUp);
+                // Execution falls through here to proceed unlocking.
             case MODE_WAKE_AND_UNLOCK_PULSING:
             case MODE_WAKE_AND_UNLOCK:
                 if (mMode == MODE_WAKE_AND_UNLOCK_PULSING) {
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 2b9c3d3..acd6e49 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
@@ -259,8 +259,6 @@
 
     void readyForKeyguardDone();
 
-    void setLockscreenUser(int newUserId);
-
     void showKeyguard();
 
     boolean hideKeyguard();
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 0a7ee52..8361d6b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -51,7 +51,6 @@
 import android.app.StatusBarManager;
 import android.app.TaskInfo;
 import android.app.UiModeManager;
-import android.app.WallpaperInfo;
 import android.app.WallpaperManager;
 import android.app.admin.DevicePolicyManager;
 import android.content.BroadcastReceiver;
@@ -413,7 +412,6 @@
 
     private final Point mCurrentDisplaySize = new Point();
 
-    protected NotificationShadeWindowView mNotificationShadeWindowView;
     protected PhoneStatusBarView mStatusBarView;
     private PhoneStatusBarViewController mPhoneStatusBarViewController;
     private PhoneStatusBarTransitions mStatusBarTransitions;
@@ -456,7 +454,8 @@
     private final FalsingManager mFalsingManager;
     private final BroadcastDispatcher mBroadcastDispatcher;
     private final ConfigurationController mConfigurationController;
-    protected NotificationShadeWindowViewController mNotificationShadeWindowViewController;
+    private final Lazy<NotificationShadeWindowViewController>
+            mNotificationShadeWindowViewControllerLazy;
     private final DozeParameters mDozeParameters;
     private final Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy;
     private final CentralSurfacesComponent.Factory mCentralSurfacesComponentFactory;
@@ -722,6 +721,7 @@
             Lazy<AssistManager> assistManagerLazy,
             ConfigurationController configurationController,
             NotificationShadeWindowController notificationShadeWindowController,
+            Lazy<NotificationShadeWindowViewController> notificationShadeWindowViewControllerLazy,
             NotificationShelfController notificationShelfController,
             NotificationStackScrollLayoutController notificationStackScrollLayoutController,
             DozeParameters dozeParameters,
@@ -825,6 +825,7 @@
         mAssistManagerLazy = assistManagerLazy;
         mConfigurationController = configurationController;
         mNotificationShadeWindowController = notificationShadeWindowController;
+        mNotificationShadeWindowViewControllerLazy = notificationShadeWindowViewControllerLazy;
         mNotificationShelfController = notificationShelfController;
         mStackScrollerController = notificationStackScrollLayoutController;
         mStackScroller = mStackScrollerController.getView();
@@ -978,16 +979,6 @@
 
         createAndAddWindows(result);
 
-        if (mWallpaperSupported) {
-            // Make sure we always have the most current wallpaper info.
-            IntentFilter wallpaperChangedFilter = new IntentFilter(Intent.ACTION_WALLPAPER_CHANGED);
-            mBroadcastDispatcher.registerReceiver(mWallpaperChangedReceiver, wallpaperChangedFilter,
-                    null /* handler */, UserHandle.ALL);
-            mWallpaperChangedReceiver.onReceive(mContext, null);
-        } else if (DEBUG) {
-            Log.v(TAG, "start(): no wallpaper service ");
-        }
-
         // Set up the initial notification state. This needs to happen before CommandQueue.disable()
         setUpPresenter();
 
@@ -1073,7 +1064,7 @@
         mDozeServiceHost.initialize(
                 this,
                 mStatusBarKeyguardViewManager,
-                mNotificationShadeWindowViewController,
+                getNotificationShadeWindowViewController(),
                 mShadeSurface,
                 mAmbientIndicationContainer);
         updateLightRevealScrimVisibility();
@@ -1235,8 +1226,8 @@
         updateTheme();
 
         inflateStatusBarWindow();
-        mNotificationShadeWindowView.setOnTouchListener(getStatusBarWindowTouchListener());
-        mWallpaperController.setRootView(mNotificationShadeWindowView);
+        getNotificationShadeWindowView().setOnTouchListener(getStatusBarWindowTouchListener());
+        mWallpaperController.setRootView(getNotificationShadeWindowView());
 
         // TODO: Deal with the ugliness that comes from having some of the status bar broken out
         // into fragments, but the rest here, it leaves some awkward lifecycle and whatnot.
@@ -1257,7 +1248,7 @@
                     mStatusBarView = statusBarView;
                     mPhoneStatusBarViewController = statusBarViewController;
                     mStatusBarTransitions = statusBarTransitions;
-                    mNotificationShadeWindowViewController
+                    getNotificationShadeWindowViewController()
                             .setStatusBarViewController(mPhoneStatusBarViewController);
                     // Ensure we re-propagate panel expansion values to the panel controller and
                     // any listeners it may have, such as PanelBar. This will also ensure we
@@ -1271,7 +1262,7 @@
         mStatusBarInitializer.initializeStatusBar(
                 mCentralSurfacesComponent::createCollapsedStatusBarFragment);
 
-        mStatusBarTouchableRegionManager.setup(this, mNotificationShadeWindowView);
+        mStatusBarTouchableRegionManager.setup(this, getNotificationShadeWindowView());
 
         createNavigationBar(result);
 
@@ -1279,7 +1270,7 @@
             mLockscreenWallpaper = mLockscreenWallpaperLazy.get();
         }
 
-        mAmbientIndicationContainer = mNotificationShadeWindowView.findViewById(
+        mAmbientIndicationContainer = getNotificationShadeWindowView().findViewById(
                 R.id.ambient_indication_container);
 
         mAutoHideController.setStatusBar(new AutoHideUiElement() {
@@ -1304,10 +1295,10 @@
             }
         });
 
-        ScrimView scrimBehind = mNotificationShadeWindowView.findViewById(R.id.scrim_behind);
-        ScrimView notificationsScrim = mNotificationShadeWindowView
+        ScrimView scrimBehind = getNotificationShadeWindowView().findViewById(R.id.scrim_behind);
+        ScrimView notificationsScrim = getNotificationShadeWindowView()
                 .findViewById(R.id.scrim_notifications);
-        ScrimView scrimInFront = mNotificationShadeWindowView.findViewById(R.id.scrim_in_front);
+        ScrimView scrimInFront = getNotificationShadeWindowView().findViewById(R.id.scrim_in_front);
 
         mScrimController.setScrimVisibleListener(scrimsVisible -> {
             mNotificationShadeWindowController.setScrimsVisibility(scrimsVisible);
@@ -1345,7 +1336,7 @@
                 mNotificationShelfController,
                 mHeadsUpManager);
 
-        BackDropView backdrop = mNotificationShadeWindowView.findViewById(R.id.backdrop);
+        BackDropView backdrop = getNotificationShadeWindowView().findViewById(R.id.backdrop);
         if (mWallpaperManager.isLockscreenLiveWallpaperEnabled()) {
             mMediaManager.setup(null, null, null, mScrimController, null);
         } else {
@@ -1364,7 +1355,7 @@
         });
 
         // Set up the quick settings tile panel
-        final View container = mNotificationShadeWindowView.findViewById(R.id.qs_frame);
+        final View container = getNotificationShadeWindowView().findViewById(R.id.qs_frame);
         if (container != null) {
             FragmentHostManager fragmentHostManager =
                     mFragmentService.getFragmentHostManager(container);
@@ -1379,7 +1370,7 @@
                             .withDefault(this::createDefaultQSFragment)
                             .build());
             mBrightnessMirrorController = new BrightnessMirrorController(
-                    mNotificationShadeWindowView,
+                    getNotificationShadeWindowView(),
                     mShadeSurface,
                     mNotificationShadeDepthControllerLazy.get(),
                     mBrightnessSliderFactory,
@@ -1396,7 +1387,7 @@
             });
         }
 
-        mReportRejectedTouch = mNotificationShadeWindowView
+        mReportRejectedTouch = getNotificationShadeWindowView()
                 .findViewById(R.id.report_rejected_touch);
         if (mReportRejectedTouch != null) {
             updateReportRejectedTouchVisibility();
@@ -1544,7 +1535,7 @@
 
     protected QS createDefaultQSFragment() {
         return mFragmentService
-                .getFragmentHostManager(mNotificationShadeWindowView)
+                .getFragmentHostManager(getNotificationShadeWindowView())
                 .create(QSFragment.class);
     }
 
@@ -1553,7 +1544,7 @@
         mActivityLaunchAnimator.setCallback(mActivityLaunchAnimatorCallback);
         mActivityLaunchAnimator.addListener(mActivityLaunchAnimatorListener);
         mNotificationAnimationProvider = new NotificationLaunchAnimatorControllerProvider(
-                mNotificationShadeWindowViewController,
+                getNotificationShadeWindowViewController(),
                 mNotifListContainer,
                 mHeadsUpManager,
                 mJankMonitor);
@@ -1592,7 +1583,7 @@
             mAutoHideController.checkUserAutoHide(event);
             mRemoteInputManager.checkRemoteInputOutside(event);
             mShadeController.onStatusBarTouch(event);
-            return mNotificationShadeWindowView.onTouchEvent(event);
+            return getNotificationShadeWindowView().onTouchEvent(event);
         };
     }
 
@@ -1606,15 +1597,12 @@
                 mCentralSurfacesComponent::createCollapsedStatusBarFragment);
 
         ViewGroup windowRootView = mCentralSurfacesComponent.getWindowRootView();
-        mNotificationShadeWindowView = mCentralSurfacesComponent.getNotificationShadeWindowView();
-        mNotificationShadeWindowViewController = mCentralSurfacesComponent
-                .getNotificationShadeWindowViewController();
         // TODO(b/277762009): Inject [NotificationShadeWindowView] directly into the controller.
         //  (Right now, there's a circular dependency.)
         mNotificationShadeWindowController.setWindowRootView(windowRootView);
-        mNotificationShadeWindowViewController.setupExpandedStatusBar();
+        getNotificationShadeWindowViewController().setupExpandedStatusBar();
         mShadeController.setNotificationShadeWindowViewController(
-                mNotificationShadeWindowViewController);
+                getNotificationShadeWindowViewController());
         mBackActionInteractor.setup(mQsController, mShadeSurface);
         mPresenter = mCentralSurfacesComponent.getNotificationPresenter();
         mNotificationActivityStarter = mCentralSurfacesComponent.getNotificationActivityStarter();
@@ -1633,6 +1621,14 @@
         mCommandQueue.addCallback(mCommandQueueCallbacks);
     }
 
+    protected NotificationShadeWindowViewController getNotificationShadeWindowViewController() {
+        return mNotificationShadeWindowViewControllerLazy.get();
+    }
+
+    protected NotificationShadeWindowView getNotificationShadeWindowView() {
+        return getNotificationShadeWindowViewController().getView();
+    }
+
     protected void startKeyguard() {
         Trace.beginSection("CentralSurfaces#startKeyguard");
         mStatusBarStateController.addCallback(mStateListener,
@@ -1688,7 +1684,7 @@
 
     @Override
     public AuthKeyguardMessageArea getKeyguardMessageArea() {
-        return mNotificationShadeWindowViewController.getKeyguardMessageArea();
+        return getNotificationShadeWindowViewController().getKeyguardMessageArea();
     }
 
     @Override
@@ -1982,11 +1978,9 @@
         pw.print("  mWallpaperSupported= "); pw.println(mWallpaperSupported);
 
         pw.println("  ShadeWindowView: ");
-        if (mNotificationShadeWindowViewController != null) {
-            mNotificationShadeWindowViewController.dump(pw, args);
-            CentralSurfaces.dumpBarTransitions(
-                    pw, "PhoneStatusBarTransitions", mStatusBarTransitions);
-        }
+        getNotificationShadeWindowViewController().dump(pw, args);
+        CentralSurfaces.dumpBarTransitions(
+                pw, "PhoneStatusBarTransitions", mStatusBarTransitions);
 
         pw.println("  mMediaManager: ");
         if (mMediaManager != null) {
@@ -2159,18 +2153,6 @@
     };
 
     /**
-     * Notify the shade controller that the current user changed
-     *
-     * @param newUserId userId of the new user
-     */
-    @Override
-    public void setLockscreenUser(int newUserId) {
-        if (mWallpaperSupported) {
-            mWallpaperChangedReceiver.onReceive(mContext, null);
-        }
-    }
-
-    /**
      * Reload some of our resources when the configuration changes.
      *
      * We don't reload everything when the configuration changes -- we probably
@@ -2860,7 +2842,7 @@
             updateVisibleToUser();
 
             updateNotificationPanelTouchState();
-            mNotificationShadeWindowViewController.cancelCurrentTouch();
+            getNotificationShadeWindowViewController().cancelCurrentTouch();
             if (mLaunchCameraOnFinishedGoingToSleep) {
                 mLaunchCameraOnFinishedGoingToSleep = false;
 
@@ -3172,12 +3154,6 @@
         updateScrimController();
     }
 
-    @VisibleForTesting
-    public void setNotificationShadeWindowViewController(
-            NotificationShadeWindowViewController nswvc) {
-        mNotificationShadeWindowViewController = nswvc;
-    }
-
     /**
      * 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
@@ -3412,7 +3388,7 @@
             mVisible = visible;
             if (visible) {
                 DejankUtils.notifyRendererOfExpensiveFrame(
-                        mNotificationShadeWindowView, "onShadeVisibilityChanged");
+                        getNotificationShadeWindowView(), "onShadeVisibilityChanged");
             } else {
                 mGutsManager.closeAndSaveGuts(true /* removeLeavebehind */, true /* force */,
                         true /* removeControls */, -1 /* x */, -1 /* y */, true /* resetMenu */);
@@ -3550,33 +3526,6 @@
         }
     };
 
-    /**
-     * @deprecated See {@link com.android.systemui.wallpapers.data.repository.WallpaperRepository}
-     * instead.
-     */
-    private final BroadcastReceiver mWallpaperChangedReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            if (!mWallpaperSupported) {
-                // Receiver should not have been registered at all...
-                Log.wtf(TAG, "WallpaperManager not supported");
-                return;
-            }
-            WallpaperInfo info = mWallpaperManager.getWallpaperInfoForUser(
-                    mUserTracker.getUserId());
-            mWallpaperController.onWallpaperInfoUpdated(info);
-
-            final boolean deviceSupportsAodWallpaper = mContext.getResources().getBoolean(
-                    com.android.internal.R.bool.config_dozeSupportsAodWallpaper);
-            // If WallpaperInfo is null, it must be ImageWallpaper.
-            final boolean supportsAmbientMode = deviceSupportsAodWallpaper
-                    && (info != null && info.supportsAmbientMode());
-
-            mNotificationShadeWindowController.setWallpaperSupportsAmbientMode(supportsAmbientMode);
-            mKeyguardViewMediator.setWallpaperSupportsAmbientMode(supportsAmbientMode);
-        }
-    };
-
     private final ConfigurationListener mConfigurationListener = new ConfigurationListener() {
         @Override
         public void onConfigChanged(Configuration newConfig) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
index 7312db6..ed9722e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
@@ -311,6 +311,7 @@
 
     @Override
     public void dozeTimeTick() {
+        mDozeInteractor.dozeTimeTick();
         mNotificationPanel.dozeTimeTick();
         mAuthController.dozeTimeTick();
         if (mAmbientIndicationContainer instanceof DozeReceiver) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt
index ff1b31d..924aac4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt
@@ -50,12 +50,6 @@
     @DevicePostureInt private var postureState: Int = DEVICE_POSTURE_UNKNOWN
     private var pendingUnlock: PendingUnlock? = null
     private val listeners = mutableListOf<OnBypassStateChangedListener>()
-    private val postureCallback = DevicePostureController.Callback { posture ->
-        if (postureState != posture) {
-            postureState = posture
-            notifyListeners()
-        }
-    }
     private val faceAuthEnabledChangedCallback = object : KeyguardStateController.Callback {
         override fun onFaceAuthEnabledChanged() = notifyListeners()
     }
@@ -162,10 +156,8 @@
 
         val dismissByDefault = if (context.resources.getBoolean(
                         com.android.internal.R.bool.config_faceAuthDismissesKeyguard)) 1 else 0
-        tunerService.addTunable(object : TunerService.Tunable {
-            override fun onTuningChanged(key: String?, newValue: String?) {
-                bypassEnabled = tunerService.getValue(key, dismissByDefault) != 0
-            }
+        tunerService.addTunable({ key, _ ->
+            bypassEnabled = tunerService.getValue(key, dismissByDefault) != 0
         }, Settings.Secure.FACE_UNLOCK_DISMISSES_KEYGUARD)
         lockscreenUserManager.addUserChangedListener(
                 object : NotificationLockscreenUserManager.UserChangedListener {
@@ -281,8 +273,6 @@
     }
 
     companion object {
-        const val BYPASS_FADE_DURATION = 67
-
         private const val FACE_UNLOCK_BYPASS_NO_OVERRIDE = 0
         private const val FACE_UNLOCK_BYPASS_ALWAYS = 1
         private const val FACE_UNLOCK_BYPASS_NEVER = 2
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
index e18c9d8..0bf0f4b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
@@ -24,6 +24,8 @@
 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.DarkIconDispatcher.DarkReceiver;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -88,7 +90,7 @@
     private final ArrayList<Rect> mTintAreas = new ArrayList<>();
     private final Context mContext;
 
-    private final FeatureFlags mFeatureFlags;
+    private final ViewRefactorFlag mShelfRefactor;
 
     private int mAodIconAppearTranslation;
 
@@ -120,12 +122,13 @@
             Optional<Bubbles> bubblesOptional,
             DemoModeController demoModeController,
             DarkIconDispatcher darkIconDispatcher,
-            FeatureFlags featureFlags, StatusBarWindowController statusBarWindowController,
+            FeatureFlags featureFlags,
+            StatusBarWindowController statusBarWindowController,
             ScreenOffAnimationController screenOffAnimationController) {
         mContrastColorUtil = ContrastColorUtil.getInstance(context);
         mContext = context;
         mStatusBarStateController = statusBarStateController;
-        mFeatureFlags = featureFlags;
+        mShelfRefactor = new ViewRefactorFlag(featureFlags, Flags.NOTIFICATION_SHELF_REFACTOR);
         mStatusBarStateController.addCallback(this);
         mMediaManager = notificationMediaManager;
         mDozeParameters = dozeParameters;
@@ -179,12 +182,12 @@
     }
 
     public void setupShelf(NotificationShelfController notificationShelfController) {
-        NotificationShelfController.assertRefactorFlagDisabled(mFeatureFlags);
+        mShelfRefactor.assertDisabled();
         mShelfIcons = notificationShelfController.getShelfIcons();
     }
 
     public void setShelfIcons(NotificationIconContainer icons) {
-        if (NotificationShelfController.checkRefactorFlagEnabled(mFeatureFlags)) {
+        if (mShelfRefactor.expectEnabled()) {
             mShelfIcons = icons;
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
index f0fc143..862f169 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.flags.Flags
 import com.android.systemui.shade.ShadeController
 import com.android.systemui.shade.ShadeLogger
+import com.android.systemui.shade.ShadeViewController
 import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.unfold.SysUIUnfoldComponent
@@ -51,6 +52,7 @@
     @Named(UNFOLD_STATUS_BAR) private val progressProvider: ScopedUnfoldTransitionProgressProvider?,
     private val centralSurfaces: CentralSurfaces,
     private val shadeController: ShadeController,
+    private val shadeViewController: ShadeViewController,
     private val shadeLogger: ShadeLogger,
     private val moveFromCenterAnimationController: StatusBarMoveFromCenterAnimationController?,
     private val userChipViewModel: StatusBarUserChipViewModel,
@@ -165,20 +167,20 @@
             if (event.action == MotionEvent.ACTION_DOWN) {
                 // If the view that would receive the touch is disabled, just have status
                 // bar eat the gesture.
-                if (!centralSurfaces.shadeViewController.isViewEnabled) {
+                if (!shadeViewController.isViewEnabled) {
                     shadeLogger.logMotionEvent(event,
                             "onTouchForwardedFromStatusBar: panel view disabled")
                     return true
                 }
-                if (centralSurfaces.shadeViewController.isFullyCollapsed &&
+                if (shadeViewController.isFullyCollapsed &&
                         event.y < 1f) {
                     // b/235889526 Eat events on the top edge of the phone when collapsed
                     shadeLogger.logMotionEvent(event, "top edge touch ignored")
                     return true
                 }
-                centralSurfaces.shadeViewController.startTrackingExpansionFromStatusBar()
+                shadeViewController.startTrackingExpansionFromStatusBar()
             }
-            return centralSurfaces.shadeViewController.handleExternalTouch(event)
+            return shadeViewController.handleExternalTouch(event)
         }
     }
 
@@ -222,6 +224,7 @@
         private val userChipViewModel: StatusBarUserChipViewModel,
         private val centralSurfaces: CentralSurfaces,
         private val shadeController: ShadeController,
+        private val shadeViewController: ShadeViewController,
         private val shadeLogger: ShadeLogger,
         private val viewUtil: ViewUtil,
         private val configurationController: ConfigurationController,
@@ -241,6 +244,7 @@
                     progressProvider.getOrNull(),
                     centralSurfaces,
                     shadeController,
+                    shadeViewController,
                     shadeLogger,
                     statusBarMoveFromCenterAnimationController,
                     userChipViewModel,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java
index 481cf3c..9a295e6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java
@@ -21,6 +21,7 @@
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent;
 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
 import com.android.systemui.statusbar.window.StatusBarWindowController;
@@ -35,6 +36,7 @@
     private final NotificationShadeWindowController mNotificationShadeWindowController;
     private final StatusBarWindowController mStatusBarWindowController;
     private final ShadeViewController mShadeViewController;
+    private final NotificationStackScrollLayoutController mNsslController;
     private final KeyguardBypassController mKeyguardBypassController;
     private final HeadsUpManagerPhone mHeadsUpManager;
     private final StatusBarStateController mStatusBarStateController;
@@ -45,14 +47,15 @@
             NotificationShadeWindowController notificationShadeWindowController,
             StatusBarWindowController statusBarWindowController,
             ShadeViewController shadeViewController,
+            NotificationStackScrollLayoutController nsslController,
             KeyguardBypassController keyguardBypassController,
             HeadsUpManagerPhone headsUpManager,
             StatusBarStateController statusBarStateController,
             NotificationRemoteInputManager notificationRemoteInputManager) {
-
         mNotificationShadeWindowController = notificationShadeWindowController;
         mStatusBarWindowController = statusBarWindowController;
         mShadeViewController = shadeViewController;
+        mNsslController = nsslController;
         mKeyguardBypassController = keyguardBypassController;
         mHeadsUpManager = headsUpManager;
         mStatusBarStateController = statusBarStateController;
@@ -85,8 +88,7 @@
                 //animation
                 // is finished.
                 mHeadsUpManager.setHeadsUpGoingAway(true);
-                mShadeViewController.getNotificationStackScrollLayoutController()
-                        .runAfterAnimationFinished(() -> {
+                mNsslController.runAfterAnimationFinished(() -> {
                     if (!mHeadsUpManager.hasPinnedHeadsUp()) {
                         mNotificationShadeWindowController.setHeadsUpShowing(false);
                         mHeadsUpManager.setHeadsUpGoingAway(false);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
index 35285b2..a9135ac 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
@@ -28,7 +28,6 @@
 import android.util.Log;
 import android.util.Slog;
 import android.view.View;
-import android.view.accessibility.AccessibilityManager;
 
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.systemui.InitController;
@@ -50,7 +49,6 @@
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.AboveShelfObserver;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
-import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.render.NotifShadeEventSource;
 import com.android.systemui.statusbar.notification.domain.interactor.NotificationsInteractor;
@@ -59,7 +57,6 @@
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager.OnSettingsClickListener;
-import com.android.systemui.statusbar.notification.row.NotificationInfo.CheckSaveListener;
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent;
@@ -78,21 +75,18 @@
     private final NotifShadeEventSource mNotifShadeEventSource;
     private final NotificationMediaManager mMediaManager;
     private final NotificationGutsManager mGutsManager;
-
     private final ShadeViewController mNotificationPanel;
     private final HeadsUpManagerPhone mHeadsUpManager;
     private final AboveShelfObserver mAboveShelfObserver;
     private final DozeScrimController mDozeScrimController;
     private final CentralSurfaces mCentralSurfaces;
     private final NotificationsInteractor mNotificationsInteractor;
+    private final NotificationStackScrollLayoutController mNsslController;
     private final LockscreenShadeTransitionController mShadeTransitionController;
     private final PowerInteractor mPowerInteractor;
     private final CommandQueue mCommandQueue;
-
-    private final AccessibilityManager mAccessibilityManager;
     private final KeyguardManager mKeyguardManager;
     private final NotificationShadeWindowController mNotificationShadeWindowController;
-    private final NotifPipelineFlags mNotifPipelineFlags;
     private final IStatusBarService mBarService;
     private final DynamicPrivacyController mDynamicPrivacyController;
     private final NotificationListContainer mNotifListContainer;
@@ -123,11 +117,9 @@
             NotifShadeEventSource notifShadeEventSource,
             NotificationMediaManager notificationMediaManager,
             NotificationGutsManager notificationGutsManager,
-            LockscreenGestureLogger lockscreenGestureLogger,
             InitController initController,
             NotificationInterruptStateProvider notificationInterruptStateProvider,
             NotificationRemoteInputManager remoteInputManager,
-            NotifPipelineFlags notifPipelineFlags,
             NotificationRemoteInputManager.Callback remoteInputManagerCallback,
             NotificationListContainer notificationListContainer) {
         mActivityStarter = activityStarter;
@@ -139,6 +131,7 @@
         // TODO: use KeyguardStateController#isOccluded to remove this dependency
         mCentralSurfaces = centralSurfaces;
         mNotificationsInteractor = notificationsInteractor;
+        mNsslController = stackScrollerController;
         mShadeTransitionController = shadeTransitionController;
         mPowerInteractor = powerInteractor;
         mCommandQueue = commandQueue;
@@ -149,10 +142,8 @@
         mGutsManager = notificationGutsManager;
         mAboveShelfObserver = new AboveShelfObserver(stackScrollerController.getView());
         mNotificationShadeWindowController = notificationShadeWindowController;
-        mNotifPipelineFlags = notifPipelineFlags;
         mAboveShelfObserver.setListener(statusBarWindow.findViewById(
                 R.id.notification_container_parent));
-        mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
         mDozeScrimController = dozeScrimController;
         mKeyguardManager = context.getSystemService(KeyguardManager.class);
         mBarService = IStatusBarService.Stub.asInterface(
@@ -170,7 +161,7 @@
         }
         remoteInputManager.setUpWithCallback(
                 remoteInputManagerCallback,
-                mNotificationPanel.getShadeNotificationPresenter().createRemoteInputDelegate());
+                mNsslController.createDelegate());
 
         initController.addPostInitTask(() -> {
             mNotifShadeEventSource.setShadeEmptiedCallback(this::maybeClosePanelForShadeEmptied);
@@ -202,7 +193,7 @@
     }
 
     private void maybeEndAmbientPulse() {
-        if (mNotificationPanel.getShadeNotificationPresenter().hasPulsingNotifications()
+        if (mNsslController.getNotificationListContainer().hasPulsingNotifications()
                 && !mHeadsUpManager.hasNotifications()) {
             // We were showing a pulse for a notification, but no notifications are pulsing anymore.
             // Finish the pulse.
@@ -217,7 +208,6 @@
         // End old BaseStatusBar.userSwitched
         mCommandQueue.animateCollapsePanels();
         mMediaManager.clearCurrentMediaNotification();
-        mCentralSurfaces.setLockscreenUser(newUserId);
         updateMediaMetaData(true, false);
     }
 
@@ -272,22 +262,6 @@
         }
     };
 
-    private final CheckSaveListener mCheckSaveListener = new CheckSaveListener() {
-        @Override
-        public void checkSave(Runnable saveImportance, StatusBarNotification sbn) {
-            // If the user has security enabled, show challenge if the setting is changed.
-            if (mLockscreenUserManager.isLockscreenPublicMode(sbn.getUser().getIdentifier())
-                    && mKeyguardManager.isKeyguardLocked()) {
-                onLockedNotificationImportanceChange(() -> {
-                    saveImportance.run();
-                    return true;
-                });
-            } else {
-                saveImportance.run();
-            }
-        }
-    };
-
     private final OnSettingsClickListener mOnSettingsClickListener = new OnSettingsClickListener() {
         @Override
         public void onSettingsClick(String key) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java
index 4ae460a..e77f419 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java
@@ -21,8 +21,6 @@
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
 import com.android.systemui.scene.ui.view.WindowRootView;
-import com.android.systemui.shade.NotificationShadeWindowView;
-import com.android.systemui.shade.NotificationShadeWindowViewController;
 import com.android.systemui.shade.ShadeHeaderController;
 import com.android.systemui.statusbar.NotificationPresenter;
 import com.android.systemui.statusbar.notification.NotificationActivityStarter;
@@ -77,16 +75,6 @@
     WindowRootView getWindowRootView();
 
     /**
-     * Creates or returns a {@link NotificationShadeWindowView}.
-     */
-    NotificationShadeWindowView getNotificationShadeWindowView();
-
-    /**
-     * Creates a NotificationShadeWindowViewController.
-     */
-    NotificationShadeWindowViewController getNotificationShadeWindowViewController();
-
-    /**
      * Creates a StatusBarHeadsUpChangeListener.
      */
     StatusBarHeadsUpChangeListener getStatusBarHeadsUpChangeListener();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index 27cc64f..0e99c67 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -19,12 +19,10 @@
 import android.net.wifi.WifiManager
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.LogBufferFactory
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.log.table.TableLogBufferFactory
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.CollapsedStatusBarViewModel
-import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.CollapsedStatusBarViewModelImpl
 import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository
 import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepositoryImpl
 import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel
@@ -46,6 +44,8 @@
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl
 import com.android.systemui.statusbar.pipeline.shared.ui.binder.CollapsedStatusBarViewBinder
 import com.android.systemui.statusbar.pipeline.shared.ui.binder.CollapsedStatusBarViewBinderImpl
+import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.CollapsedStatusBarViewModel
+import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.CollapsedStatusBarViewModelImpl
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.RealWifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositorySwitcher
@@ -58,9 +58,9 @@
 import dagger.Provides
 import dagger.multibindings.ClassKey
 import dagger.multibindings.IntoMap
-import kotlinx.coroutines.flow.Flow
 import java.util.function.Supplier
 import javax.inject.Named
+import kotlinx.coroutines.flow.Flow
 
 @Module
 abstract class StatusBarPipelineModule {
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt b/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt
index eed7950..cbe4020 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt
@@ -94,7 +94,7 @@
         wakefulnessLifecycle.addObserver(this)
 
         // TODO(b/254878364): remove this call to NPVC.getView()
-        getShadeFoldAnimator().view.repeatWhenAttached {
+        getShadeFoldAnimator().view?.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.STARTED) { listenForDozing(this) }
         }
     }
@@ -161,10 +161,9 @@
             // but we should wait for the initial animation preparations to be drawn
             // (setting initial alpha/translation)
             // TODO(b/254878364): remove this call to NPVC.getView()
-            OneShotPreDrawListener.add(
-                getShadeFoldAnimator().view,
-                onReady
-            )
+            getShadeFoldAnimator().view?.let {
+                OneShotPreDrawListener.add(it, onReady)
+            }
         } else {
             // No animation, call ready callback immediately
             onReady.run()
diff --git a/packages/SystemUI/src/com/android/systemui/util/WallpaperController.kt b/packages/SystemUI/src/com/android/systemui/util/WallpaperController.kt
index db2aca8..65a0218 100644
--- a/packages/SystemUI/src/com/android/systemui/util/WallpaperController.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/WallpaperController.kt
@@ -16,32 +16,34 @@
 
 package com.android.systemui.util
 
-import android.app.WallpaperInfo
 import android.app.WallpaperManager
 import android.util.Log
 import android.view.View
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.wallpapers.data.repository.WallpaperRepository
 import javax.inject.Inject
 import kotlin.math.max
 
 private const val TAG = "WallpaperController"
 
+/**
+ * Controller for wallpaper-related logic.
+ *
+ * Note: New logic should be added to [WallpaperRepository], not this class.
+ */
 @SysUISingleton
-class WallpaperController @Inject constructor(private val wallpaperManager: WallpaperManager) {
+class WallpaperController @Inject constructor(
+    private val wallpaperManager: WallpaperManager,
+    private val wallpaperRepository: WallpaperRepository,
+) {
 
     var rootView: View? = null
 
     private var notificationShadeZoomOut: Float = 0f
     private var unfoldTransitionZoomOut: Float = 0f
 
-    private var wallpaperInfo: WallpaperInfo? = null
-
-    fun onWallpaperInfoUpdated(wallpaperInfo: WallpaperInfo?) {
-        this.wallpaperInfo = wallpaperInfo
-    }
-
     private val shouldUseDefaultUnfoldTransition: Boolean
-        get() = wallpaperInfo?.shouldUseDefaultUnfoldTransition()
+        get() = wallpaperRepository.wallpaperInfo.value?.shouldUseDefaultUnfoldTransition()
             ?: true
 
     fun setNotificationShadeZoom(zoomOut: Float) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 349f368..87b2697 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -34,6 +34,7 @@
 
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_VOLUME_CONTROL;
 import static com.android.internal.jank.InteractionJankMonitor.Configuration.Builder;
+import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION;
 import static com.android.systemui.volume.Events.DISMISS_REASON_POSTURE_CHANGED;
 import static com.android.systemui.volume.Events.DISMISS_REASON_SETTINGS_CLICKED;
 
@@ -82,6 +83,7 @@
 import android.util.SparseBooleanArray;
 import android.view.ContextThemeWrapper;
 import android.view.Gravity;
+import android.view.HapticFeedbackConstants;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.AccessibilityDelegate;
@@ -120,6 +122,7 @@
 import com.android.systemui.Prefs;
 import com.android.systemui.R;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.VolumeDialog;
@@ -300,6 +303,7 @@
     private final DevicePostureController mDevicePostureController;
     private @DevicePostureController.DevicePostureInt int mDevicePosture;
     private int mOrientation;
+    private final FeatureFlags mFeatureFlags;
 
     public VolumeDialogImpl(
             Context context,
@@ -315,7 +319,9 @@
             CsdWarningDialog.Factory csdWarningDialogFactory,
             DevicePostureController devicePostureController,
             Looper looper,
-            DumpManager dumpManager) {
+            DumpManager dumpManager,
+            FeatureFlags featureFlags) {
+        mFeatureFlags = featureFlags;
         mContext =
                 new ContextThemeWrapper(context, R.style.volume_dialog_theme);
         mHandler = new H(looper);
@@ -1319,12 +1325,14 @@
 
     private void provideTouchFeedbackH(int newRingerMode) {
         VibrationEffect effect = null;
+        int hapticConstant = HapticFeedbackConstants.NO_HAPTICS;
         switch (newRingerMode) {
             case RINGER_MODE_NORMAL:
                 mController.scheduleTouchFeedback();
                 break;
             case RINGER_MODE_SILENT:
                 effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
+                hapticConstant = HapticFeedbackConstants.TOGGLE_OFF;
                 break;
             case RINGER_MODE_VIBRATE:
                 // Feedback handled by onStateChange, for feedback both when user toggles
@@ -1332,8 +1340,11 @@
                 break;
             default:
                 effect = VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK);
+                hapticConstant = HapticFeedbackConstants.TOGGLE_ON;
         }
-        if (effect != null) {
+        if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
+            mDialogView.performHapticFeedback(hapticConstant);
+        } else if (effect != null) {
             mController.vibrate(effect);
         }
     }
@@ -1770,7 +1781,22 @@
                 && mState.ringerModeInternal != -1
                 && mState.ringerModeInternal != state.ringerModeInternal
                 && state.ringerModeInternal == AudioManager.RINGER_MODE_VIBRATE) {
-            mController.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK));
+
+            if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
+                if (mShowing) {
+                    // The dialog view is responsible for triggering haptics in the oneway API
+                    mDialogView.performHapticFeedback(HapticFeedbackConstants.TOGGLE_ON);
+                }
+                /*
+                TODO(b/290642122): If the dialog is not showing, we have the case where haptics is
+                enabled by dragging the volume slider of Settings to a value of 0. This must be
+                handled by view Slices in Settings whilst using the performHapticFeedback API.
+                 */
+
+            } else {
+                // Old behavior only active if the oneway API is not used.
+                mController.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK));
+            }
         }
         mState = state;
         mDynamic.clear();
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
index d0edc6e..cc9f3e1 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
@@ -22,6 +22,7 @@
 
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.VolumeDialog;
@@ -61,7 +62,8 @@
             InteractionJankMonitor interactionJankMonitor,
             CsdWarningDialog.Factory csdFactory,
             DevicePostureController devicePostureController,
-            DumpManager dumpManager) {
+            DumpManager dumpManager,
+            FeatureFlags featureFlags) {
         VolumeDialogImpl impl = new VolumeDialogImpl(
                 context,
                 volumeDialogController,
@@ -76,7 +78,8 @@
                 csdFactory,
                 devicePostureController,
                 Looper.getMainLooper(),
-                dumpManager);
+                dumpManager,
+                featureFlags);
         impl.setStreamImportant(AudioManager.STREAM_SYSTEM, false);
         impl.setAutomute(true);
         impl.setSilentMode(false);
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt
index a640589..b45b8cd 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt
@@ -16,9 +16,11 @@
 
 package com.android.systemui.wallpapers.data.repository
 
+import android.app.WallpaperInfo
 import com.android.systemui.dagger.SysUISingleton
 import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 
 /**
@@ -29,5 +31,6 @@
  */
 @SysUISingleton
 class NoopWallpaperRepository @Inject constructor() : WallpaperRepository {
+    override val wallpaperInfo: StateFlow<WallpaperInfo?> = MutableStateFlow(null).asStateFlow()
     override val wallpaperSupportsAmbientMode = MutableStateFlow(false).asStateFlow()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt
index 48895ff..b8f9583 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.wallpapers.data.repository
 
+import android.app.WallpaperInfo
 import android.app.WallpaperManager
 import android.content.Context
 import android.content.Intent
@@ -36,11 +37,15 @@
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.stateIn
 
 /** A repository storing information about the current wallpaper. */
 interface WallpaperRepository {
+    /** Emits the current user's current wallpaper. */
+    val wallpaperInfo: StateFlow<WallpaperInfo?>
+
     /** Emits true if the current user's current wallpaper supports ambient mode. */
     val wallpaperSupportsAmbientMode: StateFlow<Boolean>
 }
@@ -78,28 +83,35 @@
             // Only update the wallpaper status once the user selection has finished.
             .filter { it.selectionStatus == SelectionStatus.SELECTION_COMPLETE }
 
-    override val wallpaperSupportsAmbientMode: StateFlow<Boolean> =
+    override val wallpaperInfo: StateFlow<WallpaperInfo?> =
         if (!wallpaperManager.isWallpaperSupported || !deviceSupportsAodWallpaper) {
-            MutableStateFlow(false).asStateFlow()
+            MutableStateFlow(null).asStateFlow()
         } else {
             combine(wallpaperChanged, selectedUser) { _, selectedUser ->
-                    doesWallpaperSupportAmbientMode(selectedUser)
+                    getWallpaper(selectedUser)
                 }
                 .stateIn(
                     scope,
                     // Always be listening for wallpaper changes.
                     SharingStarted.Eagerly,
-                    initialValue =
-                        doesWallpaperSupportAmbientMode(userRepository.selectedUser.value),
+                    initialValue = getWallpaper(userRepository.selectedUser.value),
                 )
         }
 
-    private fun doesWallpaperSupportAmbientMode(selectedUser: SelectedUserModel): Boolean {
-        return wallpaperManager
-            .getWallpaperInfoForUser(
-                selectedUser.userInfo.id,
+    override val wallpaperSupportsAmbientMode: StateFlow<Boolean> =
+        wallpaperInfo
+            .map {
+                // If WallpaperInfo is null, it's ImageWallpaper which never supports ambient mode.
+                it?.supportsAmbientMode() == true
+            }
+            .stateIn(
+                scope,
+                // Always be listening for wallpaper changes.
+                SharingStarted.Eagerly,
+                initialValue = wallpaperInfo.value?.supportsAmbientMode() == true,
             )
-            // If WallpaperInfo is null, it's ImageWallpaper which never supports ambient mode.
-            ?.supportsAmbientMode() == true
+
+    private fun getWallpaper(selectedUser: SelectedUserModel): WallpaperInfo? {
+        return wallpaperManager.getWallpaperInfoForUser(selectedUser.userInfo.id)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
index 5d75428..cb18229 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
@@ -76,7 +76,7 @@
   private lateinit var mKeyguardMessageAreaController:
       KeyguardMessageAreaController<BouncerKeyguardMessageArea>
 
-    @Mock private lateinit var mPostureController: DevicePostureController
+  @Mock private lateinit var mPostureController: DevicePostureController
 
   private lateinit var mKeyguardPatternViewController: KeyguardPatternViewController
   private lateinit var fakeFeatureFlags: FakeFeatureFlags
@@ -119,7 +119,7 @@
 
         mKeyguardPatternViewController.onViewAttached()
 
-        assertThat(getPatternTopGuideline()).isEqualTo(getExpectedTopGuideline())
+        assertThat(getPatternTopGuideline()).isEqualTo(getHalfOpenedBouncerHeightRatio())
     }
 
     @Test
@@ -131,15 +131,20 @@
         mKeyguardPatternViewController.onViewAttached()
 
         // Verify view begins in posture state DEVICE_POSTURE_HALF_OPENED
-        assertThat(getPatternTopGuideline()).isEqualTo(getExpectedTopGuideline())
+        assertThat(getPatternTopGuideline()).isEqualTo(getHalfOpenedBouncerHeightRatio())
 
         // Simulate posture change to state DEVICE_POSTURE_OPENED with callback
         verify(mPostureController).addCallback(postureCallbackCaptor.capture())
         val postureCallback: DevicePostureController.Callback = postureCallbackCaptor.value
         postureCallback.onPostureChanged(DEVICE_POSTURE_OPENED)
 
-        // Verify view is now in posture state DEVICE_POSTURE_OPENED
-        assertThat(getPatternTopGuideline()).isNotEqualTo(getExpectedTopGuideline())
+        // Simulate posture change to same state with callback
+        assertThat(getPatternTopGuideline()).isNotEqualTo(getHalfOpenedBouncerHeightRatio())
+
+        postureCallback.onPostureChanged(DEVICE_POSTURE_OPENED)
+
+        // Verify view is still in posture state DEVICE_POSTURE_OPENED
+        assertThat(getPatternTopGuideline()).isNotEqualTo(getHalfOpenedBouncerHeightRatio())
     }
 
     private fun getPatternTopGuideline(): Float {
@@ -150,7 +155,7 @@
         return cs.getConstraint(R.id.pattern_top_guideline).layout.guidePercent
     }
 
-    private fun getExpectedTopGuideline(): Float {
+    private fun getHalfOpenedBouncerHeightRatio(): Float {
         return mContext.resources.getFloat(R.dimen.half_opened_bouncer_height_ratio)
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
index d256ee1..4dc7652 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
@@ -19,6 +19,8 @@
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.View
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
 import androidx.test.filters.SmallTest
 import com.android.internal.util.LatencyTracker
 import com.android.internal.widget.LockPatternUtils
@@ -32,6 +34,8 @@
 import com.android.systemui.statusbar.policy.DevicePostureController
 import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_HALF_OPENED
 import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -51,7 +55,10 @@
 @RunWith(AndroidTestingRunner::class)
 @TestableLooper.RunWithLooper
 class KeyguardPinViewControllerTest : SysuiTestCase() {
-    @Mock private lateinit var keyguardPinView: KeyguardPINView
+
+    private lateinit var objectKeyguardPINView: KeyguardPINView
+
+    @Mock private lateinit var mockKeyguardPinView: KeyguardPINView
 
     @Mock private lateinit var keyguardMessageArea: BouncerKeyguardMessageArea
 
@@ -83,64 +90,73 @@
     @Mock lateinit var deleteButton: NumPadButton
     @Mock lateinit var enterButton: View
 
-    private lateinit var pinViewController: KeyguardPinViewController
-
     @Captor lateinit var postureCallbackCaptor: ArgumentCaptor<DevicePostureController.Callback>
 
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
-        Mockito.`when`(keyguardPinView.requireViewById<View>(R.id.bouncer_message_area))
+        Mockito.`when`(mockKeyguardPinView.requireViewById<View>(R.id.bouncer_message_area))
             .thenReturn(keyguardMessageArea)
         Mockito.`when`(
                 keyguardMessageAreaControllerFactory.create(any(KeyguardMessageArea::class.java))
             )
             .thenReturn(keyguardMessageAreaController)
-        `when`(keyguardPinView.passwordTextViewId).thenReturn(R.id.pinEntry)
-        `when`(keyguardPinView.findViewById<PasswordTextView>(R.id.pinEntry))
+        `when`(mockKeyguardPinView.passwordTextViewId).thenReturn(R.id.pinEntry)
+        `when`(mockKeyguardPinView.findViewById<PasswordTextView>(R.id.pinEntry))
             .thenReturn(passwordTextView)
-        `when`(keyguardPinView.resources).thenReturn(context.resources)
-        `when`(keyguardPinView.findViewById<NumPadButton>(R.id.delete_button))
+        `when`(mockKeyguardPinView.resources).thenReturn(context.resources)
+        `when`(mockKeyguardPinView.findViewById<NumPadButton>(R.id.delete_button))
             .thenReturn(deleteButton)
-        `when`(keyguardPinView.findViewById<View>(R.id.key_enter)).thenReturn(enterButton)
+        `when`(mockKeyguardPinView.findViewById<View>(R.id.key_enter)).thenReturn(enterButton)
         // For posture tests:
-        `when`(keyguardPinView.buttons).thenReturn(arrayOf())
+        `when`(mockKeyguardPinView.buttons).thenReturn(arrayOf())
         `when`(lockPatternUtils.getPinLength(anyInt())).thenReturn(6)
 
-        pinViewController =
-            KeyguardPinViewController(
-                keyguardPinView,
-                keyguardUpdateMonitor,
-                securityMode,
-                lockPatternUtils,
-                mKeyguardSecurityCallback,
-                keyguardMessageAreaControllerFactory,
-                mLatencyTracker,
-                liftToActivateListener,
-                mEmergencyButtonController,
-                falsingCollector,
-                postureController,
-                featureFlags
-            )
+        objectKeyguardPINView =
+            View.inflate(mContext, R.layout.keyguard_pin_view, null)
+                .findViewById(R.id.keyguard_pin_view) as KeyguardPINView
+    }
+
+    private fun constructPinViewController(
+        mKeyguardPinView: KeyguardPINView
+    ): KeyguardPinViewController {
+        return KeyguardPinViewController(
+            mKeyguardPinView,
+            keyguardUpdateMonitor,
+            securityMode,
+            lockPatternUtils,
+            mKeyguardSecurityCallback,
+            keyguardMessageAreaControllerFactory,
+            mLatencyTracker,
+            liftToActivateListener,
+            mEmergencyButtonController,
+            falsingCollector,
+            postureController,
+            featureFlags
+        )
     }
 
     @Test
-    fun onViewAttached_deviceHalfFolded_propagatedToPinView() {
-        `when`(postureController.devicePosture).thenReturn(DEVICE_POSTURE_HALF_OPENED)
+    fun onViewAttached_deviceHalfFolded_propagatedToPatternView() {
+        val pinViewController = constructPinViewController(objectKeyguardPINView)
+        overrideResource(R.dimen.half_opened_bouncer_height_ratio, 0.5f)
+        whenever(postureController.devicePosture).thenReturn(DEVICE_POSTURE_HALF_OPENED)
 
         pinViewController.onViewAttached()
 
-        verify(keyguardPinView).onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED)
+        assertThat(getPinTopGuideline()).isEqualTo(getHalfOpenedBouncerHeightRatio())
     }
 
     @Test
-    fun onDevicePostureChanged_deviceHalfFolded_propagatedToPinView() {
-        `when`(postureController.devicePosture).thenReturn(DEVICE_POSTURE_HALF_OPENED)
+    fun onDevicePostureChanged_deviceOpened_propagatedToPatternView() {
+        val pinViewController = constructPinViewController(objectKeyguardPINView)
+        overrideResource(R.dimen.half_opened_bouncer_height_ratio, 0.5f)
+
+        whenever(postureController.devicePosture).thenReturn(DEVICE_POSTURE_HALF_OPENED)
+        pinViewController.onViewAttached()
 
         // Verify view begins in posture state DEVICE_POSTURE_HALF_OPENED
-        pinViewController.onViewAttached()
-
-        verify(keyguardPinView).onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED)
+        assertThat(getPinTopGuideline()).isEqualTo(getHalfOpenedBouncerHeightRatio())
 
         // Simulate posture change to state DEVICE_POSTURE_OPENED with callback
         verify(postureController).addCallback(postureCallbackCaptor.capture())
@@ -148,31 +164,57 @@
         postureCallback.onPostureChanged(DEVICE_POSTURE_OPENED)
 
         // Verify view is now in posture state DEVICE_POSTURE_OPENED
-        verify(keyguardPinView).onDevicePostureChanged(DEVICE_POSTURE_OPENED)
+        assertThat(getPinTopGuideline()).isNotEqualTo(getHalfOpenedBouncerHeightRatio())
+
+        // Simulate posture change to same state with callback
+        postureCallback.onPostureChanged(DEVICE_POSTURE_OPENED)
+
+        // Verify view is still in posture state DEVICE_POSTURE_OPENED
+        assertThat(getPinTopGuideline()).isNotEqualTo(getHalfOpenedBouncerHeightRatio())
+    }
+
+    private fun getPinTopGuideline(): Float {
+        val cs = ConstraintSet()
+        val container = objectKeyguardPINView.findViewById(R.id.pin_container) as ConstraintLayout
+        cs.clone(container)
+        return cs.getConstraint(R.id.pin_pad_top_guideline).layout.guidePercent
+    }
+
+    private fun getHalfOpenedBouncerHeightRatio(): Float {
+        return mContext.resources.getFloat(R.dimen.half_opened_bouncer_height_ratio)
     }
 
     @Test
     fun startAppearAnimation() {
+        val pinViewController = constructPinViewController(mockKeyguardPinView)
+
         pinViewController.startAppearAnimation()
+
         verify(keyguardMessageAreaController)
             .setMessage(context.resources.getString(R.string.keyguard_enter_your_pin), false)
     }
 
     @Test
     fun startAppearAnimation_withExistingMessage() {
+        val pinViewController = constructPinViewController(mockKeyguardPinView)
         Mockito.`when`(keyguardMessageAreaController.message).thenReturn("Unlock to continue.")
+
         pinViewController.startAppearAnimation()
+
         verify(keyguardMessageAreaController, Mockito.never()).setMessage(anyString(), anyBoolean())
     }
 
     @Test
     fun startAppearAnimation_withAutoPinConfirmationFailedPasswordAttemptsLessThan5() {
+        val pinViewController = constructPinViewController(mockKeyguardPinView)
         `when`(featureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)).thenReturn(true)
+        `when`(lockPatternUtils.getPinLength(anyInt())).thenReturn(6)
         `when`(lockPatternUtils.isAutoPinConfirmEnabled(anyInt())).thenReturn(true)
         `when`(lockPatternUtils.getCurrentFailedPasswordAttempts(anyInt())).thenReturn(3)
         `when`(passwordTextView.text).thenReturn("")
 
         pinViewController.startAppearAnimation()
+
         verify(deleteButton).visibility = View.INVISIBLE
         verify(enterButton).visibility = View.INVISIBLE
         verify(passwordTextView).setUsePinShapes(true)
@@ -181,12 +223,15 @@
 
     @Test
     fun startAppearAnimation_withAutoPinConfirmationFailedPasswordAttemptsMoreThan5() {
+        val pinViewController = constructPinViewController(mockKeyguardPinView)
         `when`(featureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)).thenReturn(true)
+        `when`(lockPatternUtils.getPinLength(anyInt())).thenReturn(6)
         `when`(lockPatternUtils.isAutoPinConfirmEnabled(anyInt())).thenReturn(true)
         `when`(lockPatternUtils.getCurrentFailedPasswordAttempts(anyInt())).thenReturn(6)
         `when`(passwordTextView.text).thenReturn("")
 
         pinViewController.startAppearAnimation()
+
         verify(deleteButton).visibility = View.VISIBLE
         verify(enterButton).visibility = View.VISIBLE
         verify(passwordTextView).setUsePinShapes(true)
@@ -195,7 +240,10 @@
 
     @Test
     fun handleLockout_readsNumberOfErrorAttempts() {
+        val pinViewController = constructPinViewController(mockKeyguardPinView)
+
         pinViewController.handleAttemptLockout(0)
+
         verify(lockPatternUtils).getCurrentFailedPasswordAttempts(anyInt())
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ExpandHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/ExpandHelperTest.java
index 6decb88..5867a40c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ExpandHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ExpandHelperTest.java
@@ -30,6 +30,8 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
@@ -43,12 +45,14 @@
 @RunWithLooper
 public class ExpandHelperTest extends SysuiTestCase {
 
+    private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
     private ExpandableNotificationRow mRow;
     private ExpandHelper mExpandHelper;
     private ExpandHelper.Callback mCallback;
 
     @Before
     public void setUp() throws Exception {
+        mFeatureFlags.setDefault(Flags.NOTIFICATION_INLINE_REPLY_ANIMATION);
         mDependency.injectMockDependency(KeyguardUpdateMonitor.class);
         mDependency.injectMockDependency(NotificationMediaManager.class);
         allowTestableLooperAsMainThread();
@@ -56,7 +60,8 @@
         NotificationTestHelper helper = new NotificationTestHelper(
                 mContext,
                 mDependency,
-                TestableLooper.get(this));
+                TestableLooper.get(this),
+                mFeatureFlags);
         mRow = helper.createRow();
         mCallback = mock(ExpandHelper.Callback.class);
         mExpandHelper = new ExpandHelper(context, mCallback, 10, 100);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt
index da9ceb4..212dad7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt
@@ -8,6 +8,7 @@
 import android.widget.LinearLayout
 import android.widget.RelativeLayout
 import androidx.test.filters.SmallTest
+import com.android.app.animation.Interpolators
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.util.children
 import junit.framework.Assert.assertEquals
@@ -19,7 +20,6 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import com.android.app.animation.Interpolators
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
@@ -178,7 +178,7 @@
     }
 
     @Test
-    fun animatesRootAndChildren() {
+    fun animatesRootAndChildren_withoutExcludedViews() {
         setUpRootWithChildren()
 
         val success = ViewHierarchyAnimator.animate(rootView)
@@ -208,6 +208,40 @@
     }
 
     @Test
+    fun animatesRootAndChildren_withExcludedViews() {
+        setUpRootWithChildren()
+
+        val success = ViewHierarchyAnimator.animate(
+            rootView,
+            excludedViews = setOf(rootView.getChildAt(0))
+        )
+        // Change all bounds.
+        rootView.measure(
+                View.MeasureSpec.makeMeasureSpec(180, View.MeasureSpec.EXACTLY),
+                View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+        )
+        rootView.layout(10 /* l */, 20 /* t */, 200 /* r */, 120 /* b */)
+
+        assertTrue(success)
+        assertNotNull(rootView.getTag(R.id.tag_animator))
+        assertNull(rootView.getChildAt(0).getTag(R.id.tag_animator))
+        assertNotNull(rootView.getChildAt(1).getTag(R.id.tag_animator))
+        // The initial values for the affected views should be those of the previous layout, while
+        // the excluded view should be at the final values from the beginning.
+        checkBounds(rootView, l = 0, t = 0, r = 200, b = 100)
+        checkBounds(rootView.getChildAt(0), l = 0, t = 0, r = 90, b = 100)
+        checkBounds(rootView.getChildAt(1), l = 100, t = 0, r = 200, b = 100)
+        endAnimation(rootView)
+        assertNull(rootView.getTag(R.id.tag_animator))
+        assertNull(rootView.getChildAt(0).getTag(R.id.tag_animator))
+        assertNull(rootView.getChildAt(1).getTag(R.id.tag_animator))
+        // The end values should be those of the latest layout.
+        checkBounds(rootView, l = 10, t = 20, r = 200, b = 120)
+        checkBounds(rootView.getChildAt(0), l = 0, t = 0, r = 90, b = 100)
+        checkBounds(rootView.getChildAt(1), l = 90, t = 0, r = 180, b = 100)
+    }
+
+    @Test
     fun animatesInvisibleViews() {
         rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
         rootView.visibility = View.INVISIBLE
diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
index c223c5a..a6ad4b2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
@@ -103,19 +103,6 @@
         }
 
     @Test
-    fun toggleBypassEnabled() =
-        testScope.runTest {
-            val isBypassEnabled by collectLastValue(underTest.isBypassEnabled)
-            assertThat(isBypassEnabled).isFalse()
-
-            underTest.toggleBypassEnabled()
-            assertThat(isBypassEnabled).isTrue()
-
-            underTest.toggleBypassEnabled()
-            assertThat(isBypassEnabled).isFalse()
-        }
-
-    @Test
     fun isAuthenticationRequired_lockedAndSecured_true() =
         testScope.runTest {
             utils.authenticationRepository.setUnlocked(false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
index eaa31ac..ecc776b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
@@ -48,6 +48,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.airbnb.lottie.LottieAnimationView
+import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.R
 import com.android.systemui.RoboPilotTest
 import com.android.systemui.SysuiTestCase
@@ -64,7 +65,6 @@
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.concurrency.FakeExecutor
@@ -153,8 +153,8 @@
                 mock(KeyguardStateController::class.java),
                 keyguardBouncerRepository,
                 FakeBiometricSettingsRepository(),
-                FakeDeviceEntryFingerprintAuthRepository(),
                 FakeSystemClock(),
+                mock(KeyguardUpdateMonitor::class.java),
             )
         displayStateInteractor =
             DisplayStateInteractorImpl(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
index 9df06dc..8dfeb3b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
@@ -34,7 +34,6 @@
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.DismissCallbackRegistry
 import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
-import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.data.repository.FakeTrustRepository
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -106,8 +105,8 @@
                 mock(KeyguardStateController::class.java),
                 keyguardBouncerRepository,
                 mock(BiometricSettingsRepository::class.java),
-                mock(DeviceEntryFingerprintAuthRepository::class.java),
                 mock(SystemClock::class.java),
+                mKeyguardUpdateMonitor,
             )
         return createUdfpsKeyguardViewController(
             /* useModernBouncer */ true, /* useExpandedOverlay */
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt
index 37b9ca4..186df02 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt
@@ -18,6 +18,7 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.RoboPilotTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
@@ -54,6 +55,7 @@
     @Mock private lateinit var keyguardStateController: KeyguardStateController
     @Mock private lateinit var systemClock: SystemClock
     @Mock private lateinit var bouncerLogger: TableLogBuffer
+    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
 
     @Before
     fun setup() {
@@ -72,8 +74,8 @@
                 keyguardStateController,
                 bouncerRepository,
                 biometricSettingsRepository,
-                deviceEntryFingerprintAuthRepository,
                 systemClock,
+                keyguardUpdateMonitor,
             )
     }
 
@@ -118,7 +120,7 @@
     @Test
     fun canShowAlternateBouncerForFingerprint_fingerprintLockedOut() {
         givenCanShowAlternateBouncer()
-        deviceEntryFingerprintAuthRepository.setLockedOut(true)
+        whenever(keyguardUpdateMonitor.isFingerprintLockedOut).thenReturn(true)
 
         assertFalse(underTest.canShowAlternateBouncerForFingerprint())
     }
@@ -168,7 +170,7 @@
         biometricSettingsRepository.setFingerprintEnrolled(true)
         biometricSettingsRepository.setStrongBiometricAllowed(true)
         biometricSettingsRepository.setFingerprintEnabledByDevicePolicy(true)
-        deviceEntryFingerprintAuthRepository.setLockedOut(false)
+        whenever(keyguardUpdateMonitor.isFingerprintLockedOut).thenReturn(false)
         whenever(keyguardStateController.isUnlocked).thenReturn(false)
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationCollectionLiveDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationCollectionLiveDataTest.java
index 461ec65..40f0ed3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationCollectionLiveDataTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationCollectionLiveDataTest.java
@@ -28,10 +28,10 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.dreams.DreamLogger;
 import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.flags.Flags;
+import com.android.systemui.log.core.FakeLogBuffer;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.time.FakeSystemClock;
 
@@ -57,8 +57,6 @@
     private FakeFeatureFlags mFeatureFlags;
     @Mock
     private Observer mObserver;
-    @Mock
-    private DreamLogger mLogger;
 
     @Before
     public void setUp() {
@@ -70,7 +68,7 @@
                 mExecutor,
                 /* overlayEnabled= */ true,
                 mFeatureFlags,
-                mLogger);
+                FakeLogBuffer.Factory.Companion.create());
         mLiveData = new ComplicationCollectionLiveData(mStateController);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt
index a00e545..57307fc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt
@@ -9,6 +9,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.complication.ComplicationHostViewController
 import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
+import com.android.systemui.log.core.FakeLogBuffer
 import com.android.systemui.statusbar.BlurUtils
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.mockito.argumentCaptor
@@ -47,7 +48,7 @@
     @Mock private lateinit var stateController: DreamOverlayStateController
     @Mock private lateinit var configController: ConfigurationController
     @Mock private lateinit var transitionViewModel: DreamingToLockscreenTransitionViewModel
-    @Mock private lateinit var logger: DreamLogger
+    private val logBuffer = FakeLogBuffer.Factory.create()
     private lateinit var controller: DreamOverlayAnimationsController
 
     @Before
@@ -66,7 +67,7 @@
                 DREAM_IN_COMPLICATIONS_ANIMATION_DURATION,
                 DREAM_IN_TRANSLATION_Y_DISTANCE,
                 DREAM_IN_TRANSLATION_Y_DURATION,
-                logger
+                logBuffer
             )
 
         val mockView: View = mock()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
index 2c1ebe4..44a78ac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
@@ -34,6 +34,8 @@
 import com.android.systemui.complication.Complication;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
+import com.android.systemui.log.LogBuffer;
+import com.android.systemui.log.core.FakeLogBuffer;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.time.FakeSystemClock;
 
@@ -58,8 +60,7 @@
     @Mock
     private FeatureFlags mFeatureFlags;
 
-    @Mock
-    private DreamLogger mLogger;
+    private final LogBuffer mLogBuffer = FakeLogBuffer.Factory.Companion.create();
 
     final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
 
@@ -408,6 +409,11 @@
     }
 
     private DreamOverlayStateController getDreamOverlayStateController(boolean overlayEnabled) {
-        return new DreamOverlayStateController(mExecutor, overlayEnabled, mFeatureFlags, mLogger);
+        return new DreamOverlayStateController(
+                mExecutor,
+                overlayEnabled,
+                mFeatureFlags,
+                mLogBuffer
+        );
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
index 5dc0e55..4e74f451 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
@@ -48,6 +48,8 @@
 
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.log.LogBuffer;
+import com.android.systemui.log.core.FakeLogBuffer;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController;
 import com.android.systemui.statusbar.policy.NextAlarmController;
@@ -113,8 +115,8 @@
     DreamOverlayStateController mDreamOverlayStateController;
     @Mock
     UserTracker mUserTracker;
-    @Mock
-    DreamLogger mLogger;
+
+    LogBuffer mLogBuffer = FakeLogBuffer.Factory.Companion.create();
 
     @Captor
     private ArgumentCaptor<DreamOverlayStateController.Callback> mCallbackCaptor;
@@ -149,7 +151,7 @@
                 mDreamOverlayStatusBarItemsProvider,
                 mDreamOverlayStateController,
                 mUserTracker,
-                mLogger);
+                mLogBuffer);
     }
 
     @Test
@@ -293,7 +295,7 @@
                 mDreamOverlayStatusBarItemsProvider,
                 mDreamOverlayStateController,
                 mUserTracker,
-                mLogger);
+                mLogBuffer);
         controller.onViewAttached();
         verify(mView, never()).showIcon(
                 eq(DreamOverlayStatusBarView.STATUS_ICON_NOTIFICATIONS), eq(true), any());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index ddcbe79..00f67a3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -78,6 +78,7 @@
 import com.android.keyguard.KeyguardSecurityView;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.keyguard.TestScopeProvider;
 import com.android.keyguard.mediator.ScreenOnCoordinator;
 import com.android.systemui.DejankUtils;
 import com.android.systemui.SysuiTestCase;
@@ -115,9 +116,11 @@
 import com.android.systemui.util.DeviceConfigProxy;
 import com.android.systemui.util.DeviceConfigProxyFake;
 import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.kotlin.JavaAdapter;
 import com.android.systemui.util.settings.SecureSettings;
 import com.android.systemui.util.settings.SystemSettings;
 import com.android.systemui.util.time.FakeSystemClock;
+import com.android.systemui.wallpapers.data.repository.FakeWallpaperRepository;
 import com.android.wm.shell.keyguard.KeyguardTransitions;
 
 import org.junit.After;
@@ -132,6 +135,7 @@
 
 import kotlinx.coroutines.CoroutineDispatcher;
 import kotlinx.coroutines.flow.Flow;
+import kotlinx.coroutines.test.TestScope;
 
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
@@ -139,6 +143,9 @@
 public class KeyguardViewMediatorTest extends SysuiTestCase {
     private KeyguardViewMediator mViewMediator;
 
+    private final TestScope mTestScope = TestScopeProvider.getTestScope();
+    private final JavaAdapter mJavaAdapter = new JavaAdapter(mTestScope.getBackgroundScope());
+
     private @Mock UserTracker mUserTracker;
     private @Mock DevicePolicyManager mDevicePolicyManager;
     private @Mock LockPatternUtils mLockPatternUtils;
@@ -191,6 +198,7 @@
     private @Mock SecureSettings mSecureSettings;
     private @Mock AlarmManager mAlarmManager;
     private FakeSystemClock mSystemClock;
+    private final FakeWallpaperRepository mWallpaperRepository = new FakeWallpaperRepository();
 
     /** Most recent value passed to {@link KeyguardStateController#notifyKeyguardGoingAway}. */
     private boolean mKeyguardGoingAway = false;
@@ -928,6 +936,8 @@
                 mKeyguardTransitions,
                 mInteractionJankMonitor,
                 mDreamOverlayStateController,
+                mJavaAdapter,
+                mWallpaperRepository,
                 () -> mShadeController,
                 () -> mNotificationShadeWindowController,
                 () -> mActivityLaunchAnimator,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
index f9070b3..c6a2fa5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
@@ -162,11 +162,11 @@
     @Test
     fun convenienceBiometricAllowedChange() =
         testScope.runTest {
+            overrideResource(com.android.internal.R.bool.config_strongAuthRequiredOnBoot, false)
             createBiometricSettingsRepository()
             val convenienceBiometricAllowed =
                 collectLastValue(underTest.isNonStrongBiometricAllowed)
             runCurrent()
-
             onNonStrongAuthChanged(true, PRIMARY_USER_ID)
             assertThat(convenienceBiometricAllowed()).isTrue()
 
@@ -175,6 +175,45 @@
 
             onNonStrongAuthChanged(false, PRIMARY_USER_ID)
             assertThat(convenienceBiometricAllowed()).isFalse()
+            mContext.orCreateTestableResources.removeOverride(
+                com.android.internal.R.bool.config_strongAuthRequiredOnBoot
+            )
+        }
+
+    @Test
+    fun whenStrongAuthRequiredAfterBoot_nonStrongBiometricNotAllowed() =
+        testScope.runTest {
+            overrideResource(com.android.internal.R.bool.config_strongAuthRequiredOnBoot, true)
+            createBiometricSettingsRepository()
+
+            val convenienceBiometricAllowed =
+                collectLastValue(underTest.isNonStrongBiometricAllowed)
+            runCurrent()
+            onNonStrongAuthChanged(true, PRIMARY_USER_ID)
+
+            assertThat(convenienceBiometricAllowed()).isFalse()
+            mContext.orCreateTestableResources.removeOverride(
+                com.android.internal.R.bool.config_strongAuthRequiredOnBoot
+            )
+        }
+
+    @Test
+    fun whenStrongBiometricAuthIsNotAllowed_nonStrongBiometrics_alsoNotAllowed() =
+        testScope.runTest {
+            overrideResource(com.android.internal.R.bool.config_strongAuthRequiredOnBoot, false)
+            createBiometricSettingsRepository()
+
+            val convenienceBiometricAllowed =
+                collectLastValue(underTest.isNonStrongBiometricAllowed)
+            runCurrent()
+            onNonStrongAuthChanged(true, PRIMARY_USER_ID)
+            assertThat(convenienceBiometricAllowed()).isTrue()
+
+            onStrongAuthChanged(STRONG_AUTH_REQUIRED_AFTER_TIMEOUT, PRIMARY_USER_ID)
+            assertThat(convenienceBiometricAllowed()).isFalse()
+            mContext.orCreateTestableResources.removeOverride(
+                com.android.internal.R.bool.config_strongAuthRequiredOnBoot
+            )
         }
 
     private fun onStrongAuthChanged(flags: Int, userId: Int) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
index 020c0b2..47c662c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
@@ -36,6 +36,7 @@
 import com.android.keyguard.FaceAuthUiEvent
 import com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN
 import com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER
+import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.R
 import com.android.systemui.RoboPilotTest
 import com.android.systemui.SysuiTestCase
@@ -50,12 +51,12 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
-import com.android.systemui.keyguard.shared.model.AuthenticationStatus
-import com.android.systemui.keyguard.shared.model.DetectionStatus
-import com.android.systemui.keyguard.shared.model.ErrorAuthenticationStatus
-import com.android.systemui.keyguard.shared.model.HelpAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.ErrorFaceAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FaceDetectionStatus
+import com.android.systemui.keyguard.shared.model.HelpFaceAuthenticationStatus
 import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.SuccessAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.SuccessFaceAuthenticationStatus
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.keyguard.shared.model.WakeSleepReason
@@ -113,6 +114,7 @@
     @Mock private lateinit var sessionTracker: SessionTracker
     @Mock private lateinit var uiEventLogger: UiEventLogger
     @Mock private lateinit var dumpManager: DumpManager
+    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
 
     @Captor
     private lateinit var authenticationCallback: ArgumentCaptor<FaceManager.AuthenticationCallback>
@@ -131,8 +133,8 @@
     private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository
     private lateinit var testScope: TestScope
     private lateinit var fakeUserRepository: FakeUserRepository
-    private lateinit var authStatus: FlowValue<AuthenticationStatus?>
-    private lateinit var detectStatus: FlowValue<DetectionStatus?>
+    private lateinit var authStatus: FlowValue<FaceAuthenticationStatus?>
+    private lateinit var detectStatus: FlowValue<FaceDetectionStatus?>
     private lateinit var authRunning: FlowValue<Boolean?>
     private lateinit var bypassEnabled: FlowValue<Boolean?>
     private lateinit var lockedOut: FlowValue<Boolean?>
@@ -175,10 +177,10 @@
             AlternateBouncerInteractor(
                 bouncerRepository = bouncerRepository,
                 biometricSettingsRepository = biometricSettingsRepository,
-                deviceEntryFingerprintAuthRepository = deviceEntryFingerprintAuthRepository,
                 systemClock = mock(SystemClock::class.java),
                 keyguardStateController = FakeKeyguardStateController(),
                 statusBarStateController = mock(StatusBarStateController::class.java),
+                keyguardUpdateMonitor = keyguardUpdateMonitor,
             )
 
         bypassStateChangedListener =
@@ -262,7 +264,7 @@
             val successResult = successResult()
             authenticationCallback.value.onAuthenticationSucceeded(successResult)
 
-            assertThat(authStatus()).isEqualTo(SuccessAuthenticationStatus(successResult))
+            assertThat(authStatus()).isEqualTo(SuccessFaceAuthenticationStatus(successResult))
             assertThat(authenticated()).isTrue()
             assertThat(authRunning()).isFalse()
             assertThat(canFaceAuthRun()).isFalse()
@@ -383,7 +385,7 @@
 
             detectionCallback.value.onFaceDetected(1, 1, true)
 
-            assertThat(detectStatus()).isEqualTo(DetectionStatus(1, 1, true))
+            assertThat(detectStatus()).isEqualTo(FaceDetectionStatus(1, 1, true))
         }
 
     @Test
@@ -423,7 +425,7 @@
                 FACE_ERROR_CANCELED,
                 "First auth attempt cancellation completed"
             )
-            val value = authStatus() as ErrorAuthenticationStatus
+            val value = authStatus() as ErrorFaceAuthenticationStatus
             assertThat(value.msgId).isEqualTo(FACE_ERROR_CANCELED)
             assertThat(value.msg).isEqualTo("First auth attempt cancellation completed")
 
@@ -465,7 +467,7 @@
             authenticationCallback.value.onAuthenticationHelp(10, "Ignored help msg")
             authenticationCallback.value.onAuthenticationHelp(11, "Ignored help msg")
 
-            assertThat(authStatus()).isEqualTo(HelpAuthenticationStatus(9, "help msg"))
+            assertThat(authStatus()).isEqualTo(HelpFaceAuthenticationStatus(9, "help msg"))
         }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt
index 264328b..def016a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt
@@ -26,7 +26,11 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.dump.DumpManager
+import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -49,7 +53,6 @@
 @RunWith(AndroidJUnit4::class)
 class DeviceEntryFingerprintAuthRepositoryTest : SysuiTestCase() {
     @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
-    @Mock private lateinit var dumpManager: DumpManager
     @Mock private lateinit var authController: AuthController
     @Captor
     private lateinit var updateMonitorCallback: ArgumentCaptor<KeyguardUpdateMonitorCallback>
@@ -68,7 +71,6 @@
                 authController,
                 keyguardUpdateMonitor,
                 testScope.backgroundScope,
-                dumpManager,
             )
     }
 
@@ -177,4 +179,129 @@
             callback.value.onAllAuthenticatorsRegistered(TYPE_FINGERPRINT)
             assertThat(availableFpSensorType()).isEqualTo(BiometricType.UNDER_DISPLAY_FINGERPRINT)
         }
+
+    @Test
+    fun onFingerprintSuccess_successAuthenticationStatus() =
+        testScope.runTest {
+            val authenticationStatus by collectLastValue(underTest.authenticationStatus)
+            runCurrent()
+
+            verify(keyguardUpdateMonitor).registerCallback(updateMonitorCallback.capture())
+            updateMonitorCallback.value.onBiometricAuthenticated(
+                0,
+                BiometricSourceType.FINGERPRINT,
+                true,
+            )
+
+            val status = authenticationStatus as SuccessFingerprintAuthenticationStatus
+            assertThat(status.userId).isEqualTo(0)
+            assertThat(status.isStrongBiometric).isEqualTo(true)
+        }
+
+    @Test
+    fun onFingerprintFailed_failedAuthenticationStatus() =
+        testScope.runTest {
+            val authenticationStatus by collectLastValue(underTest.authenticationStatus)
+            runCurrent()
+
+            verify(keyguardUpdateMonitor).registerCallback(updateMonitorCallback.capture())
+            updateMonitorCallback.value.onBiometricAuthFailed(
+                BiometricSourceType.FINGERPRINT,
+            )
+
+            assertThat(authenticationStatus)
+                .isInstanceOf(FailFingerprintAuthenticationStatus::class.java)
+        }
+
+    @Test
+    fun onFingerprintError_errorAuthenticationStatus() =
+        testScope.runTest {
+            val authenticationStatus by collectLastValue(underTest.authenticationStatus)
+            runCurrent()
+
+            verify(keyguardUpdateMonitor).registerCallback(updateMonitorCallback.capture())
+            updateMonitorCallback.value.onBiometricError(
+                1,
+                "test_string",
+                BiometricSourceType.FINGERPRINT,
+            )
+
+            val status = authenticationStatus as ErrorFingerprintAuthenticationStatus
+            assertThat(status.msgId).isEqualTo(1)
+            assertThat(status.msg).isEqualTo("test_string")
+        }
+
+    @Test
+    fun onFingerprintHelp_helpAuthenticationStatus() =
+        testScope.runTest {
+            val authenticationStatus by collectLastValue(underTest.authenticationStatus)
+            runCurrent()
+
+            verify(keyguardUpdateMonitor).registerCallback(updateMonitorCallback.capture())
+            updateMonitorCallback.value.onBiometricHelp(
+                1,
+                "test_string",
+                BiometricSourceType.FINGERPRINT,
+            )
+
+            val status = authenticationStatus as HelpFingerprintAuthenticationStatus
+            assertThat(status.msgId).isEqualTo(1)
+            assertThat(status.msg).isEqualTo("test_string")
+        }
+
+    @Test
+    fun onFingerprintAcquired_acquiredAuthenticationStatus() =
+        testScope.runTest {
+            val authenticationStatus by collectLastValue(underTest.authenticationStatus)
+            runCurrent()
+
+            verify(keyguardUpdateMonitor).registerCallback(updateMonitorCallback.capture())
+            updateMonitorCallback.value.onBiometricAcquired(
+                BiometricSourceType.FINGERPRINT,
+                5,
+            )
+
+            val status = authenticationStatus as AcquiredFingerprintAuthenticationStatus
+            assertThat(status.acquiredInfo).isEqualTo(5)
+        }
+
+    @Test
+    fun onFaceCallbacks_fingerprintAuthenticationStatusIsUnchanged() =
+        testScope.runTest {
+            val authenticationStatus by collectLastValue(underTest.authenticationStatus)
+            runCurrent()
+
+            verify(keyguardUpdateMonitor).registerCallback(updateMonitorCallback.capture())
+            updateMonitorCallback.value.onBiometricAuthenticated(
+                0,
+                BiometricSourceType.FACE,
+                true,
+            )
+            assertThat(authenticationStatus).isNull()
+
+            updateMonitorCallback.value.onBiometricAuthFailed(
+                BiometricSourceType.FACE,
+            )
+            assertThat(authenticationStatus).isNull()
+
+            updateMonitorCallback.value.onBiometricHelp(
+                1,
+                "test_string",
+                BiometricSourceType.FACE,
+            )
+            assertThat(authenticationStatus).isNull()
+
+            updateMonitorCallback.value.onBiometricAcquired(
+                BiometricSourceType.FACE,
+                5,
+            )
+            assertThat(authenticationStatus).isNull()
+
+            updateMonitorCallback.value.onBiometricError(
+                1,
+                "test_string",
+                BiometricSourceType.FACE,
+            )
+            assertThat(authenticationStatus).isNull()
+        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
index e9f0d56..ba7d349 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
@@ -31,16 +31,20 @@
 import com.android.systemui.doze.DozeTransitionCallback
 import com.android.systemui.doze.DozeTransitionListener
 import com.android.systemui.dreams.DreamOverlayCallbackController
+import com.android.systemui.keyguard.ScreenLifecycle
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
 import com.android.systemui.keyguard.shared.model.DozeStateModel
 import com.android.systemui.keyguard.shared.model.DozeTransitionModel
+import com.android.systemui.keyguard.shared.model.ScreenModel
+import com.android.systemui.keyguard.shared.model.ScreenState
 import com.android.systemui.keyguard.shared.model.WakefulnessModel
 import com.android.systemui.keyguard.shared.model.WakefulnessState
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.phone.BiometricUnlockController
 import com.android.systemui.statusbar.phone.DozeParameters
+import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.whenever
@@ -70,9 +74,11 @@
     @Mock private lateinit var statusBarStateController: StatusBarStateController
     @Mock private lateinit var keyguardStateController: KeyguardStateController
     @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
+    @Mock private lateinit var screenLifecycle: ScreenLifecycle
     @Mock private lateinit var biometricUnlockController: BiometricUnlockController
     @Mock private lateinit var dozeTransitionListener: DozeTransitionListener
     @Mock private lateinit var authController: AuthController
+    @Mock private lateinit var keyguardBypassController: KeyguardBypassController
     @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
     @Mock private lateinit var dreamOverlayCallbackController: DreamOverlayCallbackController
     @Mock private lateinit var dozeParameters: DozeParameters
@@ -90,8 +96,10 @@
             KeyguardRepositoryImpl(
                 statusBarStateController,
                 wakefulnessLifecycle,
+                screenLifecycle,
                 biometricUnlockController,
                 keyguardStateController,
+                keyguardBypassController,
                 keyguardUpdateMonitor,
                 dozeTransitionListener,
                 dozeParameters,
@@ -157,6 +165,16 @@
         }
 
     @Test
+    fun dozeTimeTick() =
+        testScope.runTest {
+            var dozeTimeTickValue = collectLastValue(underTest.dozeTimeTick)
+            underTest.dozeTimeTick()
+            runCurrent()
+
+            assertThat(dozeTimeTickValue()).isNull()
+        }
+
+    @Test
     fun isKeyguardShowing() =
         testScope.runTest {
             whenever(keyguardStateController.isShowing).thenReturn(false)
@@ -186,6 +204,20 @@
         }
 
     @Test
+    fun isBypassEnabled_disabledInController() {
+        whenever(keyguardBypassController.isBypassEnabled).thenReturn(false)
+        whenever(keyguardBypassController.bypassEnabled).thenReturn(false)
+        assertThat(underTest.isBypassEnabled()).isFalse()
+    }
+
+    @Test
+    fun isBypassEnabled_enabledInController() {
+        whenever(keyguardBypassController.isBypassEnabled).thenReturn(true)
+        whenever(keyguardBypassController.bypassEnabled).thenReturn(true)
+        assertThat(underTest.isBypassEnabled()).isTrue()
+    }
+
+    @Test
     fun isAodAvailable() = runTest {
         val flow = underTest.isAodAvailable
         var isAodAvailable = collectLastValue(flow)
@@ -354,6 +386,48 @@
         }
 
     @Test
+    fun screenModel() =
+        testScope.runTest {
+            val values = mutableListOf<ScreenModel>()
+            val job = underTest.screenModel.onEach(values::add).launchIn(this)
+
+            runCurrent()
+            val captor = argumentCaptor<ScreenLifecycle.Observer>()
+            verify(screenLifecycle).addObserver(captor.capture())
+
+            whenever(screenLifecycle.getScreenState()).thenReturn(ScreenLifecycle.SCREEN_TURNING_ON)
+            captor.value.onScreenTurningOn()
+            runCurrent()
+
+            whenever(screenLifecycle.getScreenState()).thenReturn(ScreenLifecycle.SCREEN_ON)
+            captor.value.onScreenTurnedOn()
+            runCurrent()
+
+            whenever(screenLifecycle.getScreenState())
+                .thenReturn(ScreenLifecycle.SCREEN_TURNING_OFF)
+            captor.value.onScreenTurningOff()
+            runCurrent()
+
+            whenever(screenLifecycle.getScreenState()).thenReturn(ScreenLifecycle.SCREEN_OFF)
+            captor.value.onScreenTurnedOff()
+            runCurrent()
+
+            assertThat(values.map { it.state })
+                .isEqualTo(
+                    listOf(
+                        // Initial value will be OFF
+                        ScreenState.SCREEN_OFF,
+                        ScreenState.SCREEN_TURNING_ON,
+                        ScreenState.SCREEN_ON,
+                        ScreenState.SCREEN_TURNING_OFF,
+                        ScreenState.SCREEN_OFF,
+                    )
+                )
+
+            job.cancel()
+        }
+
+    @Test
     fun isUdfpsSupported() =
         testScope.runTest {
             whenever(keyguardUpdateMonitor.isUdfpsSupported).thenReturn(true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractorTest.kt
new file mode 100644
index 0000000..3389fa9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractorTest.kt
@@ -0,0 +1,260 @@
+/*
+ *  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.keyguard.domain.interactor
+
+import android.hardware.biometrics.BiometricSourceType.FINGERPRINT
+import android.hardware.fingerprint.FingerprintManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
+import com.android.systemui.biometrics.shared.model.FingerprintSensorType
+import com.android.systemui.biometrics.shared.model.SensorStrength
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.util.IndicationHelper
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BiometricMessageInteractorTest : SysuiTestCase() {
+
+    private lateinit var underTest: BiometricMessageInteractor
+    private lateinit var testScope: TestScope
+    private lateinit var fingerprintPropertyRepository: FakeFingerprintPropertyRepository
+    private lateinit var fingerprintAuthRepository: FakeDeviceEntryFingerprintAuthRepository
+
+    @Mock private lateinit var indicationHelper: IndicationHelper
+    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        testScope = TestScope()
+        fingerprintPropertyRepository = FakeFingerprintPropertyRepository()
+        fingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository()
+        underTest =
+            BiometricMessageInteractor(
+                mContext.resources,
+                fingerprintAuthRepository,
+                fingerprintPropertyRepository,
+                indicationHelper,
+                keyguardUpdateMonitor,
+            )
+    }
+
+    @Test
+    fun fingerprintErrorMessage() =
+        testScope.runTest {
+            val fingerprintErrorMessage by collectLastValue(underTest.fingerprintErrorMessage)
+
+            // GIVEN FINGERPRINT_ERROR_HW_UNAVAILABLE should NOT be suppressed
+            whenever(
+                    indicationHelper.shouldSuppressErrorMsg(
+                        FINGERPRINT,
+                        FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE
+                    )
+                )
+                .thenReturn(false)
+
+            // WHEN authentication status error is FINGERPRINT_ERROR_HW_UNAVAILABLE
+            fingerprintAuthRepository.setAuthenticationStatus(
+                ErrorFingerprintAuthenticationStatus(
+                    msgId = FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE,
+                    msg = "test"
+                )
+            )
+
+            // THEN fingerprintErrorMessage is updated
+            assertThat(fingerprintErrorMessage?.source).isEqualTo(FINGERPRINT)
+            assertThat(fingerprintErrorMessage?.type).isEqualTo(BiometricMessageType.ERROR)
+            assertThat(fingerprintErrorMessage?.id)
+                .isEqualTo(FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE)
+            assertThat(fingerprintErrorMessage?.message).isEqualTo("test")
+        }
+
+    @Test
+    fun fingerprintErrorMessage_suppressedError() =
+        testScope.runTest {
+            val fingerprintErrorMessage by collectLastValue(underTest.fingerprintErrorMessage)
+
+            // GIVEN FINGERPRINT_ERROR_HW_UNAVAILABLE should be suppressed
+            whenever(
+                    indicationHelper.shouldSuppressErrorMsg(
+                        FINGERPRINT,
+                        FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE
+                    )
+                )
+                .thenReturn(true)
+
+            // WHEN authentication status error is FINGERPRINT_ERROR_HW_UNAVAILABLE
+            fingerprintAuthRepository.setAuthenticationStatus(
+                ErrorFingerprintAuthenticationStatus(
+                    msgId = FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE,
+                    msg = "test"
+                )
+            )
+
+            // THEN fingerprintErrorMessage isn't update - it's still null
+            assertThat(fingerprintErrorMessage).isNull()
+        }
+
+    @Test
+    fun fingerprintHelpMessage() =
+        testScope.runTest {
+            val fingerprintHelpMessage by collectLastValue(underTest.fingerprintHelpMessage)
+
+            // GIVEN primary auth is NOT required
+            whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
+                .thenReturn(true)
+
+            // WHEN authentication status help is FINGERPRINT_ACQUIRED_IMAGER_DIRTY
+            fingerprintAuthRepository.setAuthenticationStatus(
+                HelpFingerprintAuthenticationStatus(
+                    msgId = FingerprintManager.FINGERPRINT_ACQUIRED_IMAGER_DIRTY,
+                    msg = "test"
+                )
+            )
+
+            // THEN fingerprintHelpMessage is updated
+            assertThat(fingerprintHelpMessage?.source).isEqualTo(FINGERPRINT)
+            assertThat(fingerprintHelpMessage?.type).isEqualTo(BiometricMessageType.HELP)
+            assertThat(fingerprintHelpMessage?.id)
+                .isEqualTo(FingerprintManager.FINGERPRINT_ACQUIRED_IMAGER_DIRTY)
+            assertThat(fingerprintHelpMessage?.message).isEqualTo("test")
+        }
+
+    @Test
+    fun fingerprintHelpMessage_primaryAuthRequired() =
+        testScope.runTest {
+            val fingerprintHelpMessage by collectLastValue(underTest.fingerprintHelpMessage)
+
+            // GIVEN primary auth is required
+            whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
+                .thenReturn(false)
+
+            // WHEN authentication status help is FINGERPRINT_ACQUIRED_IMAGER_DIRTY
+            fingerprintAuthRepository.setAuthenticationStatus(
+                HelpFingerprintAuthenticationStatus(
+                    msgId = FingerprintManager.FINGERPRINT_ACQUIRED_IMAGER_DIRTY,
+                    msg = "test"
+                )
+            )
+
+            // THEN fingerprintHelpMessage isn't update - it's still null
+            assertThat(fingerprintHelpMessage).isNull()
+        }
+
+    @Test
+    fun fingerprintFailMessage_nonUdfps() =
+        testScope.runTest {
+            val fingerprintFailMessage by collectLastValue(underTest.fingerprintFailMessage)
+
+            // GIVEN primary auth is NOT required
+            whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
+                .thenReturn(true)
+
+            // GIVEN rear fingerprint (not UDFPS)
+            fingerprintPropertyRepository.setProperties(
+                0,
+                SensorStrength.STRONG,
+                FingerprintSensorType.REAR,
+                mapOf()
+            )
+
+            // WHEN authentication status fail
+            fingerprintAuthRepository.setAuthenticationStatus(FailFingerprintAuthenticationStatus)
+
+            // THEN fingerprintFailMessage is updated
+            assertThat(fingerprintFailMessage?.source).isEqualTo(FINGERPRINT)
+            assertThat(fingerprintFailMessage?.type).isEqualTo(BiometricMessageType.FAIL)
+            assertThat(fingerprintFailMessage?.id)
+                .isEqualTo(BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED)
+            assertThat(fingerprintFailMessage?.message)
+                .isEqualTo(
+                    mContext.resources.getString(
+                        com.android.internal.R.string.fingerprint_error_not_match
+                    )
+                )
+        }
+
+    @Test
+    fun fingerprintFailMessage_udfps() =
+        testScope.runTest {
+            val fingerprintFailMessage by collectLastValue(underTest.fingerprintFailMessage)
+
+            // GIVEN primary auth is NOT required
+            whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
+                .thenReturn(true)
+
+            // GIVEN UDFPS
+            fingerprintPropertyRepository.setProperties(
+                0,
+                SensorStrength.STRONG,
+                FingerprintSensorType.UDFPS_OPTICAL,
+                mapOf()
+            )
+
+            // WHEN authentication status fail
+            fingerprintAuthRepository.setAuthenticationStatus(FailFingerprintAuthenticationStatus)
+
+            // THEN fingerprintFailMessage is updated to udfps message
+            assertThat(fingerprintFailMessage?.source).isEqualTo(FINGERPRINT)
+            assertThat(fingerprintFailMessage?.type).isEqualTo(BiometricMessageType.FAIL)
+            assertThat(fingerprintFailMessage?.id)
+                .isEqualTo(BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED)
+            assertThat(fingerprintFailMessage?.message)
+                .isEqualTo(
+                    mContext.resources.getString(
+                        com.android.internal.R.string.fingerprint_udfps_error_not_match
+                    )
+                )
+        }
+
+    @Test
+    fun fingerprintFailedMessage_primaryAuthRequired() =
+        testScope.runTest {
+            val fingerprintFailedMessage by collectLastValue(underTest.fingerprintFailMessage)
+
+            // GIVEN primary auth is required
+            whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
+                .thenReturn(false)
+
+            // WHEN authentication status fail
+            fingerprintAuthRepository.setAuthenticationStatus(FailFingerprintAuthenticationStatus)
+
+            // THEN fingerprintFailedMessage isn't update - it's still null
+            assertThat(fingerprintFailedMessage).isNull()
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt
index 3e81cd3..ced0a21 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt
@@ -38,10 +38,9 @@
 import com.android.systemui.keyguard.DismissCallbackRegistry
 import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
 import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.FakeTrustRepository
-import com.android.systemui.keyguard.shared.model.ErrorAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.ErrorFaceAuthenticationStatus
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
@@ -121,8 +120,8 @@
                     mock(KeyguardStateController::class.java),
                     bouncerRepository,
                     mock(BiometricSettingsRepository::class.java),
-                    FakeDeviceEntryFingerprintAuthRepository(),
                     FakeSystemClock(),
+                    keyguardUpdateMonitor,
                 ),
                 keyguardTransitionInteractor,
                 featureFlags,
@@ -160,7 +159,7 @@
 
             underTest.onDeviceLifted()
 
-            val outputValue = authenticationStatus()!! as ErrorAuthenticationStatus
+            val outputValue = authenticationStatus()!! as ErrorFaceAuthenticationStatus
             assertThat(outputValue.msgId)
                 .isEqualTo(BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT)
             assertThat(outputValue.msg).isEqualTo("Face Unlock unavailable")
diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/core/FakeLogBuffer.kt b/packages/SystemUI/tests/src/com/android/systemui/log/core/FakeLogBuffer.kt
new file mode 100644
index 0000000..272d686
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/log/core/FakeLogBuffer.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.log.core
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogMessageImpl
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.nullable
+import com.android.systemui.util.mockito.whenever
+import org.mockito.Mockito.anyString
+
+/**
+ * A fake [LogBuffer] used for testing that obtains a real [LogMessage] to prevent a
+ * [NullPointerException].
+ */
+class FakeLogBuffer private constructor() {
+    class Factory private constructor() {
+        companion object {
+            fun create(): LogBuffer {
+                val logBuffer = mock<LogBuffer>()
+                whenever(
+                        logBuffer.obtain(
+                            tag = anyString(),
+                            level = any(),
+                            messagePrinter = any(),
+                            exception = nullable(),
+                        )
+                    )
+                    .thenReturn(LogMessageImpl.Factory.create())
+                return logBuffer
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
index b4b3073..9a90a5c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
@@ -216,9 +216,6 @@
     @Mock private lateinit var recCardTitle: TextView
     @Mock private lateinit var coverItem: ImageView
     @Mock private lateinit var matrix: Matrix
-    private lateinit var coverItem1: ImageView
-    private lateinit var coverItem2: ImageView
-    private lateinit var coverItem3: ImageView
     private lateinit var recTitle1: TextView
     private lateinit var recTitle2: TextView
     private lateinit var recTitle3: TextView
@@ -233,7 +230,6 @@
         FakeFeatureFlags().apply {
             this.set(Flags.UMO_SURFACE_RIPPLE, false)
             this.set(Flags.UMO_TURBULENCE_NOISE, false)
-            this.set(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE, false)
         }
     @Mock private lateinit var globalSettings: GlobalSettings
 
@@ -467,21 +463,25 @@
         recSubtitle3 = TextView(context)
 
         whenever(recommendationViewHolder.recommendations).thenReturn(view)
-        whenever(recommendationViewHolder.cardIcon).thenReturn(appIcon)
-
-        // Add a recommendation item
-        coverItem1 = ImageView(context).also { it.setId(R.id.media_cover1) }
-        coverItem2 = ImageView(context).also { it.setId(R.id.media_cover2) }
-        coverItem3 = ImageView(context).also { it.setId(R.id.media_cover3) }
-
+        whenever(recommendationViewHolder.mediaAppIcons)
+            .thenReturn(listOf(recAppIconItem, recAppIconItem, recAppIconItem))
+        whenever(recommendationViewHolder.cardTitle).thenReturn(recCardTitle)
         whenever(recommendationViewHolder.mediaCoverItems)
-            .thenReturn(listOf(coverItem1, coverItem2, coverItem3))
+            .thenReturn(listOf(coverItem, coverItem, coverItem))
         whenever(recommendationViewHolder.mediaCoverContainers)
             .thenReturn(listOf(coverContainer1, coverContainer2, coverContainer3))
         whenever(recommendationViewHolder.mediaTitles)
             .thenReturn(listOf(recTitle1, recTitle2, recTitle3))
         whenever(recommendationViewHolder.mediaSubtitles)
             .thenReturn(listOf(recSubtitle1, recSubtitle2, recSubtitle3))
+        whenever(recommendationViewHolder.mediaProgressBars)
+            .thenReturn(listOf(recProgressBar1, recProgressBar2, recProgressBar3))
+        whenever(coverItem.imageMatrix).thenReturn(matrix)
+
+        // set ids for recommendation containers
+        whenever(coverContainer1.id).thenReturn(1)
+        whenever(coverContainer2.id).thenReturn(2)
+        whenever(coverContainer3.id).thenReturn(3)
 
         whenever(recommendationViewHolder.gutsViewHolder).thenReturn(gutsViewHolder)
 
@@ -1561,7 +1561,8 @@
         verify(viewHolder.player).contentDescription = descriptionCaptor.capture()
         val description = descriptionCaptor.value.toString()
 
-        assertThat(description).contains(REC_APP_NAME)
+        assertThat(description)
+            .isEqualTo(context.getString(R.string.controls_media_smartspace_rec_header))
     }
 
     @Test
@@ -1585,7 +1586,8 @@
         verify(viewHolder.player).contentDescription = descriptionCaptor.capture()
         val description = descriptionCaptor.value.toString()
 
-        assertThat(description).contains(REC_APP_NAME)
+        assertThat(description)
+            .isEqualTo(context.getString(R.string.controls_media_smartspace_rec_header))
     }
 
     @Test
@@ -2151,7 +2153,6 @@
 
     @Test
     fun bindRecommendation_setAfterExecutors() {
-        setupUpdatedRecommendationViewHolder()
         val albumArt = getColorIcon(Color.RED)
         val data =
             smartspaceData.copy(
@@ -2189,7 +2190,6 @@
     @Test
     fun bindRecommendationWithProgressBars() {
         useRealConstraintSets()
-        setupUpdatedRecommendationViewHolder()
         val albumArt = getColorIcon(Color.RED)
         val bundle =
             Bundle().apply {
@@ -2236,7 +2236,6 @@
     @Test
     fun bindRecommendation_carouselNotFitThreeRecs_OrientationPortrait() {
         useRealConstraintSets()
-        setupUpdatedRecommendationViewHolder()
         val albumArt = getColorIcon(Color.RED)
         val data =
             smartspaceData.copy(
@@ -2290,7 +2289,6 @@
     @Test
     fun bindRecommendation_carouselNotFitThreeRecs_OrientationLandscape() {
         useRealConstraintSets()
-        setupUpdatedRecommendationViewHolder()
         val albumArt = getColorIcon(Color.RED)
         val data =
             smartspaceData.copy(
@@ -2505,27 +2503,6 @@
         verify(activityStarter).postStartActivityDismissingKeyguard(eq(pendingIntent))
     }
 
-    private fun setupUpdatedRecommendationViewHolder() {
-        fakeFeatureFlag.set(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE, true)
-        whenever(recommendationViewHolder.mediaAppIcons)
-            .thenReturn(listOf(recAppIconItem, recAppIconItem, recAppIconItem))
-        whenever(recommendationViewHolder.cardTitle).thenReturn(recCardTitle)
-        whenever(recommendationViewHolder.mediaCoverContainers)
-            .thenReturn(listOf(coverContainer1, coverContainer2, coverContainer3))
-        whenever(recommendationViewHolder.mediaCoverItems)
-            .thenReturn(listOf(coverItem, coverItem, coverItem))
-        whenever(recommendationViewHolder.mediaProgressBars)
-            .thenReturn(listOf(recProgressBar1, recProgressBar2, recProgressBar3))
-        whenever(recommendationViewHolder.mediaSubtitles)
-            .thenReturn(listOf(recSubtitle1, recSubtitle2, recSubtitle3))
-        whenever(coverItem.imageMatrix).thenReturn(matrix)
-
-        // set ids for recommendation containers
-        whenever(coverContainer1.id).thenReturn(1)
-        whenever(coverContainer2.id).thenReturn(2)
-        whenever(coverContainer3.id).thenReturn(3)
-    }
-
     private fun getColorIcon(color: Int): Icon {
         val bmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
         val canvas = Canvas(bmp)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt
index c9956f3..ba97df9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt
@@ -201,8 +201,8 @@
         whenever(mockCopiedState.widgetStates)
             .thenReturn(
                 mutableMapOf(
-                    R.id.media_title1 to mediaTitleWidgetState,
-                    R.id.media_subtitle1 to mediaSubTitleWidgetState,
+                    R.id.media_title to mediaTitleWidgetState,
+                    R.id.media_subtitle to mediaSubTitleWidgetState,
                     R.id.media_cover1_container to mediaContainerWidgetState
                 )
             )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/domain/interactor/PowerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/power/domain/interactor/PowerInteractorTest.kt
index 45bb931..435a1f1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/power/domain/interactor/PowerInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/power/domain/interactor/PowerInteractorTest.kt
@@ -182,6 +182,32 @@
         assertThat(repository.lastWakeReason).isEqualTo(PowerManager.WAKE_REASON_APPLICATION)
     }
 
+    @Test
+    fun wakeUpIfDreaming_dreaming_woken() {
+        // GIVEN device is dreaming
+        whenever(statusBarStateController.isDreaming).thenReturn(true)
+
+        // WHEN wakeUpIfDreaming is called
+        underTest.wakeUpIfDreaming("testReason", PowerManager.WAKE_REASON_GESTURE)
+
+        // THEN device is woken up
+        assertThat(repository.lastWakeWhy).isEqualTo("testReason")
+        assertThat(repository.lastWakeReason).isEqualTo(PowerManager.WAKE_REASON_GESTURE)
+    }
+
+    @Test
+    fun wakeUpIfDreaming_notDreaming_notWoken() {
+        // GIVEN device is not dreaming
+        whenever(statusBarStateController.isDreaming).thenReturn(false)
+
+        // WHEN wakeUpIfDreaming is called
+        underTest.wakeUpIfDreaming("why", PowerManager.WAKE_REASON_TAP)
+
+        // THEN device is not woken
+        assertThat(repository.lastWakeWhy).isNull()
+        assertThat(repository.lastWakeReason).isNull()
+    }
+
     companion object {
         private val IMMEDIATE = Dispatchers.Main.immediate
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
index a0d8f98..9d9d0c7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
@@ -154,6 +154,21 @@
         verify(qsPanel).setCanCollapse(true)
     }
 
+    @Test
+    fun multipleListeningOnlyCallsBrightnessControllerOnce() {
+        controller.setListening(true, true)
+        controller.setListening(true, false)
+        controller.setListening(true, true)
+
+        verify(brightnessController).registerCallbacks()
+
+        controller.setListening(false, true)
+        controller.setListening(false, false)
+        controller.setListening(false, true)
+
+        verify(brightnessController).unregisterCallbacks()
+    }
+
     private fun setShouldUseSplitShade(shouldUse: Boolean) {
         testableResources.addOverride(R.bool.config_use_split_notification_shade, shouldUse)
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt
index 3e9ddcb..5638d70 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt
@@ -28,7 +28,6 @@
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -36,7 +35,6 @@
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 
-@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(JUnit4::class)
 class SystemUiDefaultSceneContainerStartableTest : SysuiTestCase() {
@@ -385,7 +383,7 @@
     ) {
         featureFlags.set(Flags.SCENE_CONTAINER, isFeatureEnabled)
         authenticationRepository.setUnlocked(isDeviceUnlocked)
-        authenticationRepository.setBypassEnabled(isBypassEnabled)
+        keyguardRepository.setBypassEnabled(isBypassEnabled)
         initialSceneKey?.let {
             sceneInteractor.setCurrentScene(SceneContainerNames.SYSTEM_UI_DEFAULT, SceneModel(it))
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessControllerTest.kt
new file mode 100644
index 0000000..2b78405
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessControllerTest.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.settings.brightness
+
+import android.hardware.display.DisplayManager
+import android.os.Handler
+import android.service.vr.IVrManager
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.DisplayTracker
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.settings.FakeSettings
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+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
+class BrightnessControllerTest : SysuiTestCase() {
+
+    private val executor = FakeExecutor(FakeSystemClock())
+    private val secureSettings = FakeSettings()
+    @Mock private lateinit var toggleSlider: ToggleSlider
+    @Mock private lateinit var userTracker: UserTracker
+    @Mock private lateinit var displayTracker: DisplayTracker
+    @Mock private lateinit var displayManager: DisplayManager
+    @Mock private lateinit var iVrManager: IVrManager
+
+    private lateinit var testableLooper: TestableLooper
+
+    private lateinit var underTest: BrightnessController
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        testableLooper = TestableLooper.get(this)
+
+        underTest =
+            BrightnessController(
+                context,
+                toggleSlider,
+                userTracker,
+                displayTracker,
+                displayManager,
+                secureSettings,
+                iVrManager,
+                executor,
+                mock(),
+                Handler(testableLooper.looper)
+            )
+    }
+
+    @Test
+    fun registerCallbacksMultipleTimes_onlyOneRegistration() {
+        val repeats = 100
+        repeat(repeats) { underTest.registerCallbacks() }
+        val messagesProcessed = testableLooper.processMessagesNonBlocking(repeats)
+
+        verify(displayTracker).addBrightnessChangeCallback(any(), any())
+        verify(iVrManager).registerListener(any())
+
+        assertThat(messagesProcessed).isEqualTo(1)
+    }
+
+    @Test
+    fun unregisterCallbacksMultipleTimes_onlyOneUnregistration() {
+        val repeats = 100
+        underTest.registerCallbacks()
+        testableLooper.processAllMessages()
+
+        repeat(repeats) { underTest.unregisterCallbacks() }
+        val messagesProcessed = testableLooper.processMessagesNonBlocking(repeats)
+
+        verify(displayTracker).removeCallback(any())
+        verify(iVrManager).unregisterListener(any())
+
+        assertThat(messagesProcessed).isEqualTo(1)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
index 5c35913..ed1397f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
@@ -18,7 +18,6 @@
 
 import android.content.Intent
 import android.graphics.Rect
-import android.os.Handler
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.View
@@ -29,8 +28,6 @@
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.activity.SingleActivityFactory
-import com.android.systemui.settings.FakeDisplayTracker
-import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
 import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.concurrency.FakeExecutor
@@ -53,28 +50,24 @@
 @TestableLooper.RunWithLooper
 class BrightnessDialogTest : SysuiTestCase() {
 
-    @Mock private lateinit var userTracker: UserTracker
     @Mock private lateinit var brightnessSliderControllerFactory: BrightnessSliderController.Factory
-    @Mock private lateinit var backgroundHandler: Handler
     @Mock private lateinit var brightnessSliderController: BrightnessSliderController
+    @Mock private lateinit var brightnessControllerFactory: BrightnessController.Factory
+    @Mock private lateinit var brightnessController: BrightnessController
     @Mock private lateinit var accessibilityMgr: AccessibilityManagerWrapper
 
     private val clock = FakeSystemClock()
     private val mainExecutor = FakeExecutor(clock)
 
-    private var displayTracker = FakeDisplayTracker(mContext)
-
     @Rule
     @JvmField
     var activityRule =
         ActivityTestRule(
             /* activityFactory= */ SingleActivityFactory {
                 TestDialog(
-                    userTracker,
-                    displayTracker,
                     brightnessSliderControllerFactory,
+                    brightnessControllerFactory,
                     mainExecutor,
-                    backgroundHandler,
                     accessibilityMgr
                 )
             },
@@ -88,6 +81,7 @@
         `when`(brightnessSliderControllerFactory.create(any(), any()))
             .thenReturn(brightnessSliderController)
         `when`(brightnessSliderController.rootView).thenReturn(View(context))
+        `when`(brightnessControllerFactory.create(any())).thenReturn(brightnessController)
     }
 
     @After
@@ -178,19 +172,15 @@
     }
 
     class TestDialog(
-        userTracker: UserTracker,
-        displayTracker: FakeDisplayTracker,
         brightnessSliderControllerFactory: BrightnessSliderController.Factory,
+        brightnessControllerFactory: BrightnessController.Factory,
         mainExecutor: DelayableExecutor,
-        backgroundHandler: Handler,
         accessibilityMgr: AccessibilityManagerWrapper
     ) :
         BrightnessDialog(
-            userTracker,
-            displayTracker,
             brightnessSliderControllerFactory,
+            brightnessControllerFactory,
             mainExecutor,
-            backgroundHandler,
             accessibilityMgr
         )
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LockscreenHostedDreamGestureListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LockscreenHostedDreamGestureListenerTest.kt
new file mode 100644
index 0000000..24d62fb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LockscreenHostedDreamGestureListenerTest.kt
@@ -0,0 +1,189 @@
+/*
+ * 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.shade
+
+import android.os.PowerManager
+import android.testing.AndroidTestingRunner
+import android.view.MotionEvent
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.power.data.repository.FakePowerRepository
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.phone.ScreenOffAnimationController
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidTestingRunner::class)
+class LockscreenHostedDreamGestureListenerTest : SysuiTestCase() {
+    @Mock private lateinit var falsingManager: FalsingManager
+    @Mock private lateinit var falsingCollector: FalsingCollector
+    @Mock private lateinit var statusBarStateController: StatusBarStateController
+    @Mock private lateinit var shadeLogger: ShadeLogger
+    @Mock private lateinit var screenOffAnimationController: ScreenOffAnimationController
+    @Mock private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor
+
+    private val testDispatcher = UnconfinedTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
+
+    private lateinit var powerRepository: FakePowerRepository
+    private lateinit var keyguardRepository: FakeKeyguardRepository
+    private lateinit var underTest: LockscreenHostedDreamGestureListener
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        powerRepository = FakePowerRepository()
+        keyguardRepository = FakeKeyguardRepository()
+
+        underTest =
+            LockscreenHostedDreamGestureListener(
+                falsingManager,
+                PowerInteractor(
+                    powerRepository,
+                    keyguardRepository,
+                    falsingCollector,
+                    screenOffAnimationController,
+                    statusBarStateController,
+                ),
+                statusBarStateController,
+                primaryBouncerInteractor,
+                keyguardRepository,
+                shadeLogger,
+            )
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+        whenever(primaryBouncerInteractor.isBouncerShowing()).thenReturn(false)
+    }
+
+    @Test
+    fun testGestureDetector_onSingleTap_whileDreaming() =
+        testScope.runTest {
+            // GIVEN device dreaming and the dream is hosted in lockscreen
+            whenever(statusBarStateController.isDreaming).thenReturn(true)
+            keyguardRepository.setIsActiveDreamLockscreenHosted(true)
+            testScope.runCurrent()
+
+            // GIVEN the falsing manager does NOT think the tap is a false tap
+            whenever(falsingManager.isFalseTap(ArgumentMatchers.anyInt())).thenReturn(false)
+
+            // WHEN there's a tap
+            underTest.onSingleTapUp(upEv)
+
+            // THEN wake up device if dreaming
+            Truth.assertThat(powerRepository.lastWakeWhy).isNotNull()
+            Truth.assertThat(powerRepository.lastWakeReason).isEqualTo(PowerManager.WAKE_REASON_TAP)
+        }
+
+    @Test
+    fun testGestureDetector_onSingleTap_notOnKeyguard() =
+        testScope.runTest {
+            // GIVEN device dreaming and the dream is hosted in lockscreen
+            whenever(statusBarStateController.isDreaming).thenReturn(true)
+            keyguardRepository.setIsActiveDreamLockscreenHosted(true)
+            testScope.runCurrent()
+
+            // GIVEN shade is open
+            whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
+
+            // GIVEN the falsing manager does NOT think the tap is a false tap
+            whenever(falsingManager.isFalseTap(ArgumentMatchers.anyInt())).thenReturn(false)
+
+            // WHEN there's a tap
+            underTest.onSingleTapUp(upEv)
+
+            // THEN the falsing manager never gets a call
+            verify(falsingManager, never()).isFalseTap(ArgumentMatchers.anyInt())
+        }
+
+    @Test
+    fun testGestureDetector_onSingleTap_bouncerShown() =
+        testScope.runTest {
+            // GIVEN device dreaming and the dream is hosted in lockscreen
+            whenever(statusBarStateController.isDreaming).thenReturn(true)
+            keyguardRepository.setIsActiveDreamLockscreenHosted(true)
+            testScope.runCurrent()
+
+            // GIVEN bouncer is expanded
+            whenever(primaryBouncerInteractor.isBouncerShowing()).thenReturn(true)
+
+            // GIVEN the falsing manager does NOT think the tap is a false tap
+            whenever(falsingManager.isFalseTap(ArgumentMatchers.anyInt())).thenReturn(false)
+
+            // WHEN there's a tap
+            underTest.onSingleTapUp(upEv)
+
+            // THEN the falsing manager never gets a call
+            verify(falsingManager, never()).isFalseTap(ArgumentMatchers.anyInt())
+        }
+
+    @Test
+    fun testGestureDetector_onSingleTap_falsing() =
+        testScope.runTest {
+            // GIVEN device dreaming and the dream is hosted in lockscreen
+            whenever(statusBarStateController.isDreaming).thenReturn(true)
+            keyguardRepository.setIsActiveDreamLockscreenHosted(true)
+            testScope.runCurrent()
+
+            // GIVEN the falsing manager thinks the tap is a false tap
+            whenever(falsingManager.isFalseTap(ArgumentMatchers.anyInt())).thenReturn(true)
+
+            // WHEN there's a tap
+            underTest.onSingleTapUp(upEv)
+
+            // THEN the device doesn't wake up
+            Truth.assertThat(powerRepository.lastWakeWhy).isNull()
+            Truth.assertThat(powerRepository.lastWakeReason).isNull()
+        }
+
+    @Test
+    fun testSingleTap_notDreaming_noFalsingCheck() =
+        testScope.runTest {
+            // GIVEN device not dreaming with lockscreen hosted dream
+            whenever(statusBarStateController.isDreaming).thenReturn(false)
+            keyguardRepository.setIsActiveDreamLockscreenHosted(false)
+            testScope.runCurrent()
+
+            // WHEN there's a tap
+            underTest.onSingleTapUp(upEv)
+
+            // THEN the falsing manager never gets a call
+            verify(falsingManager, never()).isFalseTap(ArgumentMatchers.anyInt())
+        }
+}
+
+private val upEv = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 0f, 0f, 0)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt
deleted file mode 100644
index 168cbb7..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt
+++ /dev/null
@@ -1,512 +0,0 @@
-package com.android.systemui.shade
-
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import android.view.View
-import android.view.ViewGroup
-import android.view.WindowInsets
-import android.view.WindowManagerPolicyConstants
-import androidx.annotation.IdRes
-import androidx.constraintlayout.widget.ConstraintLayout
-import androidx.constraintlayout.widget.ConstraintSet
-import androidx.test.filters.SmallTest
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.fragments.FragmentHostManager
-import com.android.systemui.fragments.FragmentService
-import com.android.systemui.navigationbar.NavigationModeController
-import com.android.systemui.navigationbar.NavigationModeController.ModeChangedListener
-import com.android.systemui.plugins.qs.QS
-import com.android.systemui.recents.OverviewProxyService
-import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener
-import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.time.FakeSystemClock
-import com.google.common.truth.Truth.assertThat
-import java.util.function.Consumer
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.Captor
-import org.mockito.Mock
-import org.mockito.Mockito
-import org.mockito.Mockito.RETURNS_DEEP_STUBS
-import org.mockito.Mockito.any
-import org.mockito.Mockito.anyInt
-import org.mockito.Mockito.doNothing
-import org.mockito.Mockito.eq
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.reset
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-import org.mockito.Mockito.`when` as whenever
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-@TestableLooper.RunWithLooper
-class NotificationQSContainerControllerTest : SysuiTestCase() {
-
-    companion object {
-        const val STABLE_INSET_BOTTOM = 100
-        const val CUTOUT_HEIGHT = 50
-        const val GESTURES_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL
-        const val BUTTONS_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON
-        const val NOTIFICATIONS_MARGIN = 50
-        const val SCRIM_MARGIN = 10
-        const val FOOTER_ACTIONS_INSET = 2
-        const val FOOTER_ACTIONS_PADDING = 2
-        const val FOOTER_ACTIONS_OFFSET = FOOTER_ACTIONS_INSET + FOOTER_ACTIONS_PADDING
-        const val QS_PADDING_OFFSET = SCRIM_MARGIN + FOOTER_ACTIONS_OFFSET
-    }
-
-    @Mock
-    private lateinit var navigationModeController: NavigationModeController
-    @Mock
-    private lateinit var overviewProxyService: OverviewProxyService
-    @Mock
-    private lateinit var notificationsQSContainer: NotificationsQuickSettingsContainer
-    @Mock
-    private lateinit var mShadeHeaderController: ShadeHeaderController
-    @Mock
-    private lateinit var shadeExpansionStateManager: ShadeExpansionStateManager
-    @Mock
-    private lateinit var fragmentService: FragmentService
-    @Mock
-    private lateinit var fragmentHostManager: FragmentHostManager
-    @Captor
-    lateinit var navigationModeCaptor: ArgumentCaptor<ModeChangedListener>
-    @Captor
-    lateinit var taskbarVisibilityCaptor: ArgumentCaptor<OverviewProxyListener>
-    @Captor
-    lateinit var windowInsetsCallbackCaptor: ArgumentCaptor<Consumer<WindowInsets>>
-    @Captor
-    lateinit var constraintSetCaptor: ArgumentCaptor<ConstraintSet>
-    @Captor
-    lateinit var attachStateListenerCaptor: ArgumentCaptor<View.OnAttachStateChangeListener>
-
-    private lateinit var controller: NotificationsQSContainerController
-    private lateinit var navigationModeCallback: ModeChangedListener
-    private lateinit var taskbarVisibilityCallback: OverviewProxyListener
-    private lateinit var windowInsetsCallback: Consumer<WindowInsets>
-    private lateinit var delayableExecutor: FakeExecutor
-    private lateinit var fakeSystemClock: FakeSystemClock
-
-    @Before
-    fun setup() {
-        MockitoAnnotations.initMocks(this)
-        mContext.ensureTestableResources()
-        whenever(notificationsQSContainer.context).thenReturn(mContext)
-        whenever(notificationsQSContainer.resources).thenReturn(mContext.resources)
-        whenever(fragmentService.getFragmentHostManager(any())).thenReturn(fragmentHostManager)
-        fakeSystemClock = FakeSystemClock()
-        delayableExecutor = FakeExecutor(fakeSystemClock)
-
-        controller = NotificationsQSContainerController(
-                notificationsQSContainer,
-                navigationModeController,
-                overviewProxyService,
-                mShadeHeaderController,
-                shadeExpansionStateManager,
-                fragmentService,
-                delayableExecutor
-        )
-
-        overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, SCRIM_MARGIN)
-        overrideResource(R.dimen.notification_panel_margin_bottom, NOTIFICATIONS_MARGIN)
-        overrideResource(R.bool.config_use_split_notification_shade, false)
-        overrideResource(R.dimen.qs_footer_actions_bottom_padding, FOOTER_ACTIONS_PADDING)
-        overrideResource(R.dimen.qs_footer_action_inset, FOOTER_ACTIONS_INSET)
-        whenever(navigationModeController.addListener(navigationModeCaptor.capture()))
-                .thenReturn(GESTURES_NAVIGATION)
-        doNothing().`when`(overviewProxyService).addCallback(taskbarVisibilityCaptor.capture())
-        doNothing().`when`(notificationsQSContainer)
-                .setInsetsChangedListener(windowInsetsCallbackCaptor.capture())
-        doNothing().`when`(notificationsQSContainer).applyConstraints(constraintSetCaptor.capture())
-        doNothing().`when`(notificationsQSContainer)
-            .addOnAttachStateChangeListener(attachStateListenerCaptor.capture())
-        controller.init()
-        attachStateListenerCaptor.value.onViewAttachedToWindow(notificationsQSContainer)
-
-        navigationModeCallback = navigationModeCaptor.value
-        taskbarVisibilityCallback = taskbarVisibilityCaptor.value
-        windowInsetsCallback = windowInsetsCallbackCaptor.value
-    }
-
-    @Test
-    fun testTaskbarVisibleInSplitShade() {
-        enableSplitShade()
-
-        given(taskbarVisible = true,
-                navigationMode = GESTURES_NAVIGATION,
-                insets = windowInsets().withStableBottom())
-        then(expectedContainerPadding = 0, // taskbar should disappear when shade is expanded
-                expectedNotificationsMargin = NOTIFICATIONS_MARGIN,
-                expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET)
-
-        given(taskbarVisible = true,
-                navigationMode = BUTTONS_NAVIGATION,
-                insets = windowInsets().withStableBottom())
-        then(expectedContainerPadding = STABLE_INSET_BOTTOM,
-                expectedNotificationsMargin = NOTIFICATIONS_MARGIN,
-                expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET)
-    }
-
-    @Test
-    fun testTaskbarNotVisibleInSplitShade() {
-        // when taskbar is not visible, it means we're on the home screen
-        enableSplitShade()
-
-        given(taskbarVisible = false,
-                navigationMode = GESTURES_NAVIGATION,
-                insets = windowInsets().withStableBottom())
-        then(expectedContainerPadding = 0,
-                expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET)
-
-        given(taskbarVisible = false,
-                navigationMode = BUTTONS_NAVIGATION,
-                insets = windowInsets().withStableBottom())
-        then(expectedContainerPadding = 0, // qs goes full height as it's not obscuring nav buttons
-                expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN,
-                expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET)
-    }
-
-    @Test
-    fun testTaskbarNotVisibleInSplitShadeWithCutout() {
-        enableSplitShade()
-
-        given(taskbarVisible = false,
-                navigationMode = GESTURES_NAVIGATION,
-                insets = windowInsets().withCutout())
-        then(expectedContainerPadding = CUTOUT_HEIGHT,
-            expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET)
-
-        given(taskbarVisible = false,
-                navigationMode = BUTTONS_NAVIGATION,
-                insets = windowInsets().withCutout().withStableBottom())
-        then(expectedContainerPadding = 0,
-                expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN,
-                expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET)
-    }
-
-    @Test
-    fun testTaskbarVisibleInSinglePaneShade() {
-        disableSplitShade()
-
-        given(taskbarVisible = true,
-                navigationMode = GESTURES_NAVIGATION,
-                insets = windowInsets().withStableBottom())
-        then(expectedContainerPadding = 0,
-                expectedQsPadding = STABLE_INSET_BOTTOM)
-
-        given(taskbarVisible = true,
-                navigationMode = BUTTONS_NAVIGATION,
-                insets = windowInsets().withStableBottom())
-        then(expectedContainerPadding = STABLE_INSET_BOTTOM,
-                expectedQsPadding = STABLE_INSET_BOTTOM)
-    }
-
-    @Test
-    fun testTaskbarNotVisibleInSinglePaneShade() {
-        disableSplitShade()
-
-        given(taskbarVisible = false,
-                navigationMode = GESTURES_NAVIGATION,
-                insets = emptyInsets())
-        then(expectedContainerPadding = 0)
-
-        given(taskbarVisible = false,
-                navigationMode = GESTURES_NAVIGATION,
-                insets = windowInsets().withCutout().withStableBottom())
-        then(expectedContainerPadding = CUTOUT_HEIGHT, expectedQsPadding = STABLE_INSET_BOTTOM)
-
-        given(taskbarVisible = false,
-                navigationMode = BUTTONS_NAVIGATION,
-                insets = windowInsets().withStableBottom())
-        then(expectedContainerPadding = 0,
-                expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN,
-                expectedQsPadding = STABLE_INSET_BOTTOM)
-    }
-
-    @Test
-    fun testDetailShowingInSinglePaneShade() {
-        disableSplitShade()
-        controller.setDetailShowing(true)
-
-        // always sets spacings to 0
-        given(taskbarVisible = false,
-                navigationMode = GESTURES_NAVIGATION,
-                insets = windowInsets().withStableBottom())
-        then(expectedContainerPadding = 0,
-                expectedNotificationsMargin = 0)
-
-        given(taskbarVisible = false,
-                navigationMode = BUTTONS_NAVIGATION,
-                insets = emptyInsets())
-        then(expectedContainerPadding = 0,
-                expectedNotificationsMargin = 0)
-    }
-
-    @Test
-    fun testDetailShowingInSplitShade() {
-        enableSplitShade()
-        controller.setDetailShowing(true)
-
-        given(taskbarVisible = false,
-                navigationMode = GESTURES_NAVIGATION,
-                insets = windowInsets().withStableBottom())
-        then(expectedContainerPadding = 0)
-
-        // should not influence spacing
-        given(taskbarVisible = false,
-                navigationMode = BUTTONS_NAVIGATION,
-                insets = emptyInsets())
-        then(expectedContainerPadding = 0)
-    }
-
-    @Test
-    fun testNotificationsMarginBottomIsUpdated() {
-        Mockito.clearInvocations(notificationsQSContainer)
-        enableSplitShade()
-        verify(notificationsQSContainer).setNotificationsMarginBottom(NOTIFICATIONS_MARGIN)
-
-        overrideResource(R.dimen.notification_panel_margin_bottom, 100)
-        disableSplitShade()
-        verify(notificationsQSContainer).setNotificationsMarginBottom(100)
-    }
-
-    @Test
-    fun testSplitShadeLayout_isAlignedToGuideline() {
-        enableSplitShade()
-        controller.updateResources()
-        assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd)
-                .isEqualTo(R.id.qs_edge_guideline)
-        assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).startToStart)
-                .isEqualTo(R.id.qs_edge_guideline)
-    }
-
-    @Test
-    fun testSinglePaneLayout_childrenHaveEqualMargins() {
-        disableSplitShade()
-        controller.updateResources()
-        val qsStartMargin = getConstraintSetLayout(R.id.qs_frame).startMargin
-        val qsEndMargin = getConstraintSetLayout(R.id.qs_frame).endMargin
-        val notifStartMargin = getConstraintSetLayout(R.id.notification_stack_scroller).startMargin
-        val notifEndMargin = getConstraintSetLayout(R.id.notification_stack_scroller).endMargin
-        assertThat(qsStartMargin == qsEndMargin &&
-                notifStartMargin == notifEndMargin &&
-                qsStartMargin == notifStartMargin
-        ).isTrue()
-    }
-
-    @Test
-    fun testSplitShadeLayout_childrenHaveInsideMarginsOfZero() {
-        enableSplitShade()
-        controller.updateResources()
-        assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin).isEqualTo(0)
-        assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).startMargin)
-                .isEqualTo(0)
-    }
-
-    @Test
-    fun testSplitShadeLayout_qsFrameHasHorizontalMarginsOfZero() {
-        enableSplitShade()
-        controller.updateResources()
-        assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin).isEqualTo(0)
-        assertThat(getConstraintSetLayout(R.id.qs_frame).startMargin).isEqualTo(0)
-    }
-
-    @Test
-    fun testLargeScreenLayout_qsAndNotifsTopMarginIsOfHeaderHeight() {
-        setLargeScreen()
-        val largeScreenHeaderHeight = 100
-        overrideResource(R.dimen.large_screen_shade_header_height, largeScreenHeaderHeight)
-
-        controller.updateResources()
-
-        assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin)
-                .isEqualTo(largeScreenHeaderHeight)
-        assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).topMargin)
-                .isEqualTo(largeScreenHeaderHeight)
-    }
-
-    @Test
-    fun testSmallScreenLayout_qsAndNotifsTopMarginIsZero() {
-        setSmallScreen()
-        controller.updateResources()
-        assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin).isEqualTo(0)
-        assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).topMargin)
-                .isEqualTo(0)
-    }
-
-    @Test
-    fun testSinglePaneShadeLayout_qsFrameHasHorizontalMarginsSetToCorrectValue() {
-        disableSplitShade()
-        controller.updateResources()
-        val notificationPanelMarginHorizontal = context.resources
-                .getDimensionPixelSize(R.dimen.notification_panel_margin_horizontal)
-        assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin)
-                .isEqualTo(notificationPanelMarginHorizontal)
-        assertThat(getConstraintSetLayout(R.id.qs_frame).startMargin)
-                .isEqualTo(notificationPanelMarginHorizontal)
-    }
-
-    @Test
-    fun testSinglePaneShadeLayout_isAlignedToParent() {
-        disableSplitShade()
-        controller.updateResources()
-        assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd)
-                .isEqualTo(ConstraintSet.PARENT_ID)
-        assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).startToStart)
-                .isEqualTo(ConstraintSet.PARENT_ID)
-    }
-
-    @Test
-    fun testAllChildrenOfNotificationContainer_haveIds() {
-        // set dimen to 0 to avoid triggering updating bottom spacing
-        overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, 0)
-        val container = NotificationsQuickSettingsContainer(context, null)
-        container.removeAllViews()
-        container.addView(newViewWithId(1))
-        container.addView(newViewWithId(View.NO_ID))
-        val controller = NotificationsQSContainerController(
-                container,
-                navigationModeController,
-                overviewProxyService,
-                mShadeHeaderController,
-                shadeExpansionStateManager,
-                fragmentService,
-                delayableExecutor
-        )
-        controller.updateConstraints()
-
-        assertThat(container.getChildAt(0).id).isEqualTo(1)
-        assertThat(container.getChildAt(1).id).isNotEqualTo(View.NO_ID)
-    }
-
-    @Test
-    fun testWindowInsetDebounce() {
-        disableSplitShade()
-
-        given(taskbarVisible = false,
-            navigationMode = GESTURES_NAVIGATION,
-            insets = emptyInsets(),
-            applyImmediately = false)
-        fakeSystemClock.advanceTime(INSET_DEBOUNCE_MILLIS / 2)
-        windowInsetsCallback.accept(windowInsets().withStableBottom())
-
-        delayableExecutor.advanceClockToLast()
-        delayableExecutor.runAllReady()
-
-        verify(notificationsQSContainer, never()).setQSContainerPaddingBottom(0)
-        verify(notificationsQSContainer).setQSContainerPaddingBottom(STABLE_INSET_BOTTOM)
-    }
-
-    @Test
-    fun testStartCustomizingWithDuration() {
-        controller.setCustomizerShowing(true, 100L)
-        verify(mShadeHeaderController).startCustomizingAnimation(true, 100L)
-    }
-
-    @Test
-    fun testEndCustomizingWithDuration() {
-        controller.setCustomizerShowing(true, 0L) // Only tracks changes
-        reset(mShadeHeaderController)
-
-        controller.setCustomizerShowing(false, 100L)
-        verify(mShadeHeaderController).startCustomizingAnimation(false, 100L)
-    }
-
-    @Test
-    fun testTagListenerAdded() {
-        verify(fragmentHostManager).addTagListener(eq(QS.TAG), eq(notificationsQSContainer))
-    }
-
-    @Test
-    fun testTagListenerRemoved() {
-        attachStateListenerCaptor.value.onViewDetachedFromWindow(notificationsQSContainer)
-        verify(fragmentHostManager).removeTagListener(eq(QS.TAG), eq(notificationsQSContainer))
-    }
-
-    private fun disableSplitShade() {
-        setSplitShadeEnabled(false)
-    }
-
-    private fun enableSplitShade() {
-        setSplitShadeEnabled(true)
-    }
-
-    private fun setSplitShadeEnabled(enabled: Boolean) {
-        overrideResource(R.bool.config_use_split_notification_shade, enabled)
-        controller.updateResources()
-    }
-
-    private fun setSmallScreen() {
-        setLargeScreenEnabled(false)
-    }
-
-    private fun setLargeScreen() {
-        setLargeScreenEnabled(true)
-    }
-
-    private fun setLargeScreenEnabled(enabled: Boolean) {
-        overrideResource(R.bool.config_use_large_screen_shade_header, enabled)
-    }
-
-    private fun given(
-        taskbarVisible: Boolean,
-        navigationMode: Int,
-        insets: WindowInsets,
-        applyImmediately: Boolean = true
-    ) {
-        Mockito.clearInvocations(notificationsQSContainer)
-        taskbarVisibilityCallback.onTaskbarStatusUpdated(taskbarVisible, false)
-        navigationModeCallback.onNavigationModeChanged(navigationMode)
-        windowInsetsCallback.accept(insets)
-        if (applyImmediately) {
-            delayableExecutor.advanceClockToLast()
-            delayableExecutor.runAllReady()
-        }
-    }
-
-    fun then(
-        expectedContainerPadding: Int,
-        expectedNotificationsMargin: Int = NOTIFICATIONS_MARGIN,
-        expectedQsPadding: Int = 0
-    ) {
-        verify(notificationsQSContainer)
-                .setPadding(anyInt(), anyInt(), anyInt(), eq(expectedContainerPadding))
-        verify(notificationsQSContainer).setNotificationsMarginBottom(expectedNotificationsMargin)
-        verify(notificationsQSContainer)
-                    .setQSContainerPaddingBottom(expectedQsPadding)
-        Mockito.clearInvocations(notificationsQSContainer)
-    }
-
-    private fun windowInsets() = mock(WindowInsets::class.java, RETURNS_DEEP_STUBS)
-
-    private fun emptyInsets() = mock(WindowInsets::class.java)
-
-    private fun WindowInsets.withCutout(): WindowInsets {
-        whenever(displayCutout.safeInsetBottom).thenReturn(CUTOUT_HEIGHT)
-        return this
-    }
-
-    private fun WindowInsets.withStableBottom(): WindowInsets {
-        whenever(stableInsetBottom).thenReturn(STABLE_INSET_BOTTOM)
-        return this
-    }
-
-    private fun getConstraintSetLayout(@IdRes id: Int): ConstraintSet.Layout {
-        return constraintSetCaptor.value.getConstraint(id).layout
-    }
-
-    private fun newViewWithId(id: Int): View {
-        val view = View(mContext)
-        view.id = id
-        val layoutParams = ConstraintLayout.LayoutParams(
-                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
-        // required as cloning ConstraintSet fails if view doesn't have layout params
-        view.layoutParams = layoutParams
-        return view
-    }
-}
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 5fb3a79..2a398c55 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -111,6 +111,8 @@
     @Mock private lateinit var lockIconViewController: LockIconViewController
     @Mock private lateinit var phoneStatusBarViewController: PhoneStatusBarViewController
     @Mock private lateinit var pulsingGestureListener: PulsingGestureListener
+    @Mock
+    private lateinit var mLockscreenHostedDreamGestureListener: LockscreenHostedDreamGestureListener
     @Mock private lateinit var notificationInsetsController: NotificationInsetsController
     @Mock lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory
     @Mock lateinit var keyguardBouncerComponent: KeyguardBouncerComponent
@@ -147,6 +149,7 @@
         featureFlags.set(Flags.DUAL_SHADE, false)
         featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true)
         featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
+        featureFlags.set(Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED, false)
 
         val inputProxy = MultiShadeInputProxy()
         testScope = TestScope()
@@ -183,6 +186,7 @@
                 notificationInsetsController,
                 ambientState,
                 pulsingGestureListener,
+                mLockscreenHostedDreamGestureListener,
                 keyguardBouncerViewModel,
                 keyguardBouncerComponentFactory,
                 mock(KeyguardMessageAreaController.Factory::class.java),
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 544137e..d9eb9b9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
@@ -113,6 +113,8 @@
     @Mock private lateinit var keyguardUnlockAnimationController: KeyguardUnlockAnimationController
     @Mock private lateinit var ambientState: AmbientState
     @Mock private lateinit var pulsingGestureListener: PulsingGestureListener
+    @Mock
+    private lateinit var mLockscreenHostedDreamGestureListener: LockscreenHostedDreamGestureListener
     @Mock private lateinit var keyguardBouncerViewModel: KeyguardBouncerViewModel
     @Mock private lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory
     @Mock private lateinit var keyguardBouncerComponent: KeyguardBouncerComponent
@@ -161,6 +163,7 @@
         featureFlags.set(Flags.DUAL_SHADE, false)
         featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true)
         featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
+        featureFlags.set(Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED, false)
         val inputProxy = MultiShadeInputProxy()
         testScope = TestScope()
         val multiShadeInteractor =
@@ -196,6 +199,7 @@
                 notificationInsetsController,
                 ambientState,
                 pulsingGestureListener,
+                mLockscreenHostedDreamGestureListener,
                 keyguardBouncerViewModel,
                 keyguardBouncerComponentFactory,
                 Mockito.mock(KeyguardMessageAreaController.Factory::class.java),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt
new file mode 100644
index 0000000..2bc112d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt
@@ -0,0 +1,596 @@
+/*
+ * 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.shade
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.testing.TestableResources
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowInsets
+import android.view.WindowManagerPolicyConstants
+import androidx.annotation.IdRes
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.fragments.FragmentHostManager
+import com.android.systemui.fragments.FragmentService
+import com.android.systemui.navigationbar.NavigationModeController
+import com.android.systemui.navigationbar.NavigationModeController.ModeChangedListener
+import com.android.systemui.plugins.qs.QS
+import com.android.systemui.recents.OverviewProxyService
+import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import java.util.function.Consumer
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.RETURNS_DEEP_STUBS
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+/** Uses Flags.MIGRATE_NSSL set to false. If all goes well, this set of tests will be deleted. */
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+@SmallTest
+class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() {
+
+    @Mock lateinit var view: NotificationsQuickSettingsContainer
+    @Mock lateinit var navigationModeController: NavigationModeController
+    @Mock lateinit var overviewProxyService: OverviewProxyService
+    @Mock lateinit var shadeHeaderController: ShadeHeaderController
+    @Mock lateinit var shadeExpansionStateManager: ShadeExpansionStateManager
+    @Mock lateinit var fragmentService: FragmentService
+    @Mock lateinit var fragmentHostManager: FragmentHostManager
+    @Mock
+    lateinit var notificationStackScrollLayoutController: NotificationStackScrollLayoutController
+
+    @Captor lateinit var navigationModeCaptor: ArgumentCaptor<ModeChangedListener>
+    @Captor lateinit var taskbarVisibilityCaptor: ArgumentCaptor<OverviewProxyListener>
+    @Captor lateinit var windowInsetsCallbackCaptor: ArgumentCaptor<Consumer<WindowInsets>>
+    @Captor lateinit var constraintSetCaptor: ArgumentCaptor<ConstraintSet>
+    @Captor lateinit var attachStateListenerCaptor: ArgumentCaptor<View.OnAttachStateChangeListener>
+
+    lateinit var underTest: NotificationsQSContainerController
+
+    private lateinit var fakeResources: TestableResources
+    private lateinit var featureFlags: FakeFeatureFlags
+    private lateinit var navigationModeCallback: ModeChangedListener
+    private lateinit var taskbarVisibilityCallback: OverviewProxyListener
+    private lateinit var windowInsetsCallback: Consumer<WindowInsets>
+    private lateinit var fakeSystemClock: FakeSystemClock
+    private lateinit var delayableExecutor: FakeExecutor
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        fakeSystemClock = FakeSystemClock()
+        delayableExecutor = FakeExecutor(fakeSystemClock)
+        featureFlags = FakeFeatureFlags().apply { set(Flags.MIGRATE_NSSL, false) }
+        mContext.ensureTestableResources()
+        whenever(view.context).thenReturn(mContext)
+        whenever(view.resources).thenReturn(mContext.resources)
+
+        whenever(fragmentService.getFragmentHostManager(any())).thenReturn(fragmentHostManager)
+
+        underTest =
+            NotificationsQSContainerController(
+                view,
+                navigationModeController,
+                overviewProxyService,
+                shadeHeaderController,
+                shadeExpansionStateManager,
+                fragmentService,
+                delayableExecutor,
+                featureFlags,
+                notificationStackScrollLayoutController,
+            )
+
+        overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, SCRIM_MARGIN)
+        overrideResource(R.dimen.notification_panel_margin_bottom, NOTIFICATIONS_MARGIN)
+        overrideResource(R.bool.config_use_split_notification_shade, false)
+        overrideResource(R.dimen.qs_footer_actions_bottom_padding, FOOTER_ACTIONS_PADDING)
+        overrideResource(R.dimen.qs_footer_action_inset, FOOTER_ACTIONS_INSET)
+        whenever(navigationModeController.addListener(navigationModeCaptor.capture()))
+            .thenReturn(GESTURES_NAVIGATION)
+        doNothing().`when`(overviewProxyService).addCallback(taskbarVisibilityCaptor.capture())
+        doNothing().`when`(view).setInsetsChangedListener(windowInsetsCallbackCaptor.capture())
+        doNothing().`when`(view).applyConstraints(constraintSetCaptor.capture())
+        doNothing().`when`(view).addOnAttachStateChangeListener(attachStateListenerCaptor.capture())
+        underTest.init()
+        attachStateListenerCaptor.value.onViewAttachedToWindow(view)
+
+        navigationModeCallback = navigationModeCaptor.value
+        taskbarVisibilityCallback = taskbarVisibilityCaptor.value
+        windowInsetsCallback = windowInsetsCallbackCaptor.value
+
+        Mockito.clearInvocations(view)
+    }
+
+    @Test
+    fun testSmallScreen_updateResources_splitShadeHeightIsSet() {
+        overrideResource(R.bool.config_use_large_screen_shade_header, false)
+        overrideResource(R.dimen.qs_header_height, 1)
+        overrideResource(R.dimen.large_screen_shade_header_height, 2)
+
+        underTest.updateResources()
+
+        val captor = ArgumentCaptor.forClass(ConstraintSet::class.java)
+        verify(view).applyConstraints(capture(captor))
+        assertThat(captor.value.getHeight(R.id.split_shade_status_bar)).isEqualTo(1)
+    }
+
+    @Test
+    fun testLargeScreen_updateResources_splitShadeHeightIsSet() {
+        overrideResource(R.bool.config_use_large_screen_shade_header, true)
+        overrideResource(R.dimen.qs_header_height, 1)
+        overrideResource(R.dimen.large_screen_shade_header_height, 2)
+
+        underTest.updateResources()
+
+        val captor = ArgumentCaptor.forClass(ConstraintSet::class.java)
+        verify(view).applyConstraints(capture(captor))
+        assertThat(captor.value.getHeight(R.id.split_shade_status_bar)).isEqualTo(2)
+    }
+
+    @Test
+    fun testTaskbarVisibleInSplitShade() {
+        enableSplitShade()
+
+        given(
+            taskbarVisible = true,
+            navigationMode = GESTURES_NAVIGATION,
+            insets = windowInsets().withStableBottom()
+        )
+        then(
+            expectedContainerPadding = 0, // taskbar should disappear when shade is expanded
+            expectedNotificationsMargin = NOTIFICATIONS_MARGIN,
+            expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET
+        )
+
+        given(
+            taskbarVisible = true,
+            navigationMode = BUTTONS_NAVIGATION,
+            insets = windowInsets().withStableBottom()
+        )
+        then(
+            expectedContainerPadding = STABLE_INSET_BOTTOM,
+            expectedNotificationsMargin = NOTIFICATIONS_MARGIN,
+            expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET
+        )
+    }
+
+    @Test
+    fun testTaskbarNotVisibleInSplitShade() {
+        // when taskbar is not visible, it means we're on the home screen
+        enableSplitShade()
+
+        given(
+            taskbarVisible = false,
+            navigationMode = GESTURES_NAVIGATION,
+            insets = windowInsets().withStableBottom()
+        )
+        then(
+            expectedContainerPadding = 0,
+            expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET
+        )
+
+        given(
+            taskbarVisible = false,
+            navigationMode = BUTTONS_NAVIGATION,
+            insets = windowInsets().withStableBottom()
+        )
+        then(
+            expectedContainerPadding = 0, // qs goes full height as it's not obscuring nav buttons
+            expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN,
+            expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET
+        )
+    }
+
+    @Test
+    fun testTaskbarNotVisibleInSplitShadeWithCutout() {
+        enableSplitShade()
+
+        given(
+            taskbarVisible = false,
+            navigationMode = GESTURES_NAVIGATION,
+            insets = windowInsets().withCutout()
+        )
+        then(
+            expectedContainerPadding = CUTOUT_HEIGHT,
+            expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET
+        )
+
+        given(
+            taskbarVisible = false,
+            navigationMode = BUTTONS_NAVIGATION,
+            insets = windowInsets().withCutout().withStableBottom()
+        )
+        then(
+            expectedContainerPadding = 0,
+            expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN,
+            expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET
+        )
+    }
+
+    @Test
+    fun testTaskbarVisibleInSinglePaneShade() {
+        disableSplitShade()
+
+        given(
+            taskbarVisible = true,
+            navigationMode = GESTURES_NAVIGATION,
+            insets = windowInsets().withStableBottom()
+        )
+        then(expectedContainerPadding = 0, expectedQsPadding = STABLE_INSET_BOTTOM)
+
+        given(
+            taskbarVisible = true,
+            navigationMode = BUTTONS_NAVIGATION,
+            insets = windowInsets().withStableBottom()
+        )
+        then(
+            expectedContainerPadding = STABLE_INSET_BOTTOM,
+            expectedQsPadding = STABLE_INSET_BOTTOM
+        )
+    }
+
+    @Test
+    fun testTaskbarNotVisibleInSinglePaneShade() {
+        disableSplitShade()
+
+        given(taskbarVisible = false, navigationMode = GESTURES_NAVIGATION, insets = emptyInsets())
+        then(expectedContainerPadding = 0)
+
+        given(
+            taskbarVisible = false,
+            navigationMode = GESTURES_NAVIGATION,
+            insets = windowInsets().withCutout().withStableBottom()
+        )
+        then(expectedContainerPadding = CUTOUT_HEIGHT, expectedQsPadding = STABLE_INSET_BOTTOM)
+
+        given(
+            taskbarVisible = false,
+            navigationMode = BUTTONS_NAVIGATION,
+            insets = windowInsets().withStableBottom()
+        )
+        then(
+            expectedContainerPadding = 0,
+            expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN,
+            expectedQsPadding = STABLE_INSET_BOTTOM
+        )
+    }
+
+    @Test
+    fun testDetailShowingInSinglePaneShade() {
+        disableSplitShade()
+        underTest.setDetailShowing(true)
+
+        // always sets spacings to 0
+        given(
+            taskbarVisible = false,
+            navigationMode = GESTURES_NAVIGATION,
+            insets = windowInsets().withStableBottom()
+        )
+        then(expectedContainerPadding = 0, expectedNotificationsMargin = 0)
+
+        given(taskbarVisible = false, navigationMode = BUTTONS_NAVIGATION, insets = emptyInsets())
+        then(expectedContainerPadding = 0, expectedNotificationsMargin = 0)
+    }
+
+    @Test
+    fun testDetailShowingInSplitShade() {
+        enableSplitShade()
+        underTest.setDetailShowing(true)
+
+        given(
+            taskbarVisible = false,
+            navigationMode = GESTURES_NAVIGATION,
+            insets = windowInsets().withStableBottom()
+        )
+        then(expectedContainerPadding = 0)
+
+        // should not influence spacing
+        given(taskbarVisible = false, navigationMode = BUTTONS_NAVIGATION, insets = emptyInsets())
+        then(expectedContainerPadding = 0)
+    }
+
+    @Test
+    fun testNotificationsMarginBottomIsUpdated() {
+        Mockito.clearInvocations(view)
+        enableSplitShade()
+        verify(view).setNotificationsMarginBottom(NOTIFICATIONS_MARGIN)
+
+        overrideResource(R.dimen.notification_panel_margin_bottom, 100)
+        disableSplitShade()
+        verify(view).setNotificationsMarginBottom(100)
+    }
+
+    @Test
+    fun testSplitShadeLayout_isAlignedToGuideline() {
+        enableSplitShade()
+        underTest.updateResources()
+        assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd).isEqualTo(R.id.qs_edge_guideline)
+        assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).startToStart)
+            .isEqualTo(R.id.qs_edge_guideline)
+    }
+
+    @Test
+    fun testSinglePaneLayout_childrenHaveEqualMargins() {
+        disableSplitShade()
+        underTest.updateResources()
+        val qsStartMargin = getConstraintSetLayout(R.id.qs_frame).startMargin
+        val qsEndMargin = getConstraintSetLayout(R.id.qs_frame).endMargin
+        val notifStartMargin = getConstraintSetLayout(R.id.notification_stack_scroller).startMargin
+        val notifEndMargin = getConstraintSetLayout(R.id.notification_stack_scroller).endMargin
+        assertThat(
+                qsStartMargin == qsEndMargin &&
+                    notifStartMargin == notifEndMargin &&
+                    qsStartMargin == notifStartMargin
+            )
+            .isTrue()
+    }
+
+    @Test
+    fun testSplitShadeLayout_childrenHaveInsideMarginsOfZero() {
+        enableSplitShade()
+        underTest.updateResources()
+        assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin).isEqualTo(0)
+        assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).startMargin)
+            .isEqualTo(0)
+    }
+
+    @Test
+    fun testSplitShadeLayout_qsFrameHasHorizontalMarginsOfZero() {
+        enableSplitShade()
+        underTest.updateResources()
+        assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin).isEqualTo(0)
+        assertThat(getConstraintSetLayout(R.id.qs_frame).startMargin).isEqualTo(0)
+    }
+
+    @Test
+    fun testLargeScreenLayout_qsAndNotifsTopMarginIsOfHeaderHeight() {
+        setLargeScreen()
+        val largeScreenHeaderHeight = 100
+        overrideResource(R.dimen.large_screen_shade_header_height, largeScreenHeaderHeight)
+
+        underTest.updateResources()
+
+        assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin)
+            .isEqualTo(largeScreenHeaderHeight)
+        assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).topMargin)
+            .isEqualTo(largeScreenHeaderHeight)
+    }
+
+    @Test
+    fun testSmallScreenLayout_qsAndNotifsTopMarginIsZero() {
+        setSmallScreen()
+        underTest.updateResources()
+        assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin).isEqualTo(0)
+        assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).topMargin).isEqualTo(0)
+    }
+
+    @Test
+    fun testSinglePaneShadeLayout_qsFrameHasHorizontalMarginsSetToCorrectValue() {
+        disableSplitShade()
+        underTest.updateResources()
+        val notificationPanelMarginHorizontal =
+            mContext.resources.getDimensionPixelSize(R.dimen.notification_panel_margin_horizontal)
+        assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin)
+            .isEqualTo(notificationPanelMarginHorizontal)
+        assertThat(getConstraintSetLayout(R.id.qs_frame).startMargin)
+            .isEqualTo(notificationPanelMarginHorizontal)
+    }
+
+    @Test
+    fun testSinglePaneShadeLayout_isAlignedToParent() {
+        disableSplitShade()
+        underTest.updateResources()
+        assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd)
+            .isEqualTo(ConstraintSet.PARENT_ID)
+        assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).startToStart)
+            .isEqualTo(ConstraintSet.PARENT_ID)
+    }
+
+    @Test
+    fun testAllChildrenOfNotificationContainer_haveIds() {
+        // set dimen to 0 to avoid triggering updating bottom spacing
+        overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, 0)
+        val container = NotificationsQuickSettingsContainer(mContext, null)
+        container.removeAllViews()
+        container.addView(newViewWithId(1))
+        container.addView(newViewWithId(View.NO_ID))
+        val controller =
+            NotificationsQSContainerController(
+                container,
+                navigationModeController,
+                overviewProxyService,
+                shadeHeaderController,
+                shadeExpansionStateManager,
+                fragmentService,
+                delayableExecutor,
+                featureFlags,
+                notificationStackScrollLayoutController,
+            )
+        controller.updateConstraints()
+
+        assertThat(container.getChildAt(0).id).isEqualTo(1)
+        assertThat(container.getChildAt(1).id).isNotEqualTo(View.NO_ID)
+    }
+
+    @Test
+    fun testWindowInsetDebounce() {
+        disableSplitShade()
+
+        given(
+            taskbarVisible = false,
+            navigationMode = GESTURES_NAVIGATION,
+            insets = emptyInsets(),
+            applyImmediately = false
+        )
+        fakeSystemClock.advanceTime(INSET_DEBOUNCE_MILLIS / 2)
+        windowInsetsCallback.accept(windowInsets().withStableBottom())
+
+        delayableExecutor.advanceClockToLast()
+        delayableExecutor.runAllReady()
+
+        verify(view, never()).setQSContainerPaddingBottom(0)
+        verify(view).setQSContainerPaddingBottom(STABLE_INSET_BOTTOM)
+    }
+
+    @Test
+    fun testStartCustomizingWithDuration() {
+        underTest.setCustomizerShowing(true, 100L)
+        verify(shadeHeaderController).startCustomizingAnimation(true, 100L)
+    }
+
+    @Test
+    fun testEndCustomizingWithDuration() {
+        underTest.setCustomizerShowing(true, 0L) // Only tracks changes
+        reset(shadeHeaderController)
+
+        underTest.setCustomizerShowing(false, 100L)
+        verify(shadeHeaderController).startCustomizingAnimation(false, 100L)
+    }
+
+    @Test
+    fun testTagListenerAdded() {
+        verify(fragmentHostManager).addTagListener(eq(QS.TAG), eq(view))
+    }
+
+    @Test
+    fun testTagListenerRemoved() {
+        attachStateListenerCaptor.value.onViewDetachedFromWindow(view)
+        verify(fragmentHostManager).removeTagListener(eq(QS.TAG), eq(view))
+    }
+
+    private fun disableSplitShade() {
+        setSplitShadeEnabled(false)
+    }
+
+    private fun enableSplitShade() {
+        setSplitShadeEnabled(true)
+    }
+
+    private fun setSplitShadeEnabled(enabled: Boolean) {
+        overrideResource(R.bool.config_use_split_notification_shade, enabled)
+        underTest.updateResources()
+    }
+
+    private fun setSmallScreen() {
+        setLargeScreenEnabled(false)
+    }
+
+    private fun setLargeScreen() {
+        setLargeScreenEnabled(true)
+    }
+
+    private fun setLargeScreenEnabled(enabled: Boolean) {
+        overrideResource(R.bool.config_use_large_screen_shade_header, enabled)
+    }
+
+    private fun given(
+        taskbarVisible: Boolean,
+        navigationMode: Int,
+        insets: WindowInsets,
+        applyImmediately: Boolean = true
+    ) {
+        Mockito.clearInvocations(view)
+        taskbarVisibilityCallback.onTaskbarStatusUpdated(taskbarVisible, false)
+        navigationModeCallback.onNavigationModeChanged(navigationMode)
+        windowInsetsCallback.accept(insets)
+        if (applyImmediately) {
+            delayableExecutor.advanceClockToLast()
+            delayableExecutor.runAllReady()
+        }
+    }
+
+    fun then(
+        expectedContainerPadding: Int,
+        expectedNotificationsMargin: Int = NOTIFICATIONS_MARGIN,
+        expectedQsPadding: Int = 0
+    ) {
+        verify(view).setPadding(anyInt(), anyInt(), anyInt(), eq(expectedContainerPadding))
+        verify(view).setNotificationsMarginBottom(expectedNotificationsMargin)
+        verify(view).setQSContainerPaddingBottom(expectedQsPadding)
+        Mockito.clearInvocations(view)
+    }
+
+    private fun windowInsets() = mock(WindowInsets::class.java, RETURNS_DEEP_STUBS)
+
+    private fun emptyInsets() = mock(WindowInsets::class.java)
+
+    private fun WindowInsets.withCutout(): WindowInsets {
+        whenever(displayCutout.safeInsetBottom).thenReturn(CUTOUT_HEIGHT)
+        return this
+    }
+
+    private fun WindowInsets.withStableBottom(): WindowInsets {
+        whenever(stableInsetBottom).thenReturn(STABLE_INSET_BOTTOM)
+        return this
+    }
+
+    private fun getConstraintSetLayout(@IdRes id: Int): ConstraintSet.Layout {
+        return constraintSetCaptor.value.getConstraint(id).layout
+    }
+
+    private fun newViewWithId(id: Int): View {
+        val view = View(mContext)
+        view.id = id
+        val layoutParams =
+            ConstraintLayout.LayoutParams(
+                ViewGroup.LayoutParams.WRAP_CONTENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT
+            )
+        // required as cloning ConstraintSet fails if view doesn't have layout params
+        view.layoutParams = layoutParams
+        return view
+    }
+
+    companion object {
+        const val STABLE_INSET_BOTTOM = 100
+        const val CUTOUT_HEIGHT = 50
+        const val GESTURES_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL
+        const val BUTTONS_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON
+        const val NOTIFICATIONS_MARGIN = 50
+        const val SCRIM_MARGIN = 10
+        const val FOOTER_ACTIONS_INSET = 2
+        const val FOOTER_ACTIONS_PADDING = 2
+        const val FOOTER_ACTIONS_OFFSET = FOOTER_ACTIONS_INSET + FOOTER_ACTIONS_PADDING
+        const val QS_PADDING_OFFSET = SCRIM_MARGIN + FOOTER_ACTIONS_OFFSET
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt
index d4751c8..a504818 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt
@@ -19,24 +19,47 @@
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.testing.TestableResources
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowInsets
+import android.view.WindowManagerPolicyConstants
+import androidx.annotation.IdRes
+import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.fragments.FragmentHostManager
 import com.android.systemui.fragments.FragmentService
 import com.android.systemui.navigationbar.NavigationModeController
+import com.android.systemui.navigationbar.NavigationModeController.ModeChangedListener
+import com.android.systemui.plugins.qs.QS
 import com.android.systemui.recents.OverviewProxyService
-import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
+import java.util.function.Consumer
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
+import org.mockito.Captor
 import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.RETURNS_DEEP_STUBS
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
@@ -51,19 +74,37 @@
     @Mock lateinit var shadeHeaderController: ShadeHeaderController
     @Mock lateinit var shadeExpansionStateManager: ShadeExpansionStateManager
     @Mock lateinit var fragmentService: FragmentService
+    @Mock lateinit var fragmentHostManager: FragmentHostManager
+    @Mock
+    lateinit var notificationStackScrollLayoutController: NotificationStackScrollLayoutController
+
+    @Captor lateinit var navigationModeCaptor: ArgumentCaptor<ModeChangedListener>
+    @Captor lateinit var taskbarVisibilityCaptor: ArgumentCaptor<OverviewProxyListener>
+    @Captor lateinit var windowInsetsCallbackCaptor: ArgumentCaptor<Consumer<WindowInsets>>
+    @Captor lateinit var constraintSetCaptor: ArgumentCaptor<ConstraintSet>
+    @Captor lateinit var attachStateListenerCaptor: ArgumentCaptor<View.OnAttachStateChangeListener>
 
     lateinit var underTest: NotificationsQSContainerController
 
     private lateinit var fakeResources: TestableResources
-
-    private val delayableExecutor: DelayableExecutor = FakeExecutor(FakeSystemClock())
+    private lateinit var featureFlags: FakeFeatureFlags
+    private lateinit var navigationModeCallback: ModeChangedListener
+    private lateinit var taskbarVisibilityCallback: OverviewProxyListener
+    private lateinit var windowInsetsCallback: Consumer<WindowInsets>
+    private lateinit var fakeSystemClock: FakeSystemClock
+    private lateinit var delayableExecutor: FakeExecutor
 
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
-        fakeResources = TestableResources(context.resources)
+        fakeSystemClock = FakeSystemClock()
+        delayableExecutor = FakeExecutor(fakeSystemClock)
+        featureFlags = FakeFeatureFlags().apply { set(Flags.MIGRATE_NSSL, true) }
+        mContext.ensureTestableResources()
+        whenever(view.context).thenReturn(mContext)
+        whenever(view.resources).thenReturn(mContext.resources)
 
-        whenever(view.resources).thenReturn(fakeResources.resources)
+        whenever(fragmentService.getFragmentHostManager(any())).thenReturn(fragmentHostManager)
 
         underTest =
             NotificationsQSContainerController(
@@ -74,16 +115,36 @@
                 shadeExpansionStateManager,
                 fragmentService,
                 delayableExecutor,
+                featureFlags,
+                notificationStackScrollLayoutController,
             )
+
+        overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, SCRIM_MARGIN)
+        overrideResource(R.dimen.notification_panel_margin_bottom, NOTIFICATIONS_MARGIN)
+        overrideResource(R.bool.config_use_split_notification_shade, false)
+        overrideResource(R.dimen.qs_footer_actions_bottom_padding, FOOTER_ACTIONS_PADDING)
+        overrideResource(R.dimen.qs_footer_action_inset, FOOTER_ACTIONS_INSET)
+        whenever(navigationModeController.addListener(navigationModeCaptor.capture()))
+            .thenReturn(GESTURES_NAVIGATION)
+        doNothing().`when`(overviewProxyService).addCallback(taskbarVisibilityCaptor.capture())
+        doNothing().`when`(view).setInsetsChangedListener(windowInsetsCallbackCaptor.capture())
+        doNothing().`when`(view).applyConstraints(constraintSetCaptor.capture())
+        doNothing().`when`(view).addOnAttachStateChangeListener(attachStateListenerCaptor.capture())
+        underTest.init()
+        attachStateListenerCaptor.value.onViewAttachedToWindow(view)
+
+        navigationModeCallback = navigationModeCaptor.value
+        taskbarVisibilityCallback = taskbarVisibilityCaptor.value
+        windowInsetsCallback = windowInsetsCallbackCaptor.value
+
+        Mockito.clearInvocations(view)
     }
 
     @Test
     fun testSmallScreen_updateResources_splitShadeHeightIsSet() {
-        with(fakeResources) {
-            addOverride(R.bool.config_use_large_screen_shade_header, false)
-            addOverride(R.dimen.qs_header_height, 1)
-            addOverride(R.dimen.large_screen_shade_header_height, 2)
-        }
+        overrideResource(R.bool.config_use_large_screen_shade_header, false)
+        overrideResource(R.dimen.qs_header_height, 1)
+        overrideResource(R.dimen.large_screen_shade_header_height, 2)
 
         underTest.updateResources()
 
@@ -94,11 +155,9 @@
 
     @Test
     fun testLargeScreen_updateResources_splitShadeHeightIsSet() {
-        with(fakeResources) {
-            addOverride(R.bool.config_use_large_screen_shade_header, true)
-            addOverride(R.dimen.qs_header_height, 1)
-            addOverride(R.dimen.large_screen_shade_header_height, 2)
-        }
+        overrideResource(R.bool.config_use_large_screen_shade_header, true)
+        overrideResource(R.dimen.qs_header_height, 1)
+        overrideResource(R.dimen.large_screen_shade_header_height, 2)
 
         underTest.updateResources()
 
@@ -106,4 +165,415 @@
         verify(view).applyConstraints(capture(captor))
         assertThat(captor.value.getHeight(R.id.split_shade_status_bar)).isEqualTo(2)
     }
+
+    @Test
+    fun testTaskbarVisibleInSplitShade() {
+        enableSplitShade()
+
+        given(
+            taskbarVisible = true,
+            navigationMode = GESTURES_NAVIGATION,
+            insets = windowInsets().withStableBottom()
+        )
+        then(
+            expectedContainerPadding = 0, // taskbar should disappear when shade is expanded
+            expectedNotificationsMargin = NOTIFICATIONS_MARGIN,
+            expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET
+        )
+
+        given(
+            taskbarVisible = true,
+            navigationMode = BUTTONS_NAVIGATION,
+            insets = windowInsets().withStableBottom()
+        )
+        then(
+            expectedContainerPadding = STABLE_INSET_BOTTOM,
+            expectedNotificationsMargin = NOTIFICATIONS_MARGIN,
+            expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET
+        )
+    }
+
+    @Test
+    fun testTaskbarNotVisibleInSplitShade() {
+        // when taskbar is not visible, it means we're on the home screen
+        enableSplitShade()
+
+        given(
+            taskbarVisible = false,
+            navigationMode = GESTURES_NAVIGATION,
+            insets = windowInsets().withStableBottom()
+        )
+        then(
+            expectedContainerPadding = 0,
+            expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET
+        )
+
+        given(
+            taskbarVisible = false,
+            navigationMode = BUTTONS_NAVIGATION,
+            insets = windowInsets().withStableBottom()
+        )
+        then(
+            expectedContainerPadding = 0, // qs goes full height as it's not obscuring nav buttons
+            expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN,
+            expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET
+        )
+    }
+
+    @Test
+    fun testTaskbarNotVisibleInSplitShadeWithCutout() {
+        enableSplitShade()
+
+        given(
+            taskbarVisible = false,
+            navigationMode = GESTURES_NAVIGATION,
+            insets = windowInsets().withCutout()
+        )
+        then(
+            expectedContainerPadding = CUTOUT_HEIGHT,
+            expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET
+        )
+
+        given(
+            taskbarVisible = false,
+            navigationMode = BUTTONS_NAVIGATION,
+            insets = windowInsets().withCutout().withStableBottom()
+        )
+        then(
+            expectedContainerPadding = 0,
+            expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN,
+            expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET
+        )
+    }
+
+    @Test
+    fun testTaskbarVisibleInSinglePaneShade() {
+        disableSplitShade()
+
+        given(
+            taskbarVisible = true,
+            navigationMode = GESTURES_NAVIGATION,
+            insets = windowInsets().withStableBottom()
+        )
+        then(expectedContainerPadding = 0, expectedQsPadding = STABLE_INSET_BOTTOM)
+
+        given(
+            taskbarVisible = true,
+            navigationMode = BUTTONS_NAVIGATION,
+            insets = windowInsets().withStableBottom()
+        )
+        then(
+            expectedContainerPadding = STABLE_INSET_BOTTOM,
+            expectedQsPadding = STABLE_INSET_BOTTOM
+        )
+    }
+
+    @Test
+    fun testTaskbarNotVisibleInSinglePaneShade() {
+        disableSplitShade()
+
+        given(taskbarVisible = false, navigationMode = GESTURES_NAVIGATION, insets = emptyInsets())
+        then(expectedContainerPadding = 0)
+
+        given(
+            taskbarVisible = false,
+            navigationMode = GESTURES_NAVIGATION,
+            insets = windowInsets().withCutout().withStableBottom()
+        )
+        then(expectedContainerPadding = CUTOUT_HEIGHT, expectedQsPadding = STABLE_INSET_BOTTOM)
+
+        given(
+            taskbarVisible = false,
+            navigationMode = BUTTONS_NAVIGATION,
+            insets = windowInsets().withStableBottom()
+        )
+        then(
+            expectedContainerPadding = 0,
+            expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN,
+            expectedQsPadding = STABLE_INSET_BOTTOM
+        )
+    }
+
+    @Test
+    fun testDetailShowingInSinglePaneShade() {
+        disableSplitShade()
+        underTest.setDetailShowing(true)
+
+        // always sets spacings to 0
+        given(
+            taskbarVisible = false,
+            navigationMode = GESTURES_NAVIGATION,
+            insets = windowInsets().withStableBottom()
+        )
+        then(expectedContainerPadding = 0, expectedNotificationsMargin = 0)
+
+        given(taskbarVisible = false, navigationMode = BUTTONS_NAVIGATION, insets = emptyInsets())
+        then(expectedContainerPadding = 0, expectedNotificationsMargin = 0)
+    }
+
+    @Test
+    fun testDetailShowingInSplitShade() {
+        enableSplitShade()
+        underTest.setDetailShowing(true)
+
+        given(
+            taskbarVisible = false,
+            navigationMode = GESTURES_NAVIGATION,
+            insets = windowInsets().withStableBottom()
+        )
+        then(expectedContainerPadding = 0)
+
+        // should not influence spacing
+        given(taskbarVisible = false, navigationMode = BUTTONS_NAVIGATION, insets = emptyInsets())
+        then(expectedContainerPadding = 0)
+    }
+
+    @Test
+    fun testNotificationsMarginBottomIsUpdated() {
+        Mockito.clearInvocations(view)
+        enableSplitShade()
+        verify(view).setNotificationsMarginBottom(NOTIFICATIONS_MARGIN)
+
+        overrideResource(R.dimen.notification_panel_margin_bottom, 100)
+        disableSplitShade()
+        verify(view).setNotificationsMarginBottom(100)
+    }
+
+    @Test
+    fun testSplitShadeLayout_isAlignedToGuideline() {
+        enableSplitShade()
+        underTest.updateResources()
+        assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd).isEqualTo(R.id.qs_edge_guideline)
+    }
+
+    @Test
+    fun testSinglePaneLayout_childrenHaveEqualMargins() {
+        disableSplitShade()
+        underTest.updateResources()
+        val qsStartMargin = getConstraintSetLayout(R.id.qs_frame).startMargin
+        val qsEndMargin = getConstraintSetLayout(R.id.qs_frame).endMargin
+        assertThat(qsStartMargin == qsEndMargin).isTrue()
+    }
+
+    @Test
+    fun testSplitShadeLayout_childrenHaveInsideMarginsOfZero() {
+        enableSplitShade()
+        underTest.updateResources()
+        assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin).isEqualTo(0)
+    }
+
+    @Test
+    fun testSplitShadeLayout_qsFrameHasHorizontalMarginsOfZero() {
+        enableSplitShade()
+        underTest.updateResources()
+        assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin).isEqualTo(0)
+        assertThat(getConstraintSetLayout(R.id.qs_frame).startMargin).isEqualTo(0)
+    }
+
+    @Test
+    fun testLargeScreenLayout_qsAndNotifsTopMarginIsOfHeaderHeight() {
+        setLargeScreen()
+        val largeScreenHeaderHeight = 100
+        overrideResource(R.dimen.large_screen_shade_header_height, largeScreenHeaderHeight)
+
+        underTest.updateResources()
+
+        assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin)
+            .isEqualTo(largeScreenHeaderHeight)
+    }
+
+    @Test
+    fun testSmallScreenLayout_qsAndNotifsTopMarginIsZero() {
+        setSmallScreen()
+        underTest.updateResources()
+        assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin).isEqualTo(0)
+    }
+
+    @Test
+    fun testSinglePaneShadeLayout_qsFrameHasHorizontalMarginsSetToCorrectValue() {
+        disableSplitShade()
+        underTest.updateResources()
+        val notificationPanelMarginHorizontal =
+            mContext.resources.getDimensionPixelSize(R.dimen.notification_panel_margin_horizontal)
+        assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin)
+            .isEqualTo(notificationPanelMarginHorizontal)
+        assertThat(getConstraintSetLayout(R.id.qs_frame).startMargin)
+            .isEqualTo(notificationPanelMarginHorizontal)
+    }
+
+    @Test
+    fun testSinglePaneShadeLayout_isAlignedToParent() {
+        disableSplitShade()
+        underTest.updateResources()
+        assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd)
+            .isEqualTo(ConstraintSet.PARENT_ID)
+    }
+
+    @Test
+    fun testAllChildrenOfNotificationContainer_haveIds() {
+        // set dimen to 0 to avoid triggering updating bottom spacing
+        overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, 0)
+        val container = NotificationsQuickSettingsContainer(mContext, null)
+        container.removeAllViews()
+        container.addView(newViewWithId(1))
+        container.addView(newViewWithId(View.NO_ID))
+        val controller =
+            NotificationsQSContainerController(
+                container,
+                navigationModeController,
+                overviewProxyService,
+                shadeHeaderController,
+                shadeExpansionStateManager,
+                fragmentService,
+                delayableExecutor,
+                featureFlags,
+                notificationStackScrollLayoutController,
+            )
+        controller.updateConstraints()
+
+        assertThat(container.getChildAt(0).id).isEqualTo(1)
+        assertThat(container.getChildAt(1).id).isNotEqualTo(View.NO_ID)
+    }
+
+    @Test
+    fun testWindowInsetDebounce() {
+        disableSplitShade()
+
+        given(
+            taskbarVisible = false,
+            navigationMode = GESTURES_NAVIGATION,
+            insets = emptyInsets(),
+            applyImmediately = false
+        )
+        fakeSystemClock.advanceTime(INSET_DEBOUNCE_MILLIS / 2)
+        windowInsetsCallback.accept(windowInsets().withStableBottom())
+
+        delayableExecutor.advanceClockToLast()
+        delayableExecutor.runAllReady()
+
+        verify(view, never()).setQSContainerPaddingBottom(0)
+        verify(view).setQSContainerPaddingBottom(STABLE_INSET_BOTTOM)
+    }
+
+    @Test
+    fun testStartCustomizingWithDuration() {
+        underTest.setCustomizerShowing(true, 100L)
+        verify(shadeHeaderController).startCustomizingAnimation(true, 100L)
+    }
+
+    @Test
+    fun testEndCustomizingWithDuration() {
+        underTest.setCustomizerShowing(true, 0L) // Only tracks changes
+        reset(shadeHeaderController)
+
+        underTest.setCustomizerShowing(false, 100L)
+        verify(shadeHeaderController).startCustomizingAnimation(false, 100L)
+    }
+
+    @Test
+    fun testTagListenerAdded() {
+        verify(fragmentHostManager).addTagListener(eq(QS.TAG), eq(view))
+    }
+
+    @Test
+    fun testTagListenerRemoved() {
+        attachStateListenerCaptor.value.onViewDetachedFromWindow(view)
+        verify(fragmentHostManager).removeTagListener(eq(QS.TAG), eq(view))
+    }
+
+    private fun disableSplitShade() {
+        setSplitShadeEnabled(false)
+    }
+
+    private fun enableSplitShade() {
+        setSplitShadeEnabled(true)
+    }
+
+    private fun setSplitShadeEnabled(enabled: Boolean) {
+        overrideResource(R.bool.config_use_split_notification_shade, enabled)
+        underTest.updateResources()
+    }
+
+    private fun setSmallScreen() {
+        setLargeScreenEnabled(false)
+    }
+
+    private fun setLargeScreen() {
+        setLargeScreenEnabled(true)
+    }
+
+    private fun setLargeScreenEnabled(enabled: Boolean) {
+        overrideResource(R.bool.config_use_large_screen_shade_header, enabled)
+    }
+
+    private fun given(
+        taskbarVisible: Boolean,
+        navigationMode: Int,
+        insets: WindowInsets,
+        applyImmediately: Boolean = true
+    ) {
+        Mockito.clearInvocations(view)
+        taskbarVisibilityCallback.onTaskbarStatusUpdated(taskbarVisible, false)
+        navigationModeCallback.onNavigationModeChanged(navigationMode)
+        windowInsetsCallback.accept(insets)
+        if (applyImmediately) {
+            delayableExecutor.advanceClockToLast()
+            delayableExecutor.runAllReady()
+        }
+    }
+
+    fun then(
+        expectedContainerPadding: Int,
+        expectedNotificationsMargin: Int = NOTIFICATIONS_MARGIN,
+        expectedQsPadding: Int = 0
+    ) {
+        verify(view).setPadding(anyInt(), anyInt(), anyInt(), eq(expectedContainerPadding))
+        verify(view).setNotificationsMarginBottom(expectedNotificationsMargin)
+        verify(view).setQSContainerPaddingBottom(expectedQsPadding)
+        Mockito.clearInvocations(view)
+    }
+
+    private fun windowInsets() = mock(WindowInsets::class.java, RETURNS_DEEP_STUBS)
+
+    private fun emptyInsets() = mock(WindowInsets::class.java)
+
+    private fun WindowInsets.withCutout(): WindowInsets {
+        whenever(displayCutout.safeInsetBottom).thenReturn(CUTOUT_HEIGHT)
+        return this
+    }
+
+    private fun WindowInsets.withStableBottom(): WindowInsets {
+        whenever(stableInsetBottom).thenReturn(STABLE_INSET_BOTTOM)
+        return this
+    }
+
+    private fun getConstraintSetLayout(@IdRes id: Int): ConstraintSet.Layout {
+        return constraintSetCaptor.value.getConstraint(id).layout
+    }
+
+    private fun newViewWithId(id: Int): View {
+        val view = View(mContext)
+        view.id = id
+        val layoutParams =
+            ConstraintLayout.LayoutParams(
+                ViewGroup.LayoutParams.WRAP_CONTENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT
+            )
+        // required as cloning ConstraintSet fails if view doesn't have layout params
+        view.layoutParams = layoutParams
+        return view
+    }
+
+    companion object {
+        const val STABLE_INSET_BOTTOM = 100
+        const val CUTOUT_HEIGHT = 50
+        const val GESTURES_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL
+        const val BUTTONS_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON
+        const val NOTIFICATIONS_MARGIN = 50
+        const val SCRIM_MARGIN = 10
+        const val FOOTER_ACTIONS_INSET = 2
+        const val FOOTER_ACTIONS_PADDING = 2
+        const val FOOTER_ACTIONS_OFFSET = FOOTER_ACTIONS_INSET + FOOTER_ACTIONS_PADDING
+        const val QS_PADDING_OFFSET = SCRIM_MARGIN + FOOTER_ACTIONS_OFFSET
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DreamCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DreamCoordinatorTest.kt
new file mode 100644
index 0000000..a544cad
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DreamCoordinatorTest.kt
@@ -0,0 +1,166 @@
+/*
+ * 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.
+ */
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.statusbar.notification.collection.coordinator
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.SysuiStatusBarStateController
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.withArgCaptor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DreamCoordinatorTest : SysuiTestCase() {
+    @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
+    @Mock private lateinit var notifPipeline: NotifPipeline
+    @Mock private lateinit var filterListener: Pluggable.PluggableListener<NotifFilter>
+
+    private val keyguardRepository = FakeKeyguardRepository()
+    private var fakeEntry: NotificationEntry = NotificationEntryBuilder().build()
+    val testDispatcher = UnconfinedTestDispatcher()
+    val testScope = TestScope(testDispatcher)
+
+    private lateinit var filter: NotifFilter
+    private lateinit var statusBarListener: StatusBarStateController.StateListener
+    private lateinit var dreamCoordinator: DreamCoordinator
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+
+        // Build the coordinator
+        dreamCoordinator =
+            DreamCoordinator(
+                statusBarStateController,
+                testScope.backgroundScope,
+                keyguardRepository
+            )
+
+        // Attach the pipeline and capture the listeners/filters that it registers
+        dreamCoordinator.attach(notifPipeline)
+
+        filter = withArgCaptor { verify(notifPipeline).addPreGroupFilter(capture()) }
+        filter.setInvalidationListener(filterListener)
+
+        statusBarListener = withArgCaptor {
+            verify(statusBarStateController).addCallback(capture())
+        }
+    }
+
+    @Test
+    fun hideNotifications_whenDreamingAndOnKeyguard() =
+        testScope.runTest {
+            // GIVEN we are on keyguard and not dreaming
+            keyguardRepository.setKeyguardShowing(true)
+            keyguardRepository.setIsActiveDreamLockscreenHosted(false)
+            runCurrent()
+
+            // THEN notifications are not filtered out
+            verifyPipelinesNotInvalidated()
+            assertThat(filter.shouldFilterOut(fakeEntry, 0L)).isFalse()
+
+            // WHEN dreaming starts and the active dream is hosted in lockscreen
+            keyguardRepository.setIsActiveDreamLockscreenHosted(true)
+            runCurrent()
+
+            // THEN pipeline is notified and notifications should all be filtered out
+            verifyPipelinesInvalidated()
+            assertThat(filter.shouldFilterOut(fakeEntry, 0L)).isTrue()
+        }
+
+    @Test
+    fun showNotifications_whenDreamingAndNotOnKeyguard() =
+        testScope.runTest {
+            // GIVEN we are on the keyguard and active dream is hosted in lockscreen
+            keyguardRepository.setKeyguardShowing(true)
+            keyguardRepository.setIsActiveDreamLockscreenHosted(true)
+            runCurrent()
+
+            // THEN pipeline is notified and notifications are all filtered out
+            verifyPipelinesInvalidated()
+            clearPipelineInvocations()
+            assertThat(filter.shouldFilterOut(fakeEntry, 0L)).isTrue()
+
+            // WHEN we are no longer on the keyguard
+            statusBarListener.onStateChanged(StatusBarState.SHADE)
+
+            // THEN pipeline is notified and notifications are not filtered out
+            verifyPipelinesInvalidated()
+            assertThat(filter.shouldFilterOut(fakeEntry, 0L)).isFalse()
+        }
+
+    @Test
+    fun showNotifications_whenOnKeyguardAndNotDreaming() =
+        testScope.runTest {
+            // GIVEN we are on the keyguard and active dream is hosted in lockscreen
+            keyguardRepository.setKeyguardShowing(true)
+            keyguardRepository.setIsActiveDreamLockscreenHosted(true)
+            runCurrent()
+
+            // THEN pipeline is notified and notifications are all filtered out
+            verifyPipelinesInvalidated()
+            clearPipelineInvocations()
+            assertThat(filter.shouldFilterOut(fakeEntry, 0L)).isTrue()
+
+            // WHEN the lockscreen hosted dream stops
+            keyguardRepository.setIsActiveDreamLockscreenHosted(false)
+            runCurrent()
+
+            // THEN pipeline is notified and notifications are not filtered out
+            verifyPipelinesInvalidated()
+            assertThat(filter.shouldFilterOut(fakeEntry, 0L)).isFalse()
+        }
+
+    private fun verifyPipelinesInvalidated() {
+        verify(filterListener).onPluggableInvalidated(eq(filter), any())
+    }
+
+    private fun verifyPipelinesNotInvalidated() {
+        verify(filterListener, never()).onPluggableInvalidated(eq(filter), any())
+    }
+
+    private fun clearPipelineInvocations() {
+        clearInvocations(filterListener)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
index 608778e..1dc8453 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
@@ -27,6 +27,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
@@ -87,6 +88,7 @@
 @RunWithLooper
 public class ExpandableNotificationRowTest extends SysuiTestCase {
 
+    private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
     private NotificationTestHelper mNotificationTestHelper;
     @Rule public MockitoRule mockito = MockitoJUnit.rule();
 
@@ -96,12 +98,10 @@
         mNotificationTestHelper = new NotificationTestHelper(
                 mContext,
                 mDependency,
-                TestableLooper.get(this));
+                TestableLooper.get(this),
+                mFeatureFlags);
         mNotificationTestHelper.setDefaultInflationFlags(FLAG_CONTENT_VIEW_ALL);
-
-        FakeFeatureFlags fakeFeatureFlags = new FakeFeatureFlags();
-        fakeFeatureFlags.set(Flags.SENSITIVE_REVEAL_ANIM, false);
-        mNotificationTestHelper.setFeatureFlags(fakeFeatureFlags);
+        mFeatureFlags.setDefault(Flags.SENSITIVE_REVEAL_ANIM);
     }
 
     @Test
@@ -183,6 +183,14 @@
     }
 
     @Test
+    public void testSetSensitiveOnNotifRowNotifiesOfHeightChange_withOtherFlagValue()
+            throws Exception {
+        FakeFeatureFlags flags = mFeatureFlags;
+        flags.set(Flags.SENSITIVE_REVEAL_ANIM, !flags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM));
+        testSetSensitiveOnNotifRowNotifiesOfHeightChange();
+    }
+
+    @Test
     public void testSetSensitiveOnNotifRowNotifiesOfHeightChange() throws Exception {
         // GIVEN a sensitive notification row that's currently redacted
         ExpandableNotificationRow row = mNotificationTestHelper.createRow();
@@ -199,10 +207,19 @@
         // WHEN the row is set to no longer be sensitive
         row.setSensitive(false, true);
 
+        boolean expectAnimation = mFeatureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM);
         // VERIFY that the height change listener is invoked
         assertThat(row.getShowingLayout()).isSameInstanceAs(row.getPrivateLayout());
         assertThat(row.getIntrinsicHeight()).isGreaterThan(0);
-        verify(listener).onHeightChanged(eq(row), eq(false));
+        verify(listener).onHeightChanged(eq(row), eq(expectAnimation));
+    }
+
+    @Test
+    public void testSetSensitiveOnGroupRowNotifiesOfHeightChange_withOtherFlagValue()
+            throws Exception {
+        FakeFeatureFlags flags = mFeatureFlags;
+        flags.set(Flags.SENSITIVE_REVEAL_ANIM, !flags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM));
+        testSetSensitiveOnGroupRowNotifiesOfHeightChange();
     }
 
     @Test
@@ -222,10 +239,19 @@
         // WHEN the row is set to no longer be sensitive
         group.setSensitive(false, true);
 
+        boolean expectAnimation = mFeatureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM);
         // VERIFY that the height change listener is invoked
         assertThat(group.getShowingLayout()).isSameInstanceAs(group.getPrivateLayout());
         assertThat(group.getIntrinsicHeight()).isGreaterThan(0);
-        verify(listener).onHeightChanged(eq(group), eq(false));
+        verify(listener).onHeightChanged(eq(group), eq(expectAnimation));
+    }
+
+    @Test
+    public void testSetSensitiveOnPublicRowDoesNotNotifyOfHeightChange_withOtherFlagValue()
+            throws Exception {
+        FakeFeatureFlags flags = mFeatureFlags;
+        flags.set(Flags.SENSITIVE_REVEAL_ANIM, !flags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM));
+        testSetSensitiveOnPublicRowDoesNotNotifyOfHeightChange();
     }
 
     @Test
@@ -254,7 +280,7 @@
         assertThat(publicRow.getIntrinsicHeight()).isGreaterThan(0);
         assertThat(publicRow.getPrivateLayout().getMinHeight())
                 .isEqualTo(publicRow.getPublicLayout().getMinHeight());
-        verify(listener, never()).onHeightChanged(eq(publicRow), eq(false));
+        verify(listener, never()).onHeightChanged(eq(publicRow), anyBoolean());
     }
 
     private void measureAndLayout(ExpandableNotificationRow row) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index 1a644d3..d21029d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -48,12 +48,16 @@
 import android.view.LayoutInflater;
 import android.widget.RemoteViews;
 
+import androidx.annotation.NonNull;
+
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.systemui.TestableDependency;
 import com.android.systemui.classifier.FalsingCollectorFake;
 import com.android.systemui.classifier.FalsingManagerFake;
+import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.media.controls.util.MediaFeatureFlag;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -90,6 +94,7 @@
 
 import org.mockito.ArgumentCaptor;
 
+import java.util.Objects;
 import java.util.Optional;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
@@ -130,14 +135,24 @@
     private final NotificationDismissibilityProvider mDismissibilityProvider;
     public final Runnable mFutureDismissalRunnable;
     private @InflationFlag int mDefaultInflationFlags;
-    private FeatureFlags mFeatureFlags;
+    private final FakeFeatureFlags mFeatureFlags;
 
     public NotificationTestHelper(
             Context context,
             TestableDependency dependency,
             TestableLooper testLooper) {
+        this(context, dependency, testLooper, new FakeFeatureFlags());
+    }
+
+    public NotificationTestHelper(
+            Context context,
+            TestableDependency dependency,
+            TestableLooper testLooper,
+            @NonNull FakeFeatureFlags featureFlags) {
         mContext = context;
         mTestLooper = testLooper;
+        mFeatureFlags = Objects.requireNonNull(featureFlags);
+        dependency.injectTestDependency(FeatureFlags.class, mFeatureFlags);
         dependency.injectMockDependency(NotificationMediaManager.class);
         dependency.injectMockDependency(NotificationShadeWindowController.class);
         dependency.injectMockDependency(MediaOutputDialogFactory.class);
@@ -183,17 +198,12 @@
         mFutureDismissalRunnable = mock(Runnable.class);
         when(mOnUserInteractionCallback.registerFutureDismissal(any(), anyInt()))
                 .thenReturn(mFutureDismissalRunnable);
-        mFeatureFlags = mock(FeatureFlags.class);
     }
 
     public void setDefaultInflationFlags(@InflationFlag int defaultInflationFlags) {
         mDefaultInflationFlags = defaultInflationFlags;
     }
 
-    public void setFeatureFlags(FeatureFlags featureFlags) {
-        mFeatureFlags = featureFlags;
-    }
-
     public ExpandableNotificationRowLogger getMockLogger() {
         return mMockLogger;
     }
@@ -527,6 +537,10 @@
             @InflationFlag int extraInflationFlags,
             int importance)
             throws Exception {
+        // NOTE: This flag is read when the ExpandableNotificationRow is inflated, so it needs to be
+        //  set, but we do not want to override an existing value that is needed by a specific test.
+        mFeatureFlags.setDefault(Flags.IMPROVED_HUN_ANIMATIONS);
+
         LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
                 mContext.LAYOUT_INFLATER_SERVICE);
         mRow = (ExpandableNotificationRow) inflater.inflate(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/TextPrecomputerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/TextPrecomputerTest.kt
new file mode 100644
index 0000000..d46763d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/TextPrecomputerTest.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.row
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.text.PrecomputedText
+import android.text.TextPaint
+import android.widget.TextView
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class TextPrecomputerTest : SysuiTestCase() {
+
+    private lateinit var textPrecomputer: TextPrecomputer
+
+    private lateinit var textView: TextView
+
+    @Before
+    fun before() {
+        textPrecomputer = object : TextPrecomputer {}
+        textView = TextView(mContext)
+    }
+
+    @Test
+    fun precompute_returnRunnable() {
+        // WHEN
+        val precomputeResult = textPrecomputer.precompute(textView, TEXT)
+
+        // THEN
+        assertThat(precomputeResult).isInstanceOf(Runnable::class.java)
+    }
+
+    @Test
+    fun precomputeRunnable_anyText_setPrecomputedText() {
+        // WHEN
+        textPrecomputer.precompute(textView, TEXT).run()
+
+        // THEN
+        assertThat(textView.text).isInstanceOf(PrecomputedText::class.java)
+    }
+
+    @Test
+    fun precomputeRunnable_differentPrecomputedTextConfig_notSetPrecomputedText() {
+        // GIVEN
+        val precomputedTextRunnable =
+            textPrecomputer.precompute(textView, TEXT, logException = false)
+
+        // WHEN
+        textView.textMetricsParams = PrecomputedText.Params.Builder(PAINT).build()
+        precomputedTextRunnable.run()
+
+        // THEN
+        assertThat(textView.text).isInstanceOf(String::class.java)
+    }
+
+    @Test
+    fun precomputeRunnable_nullText_setNull() {
+        // GIVEN
+        textView.text = TEXT
+        val precomputedTextRunnable = textPrecomputer.precompute(textView, null)
+
+        // WHEN
+        precomputedTextRunnable.run()
+
+        // THEN
+        assertThat(textView.text).isEqualTo("")
+    }
+
+    private companion object {
+        private val PAINT = TextPaint()
+        private const val TEXT = "Example Notification Test"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
index 09382ec..3d75288 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
@@ -20,7 +20,6 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
@@ -42,7 +41,6 @@
     private val bypassController = StackScrollAlgorithm.BypassController { false }
     private val statusBarKeyguardViewManager = mock<StatusBarKeyguardViewManager>()
     private val largeScreenShadeInterpolator = mock<LargeScreenShadeInterpolator>()
-    private val featureFlags = mock<FeatureFlags>()
 
     private lateinit var sut: AmbientState
 
@@ -55,8 +53,7 @@
                 sectionProvider,
                 bypassController,
                 statusBarKeyguardViewManager,
-                largeScreenShadeInterpolator,
-                featureFlags
+                largeScreenShadeInterpolator
             )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java
index f38881c..4b145d8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java
@@ -30,7 +30,6 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.media.controls.ui.KeyguardMediaController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.StatusBarState;
@@ -65,7 +64,6 @@
     @Mock private SectionHeaderController mPeopleHeaderController;
     @Mock private SectionHeaderController mAlertingHeaderController;
     @Mock private SectionHeaderController mSilentHeaderController;
-    @Mock private FeatureFlags mFeatureFlag;
 
     private NotificationSectionsManager mSectionsManager;
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
index 8d751e3..1dc0ab0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
@@ -9,7 +9,9 @@
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.ShadeInterpolation
+import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator
 import com.android.systemui.statusbar.NotificationShelf
 import com.android.systemui.statusbar.StatusBarIconView
@@ -20,6 +22,7 @@
 import junit.framework.Assert.assertEquals
 import junit.framework.Assert.assertFalse
 import junit.framework.Assert.assertTrue
+import org.junit.Assume.assumeTrue
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -34,22 +37,32 @@
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
 @RunWithLooper
-class NotificationShelfTest : SysuiTestCase() {
+open class NotificationShelfTest : SysuiTestCase() {
+
+    open val useShelfRefactor: Boolean = false
+    open val useSensitiveReveal: Boolean = false
+    private val flags = FakeFeatureFlags()
 
     @Mock
     private lateinit var largeScreenShadeInterpolator: LargeScreenShadeInterpolator
     @Mock
-    private lateinit var flags: FeatureFlags
-    @Mock
     private lateinit var ambientState: AmbientState
     @Mock
     private lateinit var hostLayoutController: NotificationStackScrollLayoutController
+    @Mock
+    private lateinit var hostLayout: NotificationStackScrollLayout
+    @Mock
+    private lateinit var roundnessManager: NotificationRoundnessManager
 
     private lateinit var shelf: NotificationShelf
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
+        mDependency.injectTestDependency(FeatureFlags::class.java, flags)
+        flags.set(Flags.NOTIFICATION_SHELF_REFACTOR, useShelfRefactor)
+        flags.set(Flags.SENSITIVE_REVEAL_ANIM, useSensitiveReveal)
+        flags.setDefault(Flags.IMPROVED_HUN_ANIMATIONS)
         val root = FrameLayout(context)
         shelf = LayoutInflater.from(root.context)
                 .inflate(/* resource = */ R.layout.status_bar_notification_shelf,
@@ -57,10 +70,13 @@
                     /* attachToRoot = */false) as NotificationShelf
 
         whenever(ambientState.largeScreenShadeInterpolator).thenReturn(largeScreenShadeInterpolator)
-        whenever(ambientState.featureFlags).thenReturn(flags)
         whenever(ambientState.isSmallScreen).thenReturn(true)
 
-        shelf.bind(ambientState, /* hostLayoutController */ hostLayoutController)
+        if (useShelfRefactor) {
+            shelf.bind(ambientState, hostLayout, roundnessManager)
+        } else {
+            shelf.bind(ambientState, hostLayoutController)
+        }
         shelf.layout(/* left */ 0, /* top */ 0, /* right */ 30, /* bottom */5)
     }
 
@@ -345,7 +361,7 @@
     @Test
     fun updateState_withNullLastVisibleBackgroundChild_hideShelf() {
         // GIVEN
-        shelf.setSensitiveRevealAnimEnabled(true)
+        assumeTrue(useSensitiveReveal)
         whenever(ambientState.stackY).thenReturn(100f)
         whenever(ambientState.stackHeight).thenReturn(100f)
         val paddingBetweenElements =
@@ -372,7 +388,7 @@
     @Test
     fun updateState_withNullFirstViewInShelf_hideShelf() {
         // GIVEN
-        shelf.setSensitiveRevealAnimEnabled(true)
+        assumeTrue(useSensitiveReveal)
         whenever(ambientState.stackY).thenReturn(100f)
         whenever(ambientState.stackHeight).thenReturn(100f)
         val paddingBetweenElements =
@@ -399,7 +415,7 @@
     @Test
     fun updateState_withCollapsedShade_hideShelf() {
         // GIVEN
-        shelf.setSensitiveRevealAnimEnabled(true)
+        assumeTrue(useSensitiveReveal)
         whenever(ambientState.stackY).thenReturn(100f)
         whenever(ambientState.stackHeight).thenReturn(100f)
         val paddingBetweenElements =
@@ -426,7 +442,7 @@
     @Test
     fun updateState_withHiddenSectionBeforeShelf_hideShelf() {
         // GIVEN
-        shelf.setSensitiveRevealAnimEnabled(true)
+        assumeTrue(useSensitiveReveal)
         whenever(ambientState.stackY).thenReturn(100f)
         whenever(ambientState.stackHeight).thenReturn(100f)
         val paddingBetweenElements =
@@ -486,3 +502,25 @@
         assertEquals(expectedAlpha, shelf.viewState.alpha)
     }
 }
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class NotificationShelfWithRefactorTest : NotificationShelfTest() {
+    override val useShelfRefactor: Boolean = true
+}
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class NotificationShelfWithSensitiveRevealTest : NotificationShelfTest() {
+    override val useSensitiveReveal: Boolean = true
+}
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class NotificationShelfWithBothFlagsTest : NotificationShelfTest() {
+    override val useShelfRefactor: Boolean = true
+    override val useSensitiveReveal: Boolean = true
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index ee8325e..07eadf7c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -54,7 +54,6 @@
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FakeFeatureFlags;
-import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
@@ -119,6 +118,7 @@
 @RunWith(AndroidTestingRunner.class)
 public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase {
 
+    private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
     @Mock private NotificationGutsManager mNotificationGutsManager;
     @Mock private NotificationsController mNotificationsController;
     @Mock private NotificationVisibilityProvider mVisibilityProvider;
@@ -157,7 +157,6 @@
     @Mock private StackStateLogger mStackLogger;
     @Mock private NotificationStackScrollLogger mLogger;
     @Mock private NotificationStackSizeCalculator mNotificationStackSizeCalculator;
-    private FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
     @Mock private NotificationTargetsHelper mNotificationTargetsHelper;
     @Mock private SecureSettings mSecureSettings;
     @Mock private NotificationIconAreaController mIconAreaController;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 8ad271b..72fcdec 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -68,7 +68,9 @@
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator;
 import com.android.systemui.statusbar.EmptyShadeView;
@@ -106,6 +108,7 @@
 @TestableLooper.RunWithLooper
 public class NotificationStackScrollLayoutTest extends SysuiTestCase {
 
+    private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
     private NotificationStackScrollLayout mStackScroller;  // Normally test this
     private NotificationStackScrollLayout mStackScrollerInternal;  // See explanation below
     private AmbientState mAmbientState;
@@ -129,7 +132,6 @@
     @Mock private NotificationStackSizeCalculator mNotificationStackSizeCalculator;
     @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     @Mock private LargeScreenShadeInterpolator mLargeScreenShadeInterpolator;
-    @Mock private FeatureFlags mFeatureFlags;
 
     @Before
     public void setUp() throws Exception {
@@ -143,11 +145,25 @@
                 mNotificationSectionsManager,
                 mBypassController,
                 mStatusBarKeyguardViewManager,
-                mLargeScreenShadeInterpolator,
-                mFeatureFlags
+                mLargeScreenShadeInterpolator
         ));
 
+        // Register the debug flags we use
+        assertFalse(Flags.NSSL_DEBUG_LINES.getDefault());
+        assertFalse(Flags.NSSL_DEBUG_REMOVE_ANIMATION.getDefault());
+        mFeatureFlags.set(Flags.NSSL_DEBUG_LINES, false);
+        mFeatureFlags.set(Flags.NSSL_DEBUG_REMOVE_ANIMATION, false);
+
+        // Register the feature flags we use
+        // TODO: Ideally we wouldn't need to set these unless a test actually reads them,
+        //  and then we would test both configurations, but currently they are all read
+        //  in the constructor.
+        mFeatureFlags.setDefault(Flags.SENSITIVE_REVEAL_ANIM);
+        mFeatureFlags.setDefault(Flags.ANIMATED_NOTIFICATION_SHADE_INSETS);
+        mFeatureFlags.setDefault(Flags.NOTIFICATION_SHELF_REFACTOR);
+
         // Inject dependencies before initializing the layout
+        mDependency.injectTestDependency(FeatureFlags.class, mFeatureFlags);
         mDependency.injectTestDependency(SysuiStatusBarStateController.class, mBarState);
         mDependency.injectMockDependency(ShadeController.class);
         mDependency.injectTestDependency(
@@ -176,13 +192,18 @@
         mStackScrollerInternal.initView(getContext(), mNotificationSwipeHelper,
                 mNotificationStackSizeCalculator);
         mStackScroller = spy(mStackScrollerInternal);
-        mStackScroller.setShelfController(notificationShelfController);
+        if (!mFeatureFlags.isEnabled(Flags.NOTIFICATION_SHELF_REFACTOR)) {
+            mStackScroller.setShelfController(notificationShelfController);
+        }
         mStackScroller.setNotificationsController(mNotificationsController);
         mStackScroller.setEmptyShadeView(mEmptyShadeView);
         when(mStackScrollLayoutController.isHistoryEnabled()).thenReturn(true);
         when(mStackScrollLayoutController.getNotificationRoundnessManager())
                 .thenReturn(mNotificationRoundnessManager);
         mStackScroller.setController(mStackScrollLayoutController);
+        if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_SHELF_REFACTOR)) {
+            mStackScroller.setShelf(mNotificationShelf);
+        }
 
         doNothing().when(mGroupExpansionManager).collapseGroups();
         doNothing().when(mExpandHelper).cancelImmediately();
@@ -899,7 +920,6 @@
     @Test
     public void testWindowInsetAnimationProgress_updatesBottomInset() {
         int bottomImeInset = 100;
-        mStackScrollerInternal.setAnimatedInsetsEnabled(true);
         WindowInsets windowInsets = new WindowInsets.Builder()
                 .setInsets(ime(), Insets.of(0, 0, 0, bottomImeInset)).build();
         ArrayList<WindowInsetsAnimation> windowInsetsAnimations = new ArrayList<>();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
index df65c09..85a2bdd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
@@ -47,6 +47,7 @@
 
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.classifier.FalsingManagerFake;
+import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption;
@@ -83,7 +84,7 @@
     private Handler mHandler;
     private ExpandableNotificationRow mNotificationRow;
     private Runnable mFalsingCheck;
-    private FeatureFlags mFeatureFlags;
+    private final FeatureFlags mFeatureFlags = new FakeFeatureFlags();
 
     private static final int FAKE_ROW_WIDTH = 20;
     private static final int FAKE_ROW_HEIGHT = 20;
@@ -96,7 +97,6 @@
         mCallback = mock(NotificationSwipeHelper.NotificationCallback.class);
         mListener = mock(NotificationMenuRowPlugin.OnMenuEventListener.class);
         mNotificationRoundnessManager = mock(NotificationRoundnessManager.class);
-        mFeatureFlags = mock(FeatureFlags.class);
         mSwipeHelper = spy(new NotificationSwipeHelper(
                 mContext.getResources(),
                 ViewConfiguration.get(mContext),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
index 45725ce..e30947c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
@@ -18,6 +18,7 @@
 @RunWith(AndroidTestingRunner::class)
 @RunWithLooper
 class NotificationTargetsHelperTest : SysuiTestCase() {
+    private val featureFlags = FakeFeatureFlags()
     lateinit var notificationTestHelper: NotificationTestHelper
     private val sectionsManager: NotificationSectionsManager = mock()
     private val stackScrollLayout: NotificationStackScrollLayout = mock()
@@ -26,10 +27,10 @@
     fun setUp() {
         allowTestableLooperAsMainThread()
         notificationTestHelper =
-            NotificationTestHelper(mContext, mDependency, TestableLooper.get(this))
+            NotificationTestHelper(mContext, mDependency, TestableLooper.get(this), featureFlags)
     }
 
-    private fun notificationTargetsHelper() = NotificationTargetsHelper(FakeFeatureFlags())
+    private fun notificationTargetsHelper() = NotificationTargetsHelper(featureFlags)
 
     @Test
     fun targetsForFirstNotificationInGroup() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
index 4c97d20..987861d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
@@ -8,7 +8,6 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.ShadeInterpolation.getContentAlpha
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator
 import com.android.systemui.statusbar.EmptyShadeView
 import com.android.systemui.statusbar.NotificationShelf
@@ -45,7 +44,6 @@
     private val dumpManager = mock<DumpManager>()
     private val mStatusBarKeyguardViewManager = mock<StatusBarKeyguardViewManager>()
     private val notificationShelf = mock<NotificationShelf>()
-    private val featureFlags = mock<FeatureFlags>()
     private val emptyShadeView = EmptyShadeView(context, /* attrs= */ null).apply {
         layout(/* l= */ 0, /* t= */ 0, /* r= */ 100, /* b= */ 100)
     }
@@ -56,7 +54,6 @@
             /* bypassController */ { false },
             mStatusBarKeyguardViewManager,
             largeScreenShadeInterpolator,
-            featureFlags,
         )
 
     private val testableResources = mContext.getOrCreateTestableResources()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorTest.kt
new file mode 100644
index 0000000..7bbb094
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorTest.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.stack.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SharedNotificationContainerInteractorTest : SysuiTestCase() {
+    private lateinit var configurationRepository: FakeConfigurationRepository
+    private lateinit var underTest: SharedNotificationContainerInteractor
+
+    @Before
+    fun setUp() {
+        configurationRepository = FakeConfigurationRepository()
+        underTest =
+            SharedNotificationContainerInteractor(
+                configurationRepository,
+                mContext,
+            )
+    }
+
+    @Test
+    fun validateConfigValues() = runTest {
+        overrideResource(R.bool.config_use_split_notification_shade, true)
+        overrideResource(R.bool.config_use_large_screen_shade_header, false)
+        overrideResource(R.dimen.notification_panel_margin_horizontal, 0)
+        overrideResource(R.dimen.notification_panel_margin_bottom, 10)
+        overrideResource(R.dimen.notification_panel_margin_top, 10)
+        overrideResource(R.dimen.large_screen_shade_header_height, 0)
+
+        val dimens = collectLastValue(underTest.configurationBasedDimensions)
+
+        configurationRepository.onAnyConfigurationChange()
+        runCurrent()
+
+        val lastDimens = dimens()!!
+
+        assertThat(lastDimens.useSplitShade).isTrue()
+        assertThat(lastDimens.useLargeScreenHeader).isFalse()
+        assertThat(lastDimens.marginHorizontal).isEqualTo(0)
+        assertThat(lastDimens.marginBottom).isGreaterThan(0)
+        assertThat(lastDimens.marginTop).isGreaterThan(0)
+        assertThat(lastDimens.marginTopLargeScreen).isEqualTo(0)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
new file mode 100644
index 0000000..afd9954
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -0,0 +1,142 @@
+/*
+ * 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.stack.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SharedNotificationContainerViewModelTest : SysuiTestCase() {
+    private lateinit var configurationRepository: FakeConfigurationRepository
+    private lateinit var sharedNotificationContainerInteractor:
+        SharedNotificationContainerInteractor
+    private lateinit var underTest: SharedNotificationContainerViewModel
+
+    @Before
+    fun setUp() {
+        configurationRepository = FakeConfigurationRepository()
+        sharedNotificationContainerInteractor =
+            SharedNotificationContainerInteractor(
+                configurationRepository,
+                mContext,
+            )
+        underTest = SharedNotificationContainerViewModel(sharedNotificationContainerInteractor)
+    }
+
+    @Test
+    fun validateMarginStartInSplitShade() = runTest {
+        overrideResource(R.bool.config_use_split_notification_shade, true)
+        overrideResource(R.dimen.notification_panel_margin_horizontal, 20)
+
+        val dimens = collectLastValue(underTest.configurationBasedDimensions)
+
+        configurationRepository.onAnyConfigurationChange()
+        runCurrent()
+
+        val lastDimens = dimens()!!
+
+        assertThat(lastDimens.marginStart).isEqualTo(0)
+    }
+
+    @Test
+    fun validateMarginStart() = runTest {
+        overrideResource(R.bool.config_use_split_notification_shade, false)
+        overrideResource(R.dimen.notification_panel_margin_horizontal, 20)
+
+        val dimens = collectLastValue(underTest.configurationBasedDimensions)
+
+        configurationRepository.onAnyConfigurationChange()
+        runCurrent()
+
+        val lastDimens = dimens()!!
+
+        assertThat(lastDimens.marginStart).isEqualTo(20)
+    }
+
+    @Test
+    fun validateMarginEnd() = runTest {
+        overrideResource(R.dimen.notification_panel_margin_horizontal, 50)
+
+        val dimens = collectLastValue(underTest.configurationBasedDimensions)
+
+        configurationRepository.onAnyConfigurationChange()
+        runCurrent()
+
+        val lastDimens = dimens()!!
+
+        assertThat(lastDimens.marginEnd).isEqualTo(50)
+    }
+
+    @Test
+    fun validateMarginBottom() = runTest {
+        overrideResource(R.dimen.notification_panel_margin_bottom, 50)
+
+        val dimens = collectLastValue(underTest.configurationBasedDimensions)
+
+        configurationRepository.onAnyConfigurationChange()
+        runCurrent()
+
+        val lastDimens = dimens()!!
+
+        assertThat(lastDimens.marginBottom).isEqualTo(50)
+    }
+
+    @Test
+    fun validateMarginTopWithLargeScreenHeader() = runTest {
+        overrideResource(R.bool.config_use_large_screen_shade_header, true)
+        overrideResource(R.dimen.large_screen_shade_header_height, 50)
+        overrideResource(R.dimen.notification_panel_margin_top, 0)
+
+        val dimens = collectLastValue(underTest.configurationBasedDimensions)
+
+        configurationRepository.onAnyConfigurationChange()
+        runCurrent()
+
+        val lastDimens = dimens()!!
+
+        assertThat(lastDimens.marginTop).isEqualTo(50)
+    }
+
+    @Test
+    fun validateMarginTop() = runTest {
+        overrideResource(R.bool.config_use_large_screen_shade_header, false)
+        overrideResource(R.dimen.large_screen_shade_header_height, 50)
+        overrideResource(R.dimen.notification_panel_margin_top, 0)
+
+        val dimens = collectLastValue(underTest.configurationBasedDimensions)
+
+        configurationRepository.onAnyConfigurationChange()
+        runCurrent()
+
+        val lastDimens = dimens()!!
+
+        assertThat(lastDimens.marginTop).isEqualTo(0)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
index 89f8bdb..479803e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
@@ -142,7 +142,8 @@
                 mNotificationMediaManager, mWakefulnessLifecycle, mScreenLifecycle,
                 mAuthController, mStatusBarStateController,
                 mSessionTracker, mLatencyTracker, mScreenOffAnimationController, mVibratorHelper,
-                mSystemClock
+                mSystemClock,
+                mStatusBarKeyguardViewManager
         );
         mBiometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager);
         mBiometricUnlockController.addListener(mBiometricUnlockEventsListener);
@@ -464,6 +465,69 @@
     }
 
     @Test
+    public void onSideFingerprintSuccess_dreaming_unlockThenWake() {
+        when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
+        when(mWakefulnessLifecycle.getLastWakeReason())
+                .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON);
+        final ArgumentCaptor<Runnable> afterKeyguardGoneRunnableCaptor =
+                ArgumentCaptor.forClass(Runnable.class);
+        givenDreamingLocked();
+        mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, true);
+
+        // Make sure the BiometricUnlockController has registered a callback for when the keyguard
+        // is gone
+        verify(mStatusBarKeyguardViewManager).addAfterKeyguardGoneRunnable(
+                afterKeyguardGoneRunnableCaptor.capture());
+        // Ensure that the power hasn't been told to wake up yet.
+        verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString());
+        // Check that the keyguard has been told to unlock.
+        verify(mKeyguardViewMediator).onWakeAndUnlocking();
+
+        // Simulate the keyguard disappearing.
+        afterKeyguardGoneRunnableCaptor.getValue().run();
+        // Verify that the power manager has been told to wake up now.
+        verify(mPowerManager).wakeUp(anyLong(), anyInt(), anyString());
+    }
+
+    @Test
+    public void onSideFingerprintSuccess_dreaming_unlockIfStillLockedNotDreaming() {
+        when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
+        when(mWakefulnessLifecycle.getLastWakeReason())
+                .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON);
+        final ArgumentCaptor<Runnable> afterKeyguardGoneRunnableCaptor =
+                ArgumentCaptor.forClass(Runnable.class);
+        givenDreamingLocked();
+        mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, true);
+
+        // Make sure the BiometricUnlockController has registered a callback for when the keyguard
+        // is gone
+        verify(mStatusBarKeyguardViewManager).addAfterKeyguardGoneRunnable(
+                afterKeyguardGoneRunnableCaptor.capture());
+        // Ensure that the power hasn't been told to wake up yet.
+        verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString());
+        // Check that the keyguard has been told to unlock.
+        verify(mKeyguardViewMediator).onWakeAndUnlocking();
+
+        when(mUpdateMonitor.isDreaming()).thenReturn(false);
+        when(mKeyguardStateController.isUnlocked()).thenReturn(false);
+
+        // Simulate the keyguard disappearing.
+        afterKeyguardGoneRunnableCaptor.getValue().run();
+
+        final ArgumentCaptor<Runnable> dismissKeyguardRunnableCaptor =
+                ArgumentCaptor.forClass(Runnable.class);
+        verify(mHandler).post(dismissKeyguardRunnableCaptor.capture());
+
+        // Verify that the power manager was not told to wake up.
+        verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString());
+
+        dismissKeyguardRunnableCaptor.getValue().run();
+        // Verify that the keyguard controller is told to unlock.
+        verify(mStatusBarKeyguardViewManager).notifyKeyguardAuthenticated(eq(false));
+    }
+
+
+    @Test
     public void onSideFingerprintSuccess_oldPowerButtonPress_playHaptic() {
         // GIVEN side fingerprint enrolled, last wake reason was power button
         when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
@@ -537,6 +601,11 @@
         verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean());
     }
 
+    private void givenDreamingLocked() {
+        when(mUpdateMonitor.isDreaming()).thenReturn(true);
+        when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true);
+    }
+
     private void givenFingerprintModeUnlockCollapsing() {
         when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true);
         when(mUpdateMonitor.isDeviceInteractive()).thenReturn(true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index 3d35233..5300851 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -129,7 +129,6 @@
 import com.android.systemui.shade.CameraLauncher;
 import com.android.systemui.shade.NotificationPanelView;
 import com.android.systemui.shade.NotificationPanelViewController;
-import com.android.systemui.shade.NotificationShadeWindowView;
 import com.android.systemui.shade.NotificationShadeWindowViewController;
 import com.android.systemui.shade.QuickSettingsController;
 import com.android.systemui.shade.ShadeController;
@@ -252,7 +251,6 @@
     @Mock private NotificationLogger.ExpansionStateLogger mExpansionStateLogger;
     @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     @Mock private StatusBarSignalPolicy mStatusBarSignalPolicy;
-    @Mock private NotificationShadeWindowView mNotificationShadeWindowView;
     @Mock private BroadcastDispatcher mBroadcastDispatcher;
     @Mock private AssistManager mAssistManager;
     @Mock private NotificationGutsManager mNotificationGutsManager;
@@ -276,6 +274,8 @@
     @Mock private NotificationShadeWindowController mNotificationShadeWindowController;
     @Mock private NotificationIconAreaController mNotificationIconAreaController;
     @Mock private NotificationShadeWindowViewController mNotificationShadeWindowViewController;
+    @Mock private Lazy<NotificationShadeWindowViewController>
+            mNotificationShadeWindowViewControllerLazy;
     @Mock private NotificationShelfController mNotificationShelfController;
     @Mock private DozeParameters mDozeParameters;
     @Mock private Lazy<LockscreenWallpaper> mLockscreenWallpaperLazy;
@@ -428,10 +428,10 @@
         when(mLockscreenWallpaperLazy.get()).thenReturn(mLockscreenWallpaper);
         when(mBiometricUnlockControllerLazy.get()).thenReturn(mBiometricUnlockController);
         when(mCameraLauncherLazy.get()).thenReturn(mCameraLauncher);
+        when(mNotificationShadeWindowViewControllerLazy.get())
+                .thenReturn(mNotificationShadeWindowViewController);
 
         when(mStatusBarComponentFactory.create()).thenReturn(mCentralSurfacesComponent);
-        when(mCentralSurfacesComponent.getNotificationShadeWindowViewController()).thenReturn(
-                mNotificationShadeWindowViewController);
         doAnswer(invocation -> {
             ((Runnable) invocation.getArgument(0)).run();
             return null;
@@ -510,6 +510,7 @@
                 () -> mAssistManager,
                 configurationController,
                 mNotificationShadeWindowController,
+                mNotificationShadeWindowViewControllerLazy,
                 mNotificationShelfController,
                 mStackScrollerController,
                 mDozeParameters,
@@ -586,9 +587,8 @@
         when(mKeyguardViewMediator.getViewMediatorCallback()).thenReturn(
                 mKeyguardVieMediatorCallback);
 
-        // TODO: we should be able to call mCentralSurfaces.start() and have all the below values
-        // initialized automatically and make NPVC private.
-        mCentralSurfaces.mNotificationShadeWindowView = mNotificationShadeWindowView;
+        // TODO(b/277764509): we should be able to call mCentralSurfaces.start() and have all the
+        //  below values initialized automatically.
         mCentralSurfaces.mDozeScrimController = mDozeScrimController;
         mCentralSurfaces.mPresenter = mNotificationPresenter;
         mCentralSurfaces.mKeyguardIndicationController = mKeyguardIndicationController;
@@ -823,8 +823,6 @@
      */
     @Test
     public void testPredictiveBackCallback_invocationCollapsesPanel() {
-        mCentralSurfaces.setNotificationShadeWindowViewController(
-                mNotificationShadeWindowViewController);
         mCentralSurfaces.handleVisibleToUserChanged(true);
         verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback(
                 eq(OnBackInvokedDispatcher.PRIORITY_DEFAULT),
@@ -841,8 +839,6 @@
      */
     @Test
     public void testPredictiveBackAnimation_progressMaxScalesPanel() {
-        mCentralSurfaces.setNotificationShadeWindowViewController(
-                mNotificationShadeWindowViewController);
         mCentralSurfaces.handleVisibleToUserChanged(true);
         verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback(
                 eq(OnBackInvokedDispatcher.PRIORITY_DEFAULT),
@@ -864,8 +860,6 @@
      */
     @Test
     public void testPredictiveBackAnimation_progressMinScalesPanel() {
-        mCentralSurfaces.setNotificationShadeWindowViewController(
-                mNotificationShadeWindowViewController);
         mCentralSurfaces.handleVisibleToUserChanged(true);
         verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback(
                 eq(OnBackInvokedDispatcher.PRIORITY_DEFAULT),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
index 2d96e59..c8ec1bf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
@@ -204,6 +204,7 @@
             userChipViewModel,
             centralSurfacesImpl,
             shadeControllerImpl,
+            shadeViewController,
             shadeLogger,
             viewUtil,
             configurationController
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
index 9c7f619..33144f2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
@@ -64,7 +64,7 @@
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.assist.AssistManager;
 import com.android.systemui.classifier.FalsingCollectorFake;
-import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
@@ -117,6 +117,7 @@
 public class StatusBarNotificationActivityStarterTest extends SysuiTestCase {
 
     private static final int DISPLAY_ID = 0;
+    private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
 
     @Mock
     private AssistManager mAssistManager;
@@ -256,7 +257,7 @@
                         notificationAnimationProvider,
                         mock(LaunchFullScreenIntentProvider.class),
                         mPowerInteractor,
-                        mock(FeatureFlags.class),
+                        mFeatureFlags,
                         mUserTracker
                 );
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
index 5bd6ff4..cd8aaa2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
@@ -42,7 +42,6 @@
 import com.android.systemui.shade.NotificationShadeWindowView;
 import com.android.systemui.shade.QuickSettingsController;
 import com.android.systemui.shade.ShadeController;
-import com.android.systemui.shade.ShadeNotificationPresenter;
 import com.android.systemui.shade.ShadeViewController;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
@@ -52,7 +51,6 @@
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
-import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
 import com.android.systemui.statusbar.notification.collection.render.NotifShadeEventSource;
@@ -86,14 +84,11 @@
             mock(NotificationsInteractor.class);
     private final KeyguardStateController mKeyguardStateController =
             mock(KeyguardStateController.class);
-    private final NotifPipelineFlags mNotifPipelineFlags = mock(NotifPipelineFlags.class);
     private final InitController mInitController = new InitController();
 
     @Before
     public void setup() {
         mMetricsLogger = new FakeMetricsLogger();
-        LockscreenGestureLogger lockscreenGestureLogger = new LockscreenGestureLogger(
-                mMetricsLogger);
         mCommandQueue = new CommandQueue(mContext, new FakeDisplayTracker(mContext));
         mDependency.injectTestDependency(StatusBarStateController.class,
                 mock(SysuiStatusBarStateController.class));
@@ -111,8 +106,6 @@
         when(notificationShadeWindowView.getResources()).thenReturn(mContext.getResources());
 
         ShadeViewController shadeViewController = mock(ShadeViewController.class);
-        when(shadeViewController.getShadeNotificationPresenter())
-                .thenReturn(mock(ShadeNotificationPresenter.class));
         mStatusBarNotificationPresenter = new StatusBarNotificationPresenter(
                 mContext,
                 shadeViewController,
@@ -135,11 +128,9 @@
                 mock(NotifShadeEventSource.class),
                 mock(NotificationMediaManager.class),
                 mock(NotificationGutsManager.class),
-                lockscreenGestureLogger,
                 mInitController,
                 mNotificationInterruptStateProvider,
                 mock(NotificationRemoteInputManager.class),
-                mNotifPipelineFlags,
                 mock(NotificationRemoteInputManager.Callback.class),
                 mock(NotificationListContainer.class));
         mInitController.executePostInitTasks();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
index 391c8ca..7c285b8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
@@ -102,6 +102,8 @@
             "com.android.sysuitest.dummynotificationsender";
     private static final int DUMMY_MESSAGE_APP_ID = Process.LAST_APPLICATION_UID - 1;
 
+    private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
+
     @Mock private RemoteInputController mController;
     @Mock private ShortcutManager mShortcutManager;
     @Mock private RemoteInputQuickSettingsDisabler mRemoteInputQuickSettingsDisabler;
@@ -453,8 +455,7 @@
     private RemoteInputViewController bindController(
             RemoteInputView view,
             NotificationEntry entry) {
-        FakeFeatureFlags fakeFeatureFlags = new FakeFeatureFlags();
-        fakeFeatureFlags.set(Flags.NOTIFICATION_INLINE_REPLY_ANIMATION, true);
+        mFeatureFlags.set(Flags.NOTIFICATION_INLINE_REPLY_ANIMATION, true);
         RemoteInputViewControllerImpl viewController = new RemoteInputViewControllerImpl(
                 view,
                 entry,
@@ -462,7 +463,7 @@
                 mController,
                 mShortcutManager,
                 mUiEventLoggerFake,
-                fakeFeatureFlags
+                mFeatureFlags
                 );
         viewController.bind();
         return viewController;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/WallpaperControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/WallpaperControllerTest.kt
index d8e418a..b13cb72 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/WallpaperControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/WallpaperControllerTest.kt
@@ -26,6 +26,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.util.mockito.eq
+import com.android.systemui.wallpapers.data.repository.FakeWallpaperRepository
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -56,6 +57,7 @@
     private lateinit var viewRootImpl: ViewRootImpl
     @Mock
     private lateinit var windowToken: IBinder
+    private val wallpaperRepository = FakeWallpaperRepository()
 
     @JvmField
     @Rule
@@ -69,7 +71,7 @@
         `when`(root.windowToken).thenReturn(windowToken)
         `when`(root.isAttachedToWindow).thenReturn(true)
 
-        wallaperController = WallpaperController(wallpaperManager)
+        wallaperController = WallpaperController(wallpaperManager, wallpaperRepository)
 
         wallaperController.rootView = root
     }
@@ -90,9 +92,9 @@
 
     @Test
     fun setUnfoldTransitionZoom_defaultUnfoldTransitionIsDisabled_doesNotUpdateWallpaperZoom() {
-        wallaperController.onWallpaperInfoUpdated(createWallpaperInfo(
+        wallpaperRepository.wallpaperInfo.value = createWallpaperInfo(
             useDefaultTransition = false
-        ))
+        )
 
         wallaperController.setUnfoldTransitionZoom(0.5f)
 
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 0c77529..c819108 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.volume;
 
+import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION;
 import static com.android.systemui.volume.Events.DISMISS_REASON_UNKNOWN;
 import static com.android.systemui.volume.Events.SHOW_REASON_UNKNOWN;
 import static com.android.systemui.volume.VolumeDialogControllerImpl.STREAMS;
@@ -51,6 +52,7 @@
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.VolumeDialogController;
@@ -117,6 +119,8 @@
         }
     };
 
+    private FakeFeatureFlags mFeatureFlags;
+
     @Before
     public void setup() throws Exception {
         MockitoAnnotations.initMocks(this);
@@ -132,6 +136,8 @@
 
         mConfigurationController = new FakeConfigurationController();
 
+        mFeatureFlags = new FakeFeatureFlags();
+
         mDialog = new VolumeDialogImpl(
                 getContext(),
                 mVolumeDialogController,
@@ -146,7 +152,8 @@
                 mCsdWarningDialogFactory,
                 mPostureController,
                 mTestableLooper.getLooper(),
-                mDumpManager);
+                mDumpManager,
+                mFeatureFlags);
         mDialog.init(0, null);
         State state = createShellState();
         mDialog.onStateChangedH(state);
@@ -254,6 +261,7 @@
 
     @Test
     public void testVibrateOnRingerChangedToVibrate() {
+        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false);
         final State initialSilentState = new State();
         initialSilentState.ringerModeInternal = AudioManager.RINGER_MODE_SILENT;
 
@@ -274,7 +282,30 @@
     }
 
     @Test
+    public void testControllerDoesNotVibrateOnRingerChangedToVibrate_OnewayAPI_On() {
+        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
+        final State initialSilentState = new State();
+        initialSilentState.ringerModeInternal = AudioManager.RINGER_MODE_SILENT;
+
+        final State vibrateState = new State();
+        vibrateState.ringerModeInternal = AudioManager.RINGER_MODE_VIBRATE;
+
+        // change ringer to silent
+        mDialog.onStateChangedH(initialSilentState);
+
+        // expected: shouldn't call vibrate yet
+        verify(mVolumeDialogController, never()).vibrate(any());
+
+        // changed ringer to vibrate
+        mDialog.onStateChangedH(vibrateState);
+
+        // expected: vibrate method of controller is not used
+        verify(mVolumeDialogController, never()).vibrate(any());
+    }
+
+    @Test
     public void testNoVibrateOnRingerInitialization() {
+        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false);
         final State initialUnsetState = new State();
         initialUnsetState.ringerModeInternal = -1;
 
@@ -292,7 +323,42 @@
     }
 
     @Test
+    public void testControllerDoesNotVibrateOnRingerInitialization_OnewayAPI_On() {
+        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
+        final State initialUnsetState = new State();
+        initialUnsetState.ringerModeInternal = -1;
+
+        // ringer not initialized yet:
+        mDialog.onStateChangedH(initialUnsetState);
+
+        final State vibrateState = new State();
+        vibrateState.ringerModeInternal = AudioManager.RINGER_MODE_VIBRATE;
+
+        // changed ringer to vibrate
+        mDialog.onStateChangedH(vibrateState);
+
+        // shouldn't call vibrate on the controller either
+        verify(mVolumeDialogController, never()).vibrate(any());
+    }
+
+    @Test
     public void testSelectVibrateFromDrawer() {
+        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false);
+        final State initialUnsetState = new State();
+        initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_NORMAL;
+        mDialog.onStateChangedH(initialUnsetState);
+
+        mActiveRinger.performClick();
+        mDrawerVibrate.performClick();
+
+        // Make sure we've actually changed the ringer mode.
+        verify(mVolumeDialogController, times(1)).setRingerMode(
+                AudioManager.RINGER_MODE_VIBRATE, false);
+    }
+
+    @Test
+    public void testSelectVibrateFromDrawer_OnewayAPI_On() {
+        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
         final State initialUnsetState = new State();
         initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_NORMAL;
         mDialog.onStateChangedH(initialUnsetState);
@@ -307,6 +373,22 @@
 
     @Test
     public void testSelectMuteFromDrawer() {
+        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false);
+        final State initialUnsetState = new State();
+        initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_NORMAL;
+        mDialog.onStateChangedH(initialUnsetState);
+
+        mActiveRinger.performClick();
+        mDrawerMute.performClick();
+
+        // Make sure we've actually changed the ringer mode.
+        verify(mVolumeDialogController, times(1)).setRingerMode(
+                AudioManager.RINGER_MODE_SILENT, false);
+    }
+
+    @Test
+    public void testSelectMuteFromDrawer_OnewayAPI_On() {
+        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
         final State initialUnsetState = new State();
         initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_NORMAL;
         mDialog.onStateChangedH(initialUnsetState);
@@ -321,6 +403,22 @@
 
     @Test
     public void testSelectNormalFromDrawer() {
+        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false);
+        final State initialUnsetState = new State();
+        initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_VIBRATE;
+        mDialog.onStateChangedH(initialUnsetState);
+
+        mActiveRinger.performClick();
+        mDrawerNormal.performClick();
+
+        // Make sure we've actually changed the ringer mode.
+        verify(mVolumeDialogController, times(1)).setRingerMode(
+                AudioManager.RINGER_MODE_NORMAL, false);
+    }
+
+    @Test
+    public void testSelectNormalFromDrawer_OnewayAPI_On() {
+        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
         final State initialUnsetState = new State();
         initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_VIBRATE;
         mDialog.onStateChangedH(initialUnsetState);
@@ -383,7 +481,8 @@
                 mCsdWarningDialogFactory,
                 devicePostureController,
                 mTestableLooper.getLooper(),
-                mDumpManager
+                mDumpManager,
+                mFeatureFlags
         );
         dialog.init(0 , null);
 
@@ -423,7 +522,8 @@
                 mCsdWarningDialogFactory,
                 devicePostureController,
                 mTestableLooper.getLooper(),
-                mDumpManager
+                mDumpManager,
+                mFeatureFlags
         );
         dialog.init(0, null);
 
@@ -462,7 +562,8 @@
                 mCsdWarningDialogFactory,
                 devicePostureController,
                 mTestableLooper.getLooper(),
-                mDumpManager
+                mDumpManager,
+                mFeatureFlags
         );
         dialog.init(0, null);
 
@@ -503,7 +604,9 @@
                 mCsdWarningDialogFactory,
                 mPostureController,
                 mTestableLooper.getLooper(),
-                mDumpManager);
+                mDumpManager,
+                mFeatureFlags
+        );
         dialog.init(0, null);
 
         verify(mPostureController, never()).removeCallback(any());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt
index 6fc36b0..fe5024f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt
@@ -16,9 +16,11 @@
 
 package com.android.systemui.wallpapers.data.repository
 
+import android.app.WallpaperInfo
 import kotlinx.coroutines.flow.MutableStateFlow
 
 /** Fake implementation of the wallpaper repository. */
 class FakeWallpaperRepository : WallpaperRepository {
+    override val wallpaperInfo = MutableStateFlow<WallpaperInfo?>(null)
     override val wallpaperSupportsAmbientMode = MutableStateFlow(false)
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt
index 132b9b4..f8b096a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt
@@ -65,6 +65,171 @@
     }
 
     @Test
+    fun wallpaperInfo_nullInfo() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.wallpaperInfo)
+
+            whenever(wallpaperManager.getWallpaperInfoForUser(any())).thenReturn(null)
+
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+                context,
+                Intent(Intent.ACTION_WALLPAPER_CHANGED),
+            )
+
+            assertThat(latest).isNull()
+        }
+
+    @Test
+    fun wallpaperInfo_hasInfoFromManager() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.wallpaperInfo)
+
+            whenever(wallpaperManager.getWallpaperInfoForUser(any())).thenReturn(UNSUPPORTED_WP)
+
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+                context,
+                Intent(Intent.ACTION_WALLPAPER_CHANGED),
+            )
+
+            assertThat(latest).isEqualTo(UNSUPPORTED_WP)
+        }
+
+    @Test
+    fun wallpaperInfo_initialValueIsFetched() =
+        testScope.runTest {
+            whenever(wallpaperManager.getWallpaperInfoForUser(USER_WITH_SUPPORTED_WP.id))
+                .thenReturn(SUPPORTED_WP)
+            userRepository.setUserInfos(listOf(USER_WITH_SUPPORTED_WP))
+            userRepository.setSelectedUserInfo(USER_WITH_SUPPORTED_WP)
+
+            // WHEN the repo initially starts up (underTest is lazy), then it fetches the current
+            // value for the wallpaper
+            assertThat(underTest.wallpaperInfo.value).isEqualTo(SUPPORTED_WP)
+        }
+
+    @Test
+    fun wallpaperInfo_updatesOnUserChanged() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.wallpaperInfo)
+
+            val user3 = UserInfo(/* id= */ 3, /* name= */ "user3", /* flags= */ 0)
+            val user3Wp = mock<WallpaperInfo>()
+            whenever(wallpaperManager.getWallpaperInfoForUser(user3.id)).thenReturn(user3Wp)
+
+            val user4 = UserInfo(/* id= */ 4, /* name= */ "user4", /* flags= */ 0)
+            val user4Wp = mock<WallpaperInfo>()
+            whenever(wallpaperManager.getWallpaperInfoForUser(user4.id)).thenReturn(user4Wp)
+
+            userRepository.setUserInfos(listOf(user3, user4))
+
+            // WHEN user3 is selected
+            userRepository.setSelectedUserInfo(user3)
+
+            // THEN user3's wallpaper is used
+            assertThat(latest).isEqualTo(user3Wp)
+
+            // WHEN the user is switched to user4
+            userRepository.setSelectedUserInfo(user4)
+
+            // THEN user4's wallpaper is used
+            assertThat(latest).isEqualTo(user4Wp)
+        }
+
+    @Test
+    fun wallpaperInfo_doesNotUpdateOnUserChanging() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.wallpaperInfo)
+
+            val user3 = UserInfo(/* id= */ 3, /* name= */ "user3", /* flags= */ 0)
+            val user3Wp = mock<WallpaperInfo>()
+            whenever(wallpaperManager.getWallpaperInfoForUser(user3.id)).thenReturn(user3Wp)
+
+            val user4 = UserInfo(/* id= */ 4, /* name= */ "user4", /* flags= */ 0)
+            val user4Wp = mock<WallpaperInfo>()
+            whenever(wallpaperManager.getWallpaperInfoForUser(user4.id)).thenReturn(user4Wp)
+
+            userRepository.setUserInfos(listOf(user3, user4))
+
+            // WHEN user3 is selected
+            userRepository.setSelectedUserInfo(user3)
+
+            // THEN user3's wallpaper is used
+            assertThat(latest).isEqualTo(user3Wp)
+
+            // WHEN the user has started switching to user4 but hasn't finished yet
+            userRepository.selectedUser.value =
+                SelectedUserModel(user4, SelectionStatus.SELECTION_IN_PROGRESS)
+
+            // THEN the wallpaper still matches user3
+            assertThat(latest).isEqualTo(user3Wp)
+        }
+
+    @Test
+    fun wallpaperInfo_updatesOnIntent() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.wallpaperInfo)
+
+            val wp1 = mock<WallpaperInfo>()
+            whenever(wallpaperManager.getWallpaperInfoForUser(any())).thenReturn(wp1)
+
+            assertThat(latest).isEqualTo(wp1)
+
+            // WHEN the info is new and a broadcast is sent
+            val wp2 = mock<WallpaperInfo>()
+            whenever(wallpaperManager.getWallpaperInfoForUser(any())).thenReturn(wp2)
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+                context,
+                Intent(Intent.ACTION_WALLPAPER_CHANGED),
+            )
+
+            // THEN the flow updates
+            assertThat(latest).isEqualTo(wp2)
+        }
+
+    @Test
+    fun wallpaperInfo_wallpaperNotSupported_alwaysNull() =
+        testScope.runTest {
+            whenever(wallpaperManager.isWallpaperSupported).thenReturn(false)
+
+            val latest by collectLastValue(underTest.wallpaperInfo)
+            assertThat(latest).isNull()
+
+            // Even WHEN there *is* current wallpaper
+            val wp1 = mock<WallpaperInfo>()
+            whenever(wallpaperManager.getWallpaperInfoForUser(any())).thenReturn(wp1)
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+                context,
+                Intent(Intent.ACTION_WALLPAPER_CHANGED),
+            )
+
+            // THEN the value is still null because wallpaper isn't supported
+            assertThat(latest).isNull()
+        }
+
+    @Test
+    fun wallpaperInfo_deviceDoesNotSupportAmbientWallpaper_alwaysFalse() =
+        testScope.runTest {
+            context.orCreateTestableResources.addOverride(
+                com.android.internal.R.bool.config_dozeSupportsAodWallpaper,
+                false
+            )
+
+            val latest by collectLastValue(underTest.wallpaperInfo)
+            assertThat(latest).isNull()
+
+            // Even WHEN there *is* current wallpaper
+            val wp1 = mock<WallpaperInfo>()
+            whenever(wallpaperManager.getWallpaperInfoForUser(any())).thenReturn(wp1)
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+                context,
+                Intent(Intent.ACTION_WALLPAPER_CHANGED),
+            )
+
+            // THEN the value is still null because wallpaper isn't supported
+            assertThat(latest).isNull()
+        }
+
+    @Test
     fun wallpaperSupportsAmbientMode_nullInfo_false() =
         testScope.runTest {
             val latest by collectLastValue(underTest.wallpaperSupportsAmbientMode)
@@ -190,14 +355,12 @@
         testScope.runTest {
             val latest by collectLastValue(underTest.wallpaperSupportsAmbientMode)
 
-            val info: WallpaperInfo = mock()
-            whenever(info.supportsAmbientMode()).thenReturn(false)
-            whenever(wallpaperManager.getWallpaperInfoForUser(any())).thenReturn(info)
+            whenever(wallpaperManager.getWallpaperInfoForUser(any())).thenReturn(UNSUPPORTED_WP)
 
             assertThat(latest).isFalse()
 
             // WHEN the info now supports ambient mode and a broadcast is sent
-            whenever(info.supportsAmbientMode()).thenReturn(true)
+            whenever(wallpaperManager.getWallpaperInfoForUser(any())).thenReturn(SUPPORTED_WP)
             fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
                 context,
                 Intent(Intent.ACTION_WALLPAPER_CHANGED),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 4839eeb..17bb73b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -92,7 +92,7 @@
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -305,6 +305,7 @@
     private TestableLooper mTestableLooper;
 
     private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
+    private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
 
     private UserHandle mUser0;
 
@@ -423,7 +424,7 @@
                 mCommonNotifCollection,
                 mNotifPipeline,
                 mSysUiState,
-                mock(FeatureFlags.class),
+                mFeatureFlags,
                 mNotifPipelineFlags,
                 syncExecutor);
         mBubblesManager.addNotifCallback(mNotifCallback);
@@ -432,7 +433,8 @@
         mNotificationTestHelper = new NotificationTestHelper(
                 mContext,
                 mDependency,
-                TestableLooper.get(this));
+                TestableLooper.get(this),
+                mFeatureFlags);
         mRow = mNotificationTestHelper.createBubble(mDeleteIntent);
         mRow2 = mNotificationTestHelper.createBubble(mDeleteIntent);
         mNonBubbleNotifRow = mNotificationTestHelper.createRow();
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
index 28892ba..c2e1ac7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
@@ -31,9 +31,6 @@
     private val currentTime: () -> Long,
 ) : AuthenticationRepository {
 
-    private val _isBypassEnabled = MutableStateFlow(false)
-    override val isBypassEnabled: StateFlow<Boolean> = _isBypassEnabled
-
     private val _isAutoConfirmEnabled = MutableStateFlow(false)
     override val isAutoConfirmEnabled: StateFlow<Boolean> = _isAutoConfirmEnabled.asStateFlow()
 
@@ -85,10 +82,6 @@
         return (credentialOverride ?: DEFAULT_PIN).size
     }
 
-    override fun setBypassEnabled(isBypassEnabled: Boolean) {
-        _isBypassEnabled.value = isBypassEnabled
-    }
-
     override suspend fun getFailedAuthenticationAttemptCount(): Int {
         return failedAttemptCount
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
index b94f816e..36fa7e6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
@@ -62,6 +62,32 @@
         }
     }
 
+    /**
+     * Set the given flag's default value if no other value has been set.
+     *
+     * REMINDER: You should always test your code with your flag in both configurations, so
+     *  generally you should be setting a particular value.  This method should be reserved for
+     *  situations where the flag needs to be read (e.g. in the class constructor), but its
+     *  value shouldn't affect the actual test cases. In those cases, it's mildly safer to use
+     *  this method than to hard-code `false` or `true` because then at least if you're wrong,
+     *  and the flag value *does* matter, you'll notice when the flag is flipped and tests
+     *  start failing.
+     */
+    fun setDefault(flag: BooleanFlag) = booleanFlags.putIfAbsent(flag.id, flag.default)
+
+    /**
+     * Set the given flag's default value if no other value has been set.
+     *
+     * REMINDER: You should always test your code with your flag in both configurations, so
+     *  generally you should be setting a particular value.  This method should be reserved for
+     *  situations where the flag needs to be read (e.g. in the class constructor), but its
+     *  value shouldn't affect the actual test cases. In those cases, it's mildly safer to use
+     *  this method than to hard-code `false` or `true` because then at least if you're wrong,
+     *  and the flag value *does* matter, you'll notice when the flag is flipped and tests
+     *  start failing.
+     */
+    fun setDefault(flag: SysPropBooleanFlag) = booleanFlags.putIfAbsent(flag.id, flag.default)
+
     private fun notifyFlagChanged(flag: Flag<*>) {
         flagListeners[flag.id]?.let { listeners ->
             listeners.forEach { listener ->
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt
index 2715aaa..548169e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt
@@ -17,8 +17,8 @@
 package com.android.systemui.keyguard.data.repository
 
 import com.android.keyguard.FaceAuthUiEvent
-import com.android.systemui.keyguard.shared.model.AuthenticationStatus
-import com.android.systemui.keyguard.shared.model.DetectionStatus
+import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FaceDetectionStatus
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
@@ -29,16 +29,16 @@
 
     override val isAuthenticated = MutableStateFlow(false)
     override val canRunFaceAuth = MutableStateFlow(false)
-    private val _authenticationStatus = MutableStateFlow<AuthenticationStatus?>(null)
-    override val authenticationStatus: Flow<AuthenticationStatus> =
+    private val _authenticationStatus = MutableStateFlow<FaceAuthenticationStatus?>(null)
+    override val authenticationStatus: Flow<FaceAuthenticationStatus> =
         _authenticationStatus.filterNotNull()
-    fun setAuthenticationStatus(status: AuthenticationStatus) {
+    fun setAuthenticationStatus(status: FaceAuthenticationStatus) {
         _authenticationStatus.value = status
     }
-    private val _detectionStatus = MutableStateFlow<DetectionStatus?>(null)
-    override val detectionStatus: Flow<DetectionStatus>
+    private val _detectionStatus = MutableStateFlow<FaceDetectionStatus?>(null)
+    override val detectionStatus: Flow<FaceDetectionStatus>
         get() = _detectionStatus.filterNotNull()
-    fun setDetectionStatus(status: DetectionStatus) {
+    fun setDetectionStatus(status: FaceDetectionStatus) {
         _detectionStatus.value = status
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt
index 4bfd3d6..38791ca 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt
@@ -17,32 +17,38 @@
 
 package com.android.systemui.keyguard.data.repository
 
+import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.filterNotNull
 
 class FakeDeviceEntryFingerprintAuthRepository : DeviceEntryFingerprintAuthRepository {
     private val _isLockedOut = MutableStateFlow(false)
     override val isLockedOut: StateFlow<Boolean> = _isLockedOut.asStateFlow()
-
-    private val _isRunning = MutableStateFlow(false)
-    override val isRunning: Flow<Boolean>
-        get() = _isRunning
-
-    private var fpSensorType = MutableStateFlow<BiometricType?>(null)
-    override val availableFpSensorType: Flow<BiometricType?>
-        get() = fpSensorType
-
     fun setLockedOut(lockedOut: Boolean) {
         _isLockedOut.value = lockedOut
     }
 
+    private val _isRunning = MutableStateFlow(false)
+    override val isRunning: Flow<Boolean>
+        get() = _isRunning
     fun setIsRunning(value: Boolean) {
         _isRunning.value = value
     }
 
+    private var fpSensorType = MutableStateFlow<BiometricType?>(null)
+    override val availableFpSensorType: Flow<BiometricType?>
+        get() = fpSensorType
     fun setAvailableFpSensorType(value: BiometricType?) {
         fpSensorType.value = value
     }
+
+    private var _authenticationStatus = MutableStateFlow<FingerprintAuthenticationStatus?>(null)
+    override val authenticationStatus: Flow<FingerprintAuthenticationStatus>
+        get() = _authenticationStatus.filterNotNull()
+    fun setAuthenticationStatus(status: FingerprintAuthenticationStatus) {
+        _authenticationStatus.value = status
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index 6309740..8428566 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -22,11 +22,14 @@
 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
 import com.android.systemui.keyguard.shared.model.DozeTransitionModel
+import com.android.systemui.keyguard.shared.model.ScreenModel
+import com.android.systemui.keyguard.shared.model.ScreenState
 import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.keyguard.shared.model.WakeSleepReason
 import com.android.systemui.keyguard.shared.model.WakefulnessModel
 import com.android.systemui.keyguard.shared.model.WakefulnessState
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
@@ -56,6 +59,9 @@
     private val _isDozing = MutableStateFlow(false)
     override val isDozing: StateFlow<Boolean> = _isDozing
 
+    private val _dozeTimeTick = MutableSharedFlow<Unit>()
+    override val dozeTimeTick = _dozeTimeTick
+
     private val _lastDozeTapToWakePosition = MutableStateFlow<Point?>(null)
     override val lastDozeTapToWakePosition = _lastDozeTapToWakePosition.asStateFlow()
 
@@ -86,6 +92,9 @@
         )
     override val wakefulness = _wakefulnessModel
 
+    private val _screenModel = MutableStateFlow(ScreenModel(ScreenState.SCREEN_OFF))
+    override val screenModel = _screenModel
+
     private val _isUdfpsSupported = MutableStateFlow(false)
 
     private val _isKeyguardGoingAway = MutableStateFlow(false)
@@ -114,6 +123,11 @@
         return _isKeyguardShowing.value
     }
 
+    private var _isBypassEnabled = false
+    override fun isBypassEnabled(): Boolean {
+        return _isBypassEnabled
+    }
+
     override fun setAnimateDozingTransitions(animate: Boolean) {
         _animateBottomAreaDozingTransitions.tryEmit(animate)
     }
@@ -142,6 +156,10 @@
         _isDozing.value = isDozing
     }
 
+    override fun dozeTimeTick() {
+        _dozeTimeTick.tryEmit(Unit)
+    }
+
     override fun setLastDozeTapToWakePosition(position: Point) {
         _lastDozeTapToWakePosition.value = position
     }
@@ -198,6 +216,10 @@
         _isKeyguardUnlocked.value = isUnlocked
     }
 
+    fun setBypassEnabled(isEnabled: Boolean) {
+        _isBypassEnabled = isEnabled
+    }
+
     override fun isUdfpsSupported(): Boolean {
         return _isUdfpsSupported.value
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
index 47e1daf4..9317981 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
@@ -142,6 +142,7 @@
             repository = repository,
             backgroundDispatcher = testDispatcher,
             userRepository = userRepository,
+            keyguardRepository = keyguardRepository,
             clock = mock { whenever(elapsedRealtime()).thenAnswer { testScope.currentTime } }
         )
     }
diff --git a/services/Android.bp b/services/Android.bp
index 7b64b47..53dc068 100644
--- a/services/Android.bp
+++ b/services/Android.bp
@@ -164,6 +164,7 @@
         "services.coverage",
         "services.credentials",
         "services.devicepolicy",
+        "services.flags",
         "services.midi",
         "services.musicsearch",
         "services.net",
diff --git a/services/core/java/com/android/server/MasterClearReceiver.java b/services/core/java/com/android/server/MasterClearReceiver.java
index 7c3a75f..5a15f17 100644
--- a/services/core/java/com/android/server/MasterClearReceiver.java
+++ b/services/core/java/com/android/server/MasterClearReceiver.java
@@ -49,6 +49,7 @@
     private static final String TAG = "MasterClear";
     private boolean mWipeExternalStorage;
     private boolean mWipeEsims;
+    private boolean mShowWipeProgress = true;
 
     @Override
     public void onReceive(final Context context, final Intent intent) {
@@ -77,8 +78,12 @@
             return;
         }
 
-        final boolean shutdown = intent.getBooleanExtra("shutdown", false);
         final String reason = intent.getStringExtra(Intent.EXTRA_REASON);
+
+        // Factory reset dialog has its own UI for the reset process, so suppress ours if indicated.
+        mShowWipeProgress = intent.getBooleanExtra(Intent.EXTRA_SHOW_WIPE_PROGRESS, true);
+
+        final boolean shutdown = intent.getBooleanExtra("shutdown", false);
         mWipeExternalStorage = intent.getBooleanExtra(Intent.EXTRA_WIPE_EXTERNAL_STORAGE, false);
         mWipeEsims = intent.getBooleanExtra(Intent.EXTRA_WIPE_ESIMS, false);
         final boolean forceWipe = intent.getBooleanExtra(Intent.EXTRA_FORCE_MASTER_CLEAR, false)
@@ -190,15 +195,19 @@
         public WipeDataTask(Context context, Thread chainedTask) {
             mContext = context;
             mChainedTask = chainedTask;
-            mProgressDialog = new ProgressDialog(context, R.style.Theme_DeviceDefault_System);
+            mProgressDialog = mShowWipeProgress
+                    ? new ProgressDialog(context, R.style.Theme_DeviceDefault_System)
+                    : null;
         }
 
         @Override
         protected void onPreExecute() {
-            mProgressDialog.setIndeterminate(true);
-            mProgressDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
-            mProgressDialog.setMessage(mContext.getText(R.string.progress_erasing));
-            mProgressDialog.show();
+            if (mProgressDialog != null) {
+                mProgressDialog.setIndeterminate(true);
+                mProgressDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
+                mProgressDialog.setMessage(mContext.getText(R.string.progress_erasing));
+                mProgressDialog.show();
+            }
         }
 
         @Override
@@ -214,7 +223,9 @@
 
         @Override
         protected void onPostExecute(Void result) {
-            mProgressDialog.dismiss();
+            if (mProgressDialog != null && mProgressDialog.isShowing()) {
+                mProgressDialog.dismiss();
+            }
             mChainedTask.start();
         }
 
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index ecc0aa5..830d55a 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -1269,6 +1269,21 @@
         }
     }
 
+    /**
+     * This method checks if the volume is public and the volume is visible and the volume it is
+     * trying to mount doesn't have the same mount user id as the current user being maintained by
+     * StorageManagerService and change the mount Id. The checks are same as
+     * {@link StorageManagerService#maybeRemountVolumes(int)}
+     * @param VolumeInfo object to consider for changing the mountId
+     */
+    private void updateVolumeMountIdIfRequired(VolumeInfo vol) {
+        synchronized (mLock) {
+            if (!vol.isPrimary() && vol.isVisible() && vol.getMountUserId() != mCurrentUserId) {
+                vol.mountUserId = mCurrentUserId;
+            }
+        }
+    }
+
     private boolean supportsBlockCheckpoint() throws RemoteException {
         enforcePermission(android.Manifest.permission.MOUNT_FORMAT_FILESYSTEMS);
         return mVold.supportsBlockCheckpoint();
@@ -1382,13 +1397,14 @@
         }
 
         @Override
-        public void onVolumeStateChanged(String volId, final int newState) {
+        public void onVolumeStateChanged(String volId, final int newState, final int userId) {
             synchronized (mLock) {
                 final VolumeInfo vol = mVolumes.get(volId);
                 if (vol != null) {
                     final int oldState = vol.state;
                     vol.state = newState;
                     final VolumeInfo vInfo = new VolumeInfo(vol);
+                    vInfo.mountUserId = userId;
                     final SomeArgs args = SomeArgs.obtain();
                     args.arg1 = vInfo;
                     args.argi1 = oldState;
@@ -2232,7 +2248,7 @@
         if (isMountDisallowed(vol)) {
             throw new SecurityException("Mounting " + volId + " restricted by policy");
         }
-
+        updateVolumeMountIdIfRequired(vol);
         mount(vol);
     }
 
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 2f7c890..d199006 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -256,7 +256,7 @@
 public final class ActiveServices {
     private static final String TAG = TAG_WITH_CLASS_NAME ? "ActiveServices" : TAG_AM;
     private static final String TAG_MU = TAG + POSTFIX_MU;
-    private static final String TAG_SERVICE = TAG + POSTFIX_SERVICE;
+    static final String TAG_SERVICE = TAG + POSTFIX_SERVICE;
     private static final String TAG_SERVICE_EXECUTING = TAG + POSTFIX_SERVICE_EXECUTING;
 
     private static final boolean DEBUG_DELAYED_SERVICE = DEBUG_SERVICE;
@@ -850,8 +850,7 @@
         // Service.startForeground()), at that point we will consult the BFSL check and the timeout
         // and make the necessary decisions.
         setFgsRestrictionLocked(callingPackage, callingPid, callingUid, service, r, userId,
-                backgroundStartPrivileges, false /* isBindService */,
-                !fgRequired /* isStartService */);
+                backgroundStartPrivileges, false /* isBindService */);
 
         if (!mAm.mUserController.exists(r.userId)) {
             Slog.w(TAG, "Trying to start service with non-existent user! " + r.userId);
@@ -894,7 +893,7 @@
 
         if (fgRequired) {
             logFgsBackgroundStart(r);
-            if (r.mAllowStartForeground == REASON_DENIED && isBgFgsRestrictionEnabled(r)) {
+            if (!r.isFgsAllowedStart() && isBgFgsRestrictionEnabled(r)) {
                 String msg = "startForegroundService() not allowed due to "
                         + "mAllowStartForeground false: service "
                         + r.shortInstanceName;
@@ -1060,7 +1059,7 @@
             // Use that as a shortcut if possible to avoid having to recheck all the conditions.
             final boolean whileInUseAllowsUiJobScheduling =
                     ActivityManagerService.doesReasonCodeAllowSchedulingUserInitiatedJobs(
-                            r.mAllowWhileInUsePermissionInFgsReason);
+                            r.getFgsAllowWIU());
             r.updateAllowUiJobScheduling(whileInUseAllowsUiJobScheduling
                     || mAm.canScheduleUserInitiatedJobs(callingUid, callingPid, callingPackage));
         } else {
@@ -2178,12 +2177,12 @@
                         // on a SHORT_SERVICE FGS.
 
                         // See if the app could start an FGS or not.
-                        r.mAllowStartForeground = REASON_DENIED;
+                        r.clearFgsAllowStart();
                         setFgsRestrictionLocked(r.serviceInfo.packageName, r.app.getPid(),
                                 r.appInfo.uid, r.intent.getIntent(), r, r.userId,
                                 BackgroundStartPrivileges.NONE,
-                                false /* isBindService */, false /* isStartService */);
-                        if (r.mAllowStartForeground == REASON_DENIED) {
+                                false /* isBindService */);
+                        if (!r.isFgsAllowedStart()) {
                             Slog.w(TAG_SERVICE, "FGS type change to/from SHORT_SERVICE: "
                                     + " BFSL DENIED.");
                         } else {
@@ -2191,13 +2190,13 @@
                                 Slog.w(TAG_SERVICE, "FGS type change to/from SHORT_SERVICE: "
                                         + " BFSL Allowed: "
                                         + PowerExemptionManager.reasonCodeToString(
-                                                r.mAllowStartForeground));
+                                                r.getFgsAllowStart()));
                             }
                         }
 
                         final boolean fgsStartAllowed =
                                 !isBgFgsRestrictionEnabledForService
-                                        || (r.mAllowStartForeground != REASON_DENIED);
+                                        || r.isFgsAllowedStart();
 
                         if (fgsStartAllowed) {
                             if (isNewTypeShortFgs) {
@@ -2246,7 +2245,7 @@
                                 setFgsRestrictionLocked(r.serviceInfo.packageName, r.app.getPid(),
                                         r.appInfo.uid, r.intent.getIntent(), r, r.userId,
                                         BackgroundStartPrivileges.NONE,
-                                        false /* isBindService */, false /* isStartService */);
+                                        false /* isBindService */);
                                 final String temp = "startForegroundDelayMs:" + delayMs;
                                 if (r.mInfoAllowStartForeground != null) {
                                     r.mInfoAllowStartForeground += "; " + temp;
@@ -2266,20 +2265,21 @@
                         setFgsRestrictionLocked(r.serviceInfo.packageName, r.app.getPid(),
                                 r.appInfo.uid, r.intent.getIntent(), r, r.userId,
                                 BackgroundStartPrivileges.NONE,
-                                false /* isBindService */, false /* isStartService */);
+                                false /* isBindService */);
                     }
 
                     // If the foreground service is not started from TOP process, do not allow it to
                     // have while-in-use location/camera/microphone access.
-                    if (!r.mAllowWhileInUsePermissionInFgs) {
+                    if (!r.isFgsAllowedWIU()) {
                         Slog.w(TAG,
                                 "Foreground service started from background can not have "
                                         + "location/camera/microphone access: service "
                                         + r.shortInstanceName);
                     }
+                    r.maybeLogFgsLogicChange();
                     if (!bypassBfslCheck) {
                         logFgsBackgroundStart(r);
-                        if (r.mAllowStartForeground == REASON_DENIED
+                        if (!r.isFgsAllowedStart()
                                 && isBgFgsRestrictionEnabledForService) {
                             final String msg = "Service.startForeground() not allowed due to "
                                     + "mAllowStartForeground false: service "
@@ -2378,9 +2378,9 @@
                         // The logging of FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER event could
                         // be deferred, make a copy of mAllowStartForeground and
                         // mAllowWhileInUsePermissionInFgs.
-                        r.mAllowStartForegroundAtEntering = r.mAllowStartForeground;
+                        r.mAllowStartForegroundAtEntering = r.getFgsAllowStart();
                         r.mAllowWhileInUsePermissionInFgsAtEntering =
-                                r.mAllowWhileInUsePermissionInFgs;
+                                r.isFgsAllowedWIU();
                         r.mStartForegroundCount++;
                         r.mFgsEnterTime = SystemClock.uptimeMillis();
                         if (!stopProcStatsOp) {
@@ -2558,7 +2558,7 @@
                 policy.getForegroundServiceTypePolicyInfo(type, defaultToType);
         final @ForegroundServicePolicyCheckCode int code = policy.checkForegroundServiceTypePolicy(
                 mAm.mContext, r.packageName, r.app.uid, r.app.getPid(),
-                r.mAllowWhileInUsePermissionInFgs, policyInfo);
+                r.isFgsAllowedWIU(), policyInfo);
         RuntimeException exception = null;
         switch (code) {
             case FGS_TYPE_POLICY_CHECK_DEPRECATED: {
@@ -3744,8 +3744,7 @@
                 }
             }
             setFgsRestrictionLocked(callingPackage, callingPid, callingUid, service, s, userId,
-                    BackgroundStartPrivileges.NONE, true /* isBindService */,
-                    false /* isStartService */);
+                    BackgroundStartPrivileges.NONE, true /* isBindService */);
 
             if (s.app != null) {
                 ProcessServiceRecord servicePsr = s.app.mServices;
@@ -7444,54 +7443,80 @@
      * @param callingUid caller app's uid.
      * @param intent intent to start/bind service.
      * @param r the service to start.
-     * @param isStartService True if it's called from Context.startService().
-     *                       False if it's called from Context.startForegroundService() or
-     *                       Service.startForeground().
+     * @param isBindService True if it's called from bindService().
      * @return true if allow, false otherwise.
      */
     private void setFgsRestrictionLocked(String callingPackage,
             int callingPid, int callingUid, Intent intent, ServiceRecord r, int userId,
-            BackgroundStartPrivileges backgroundStartPrivileges, boolean isBindService,
-            boolean isStartService) {
-        // Check DeviceConfig flag.
-        if (!mAm.mConstants.mFlagBackgroundFgsStartRestrictionEnabled) {
-            if (!r.mAllowWhileInUsePermissionInFgs) {
-                // BGFGS start restrictions are disabled. We're allowing while-in-use permissions.
-                // Note REASON_OTHER since there's no other suitable reason.
-                r.mAllowWhileInUsePermissionInFgsReason = REASON_OTHER;
-            }
-            r.mAllowWhileInUsePermissionInFgs = true;
+            BackgroundStartPrivileges backgroundStartPrivileges, boolean isBindService) {
+
+        @ReasonCode int allowWIU;
+        @ReasonCode int allowStart;
+
+        // If called from bindService(), do not update the actual fields, but instead
+        // keep it in a separate set of fields.
+        if (isBindService) {
+            allowWIU = r.mAllowWIUInBindService;
+            allowStart = r.mAllowStartInBindService;
+        } else {
+            allowWIU = r.mAllowWhileInUsePermissionInFgsReasonNoBinding;
+            allowStart = r.mAllowStartForegroundNoBinding;
         }
 
-        if (!r.mAllowWhileInUsePermissionInFgs
-                || (r.mAllowStartForeground == REASON_DENIED)) {
+        // Check DeviceConfig flag.
+        if (!mAm.mConstants.mFlagBackgroundFgsStartRestrictionEnabled) {
+            if (allowWIU == REASON_DENIED) {
+                // BGFGS start restrictions are disabled. We're allowing while-in-use permissions.
+                // Note REASON_OTHER since there's no other suitable reason.
+                allowWIU = REASON_OTHER;
+            }
+        }
+
+        if ((allowWIU == REASON_DENIED)
+                || (allowStart == REASON_DENIED)) {
             @ReasonCode final int allowWhileInUse = shouldAllowFgsWhileInUsePermissionLocked(
                     callingPackage, callingPid, callingUid, r.app, backgroundStartPrivileges);
             // We store them to compare the old and new while-in-use logics to each other.
             // (They're not used for any other purposes.)
-            if (!r.mAllowWhileInUsePermissionInFgs) {
-                r.mAllowWhileInUsePermissionInFgs = (allowWhileInUse != REASON_DENIED);
-                r.mAllowWhileInUsePermissionInFgsReason = allowWhileInUse;
+            if (allowWIU == REASON_DENIED) {
+                allowWIU = allowWhileInUse;
             }
-            if (r.mAllowStartForeground == REASON_DENIED) {
-                r.mAllowStartForeground = shouldAllowFgsStartForegroundWithBindingCheckLocked(
+            if (allowStart == REASON_DENIED) {
+                allowStart = shouldAllowFgsStartForegroundWithBindingCheckLocked(
                         allowWhileInUse, callingPackage, callingPid, callingUid, intent, r,
                         backgroundStartPrivileges, isBindService);
             }
         }
+
+        if (isBindService) {
+            r.mAllowWIUInBindService = allowWIU;
+            r.mAllowStartInBindService = allowStart;
+        } else {
+            r.mAllowWhileInUsePermissionInFgsReasonNoBinding = allowWIU;
+            r.mAllowStartForegroundNoBinding = allowStart;
+
+            // Also do a binding client check, unless called from bindService().
+            if (r.mAllowWIUByBindings == REASON_DENIED) {
+                r.mAllowWIUByBindings =
+                        shouldAllowFgsWhileInUsePermissionByBindingsLocked(callingUid);
+            }
+            if (r.mAllowStartByBindings == REASON_DENIED) {
+                r.mAllowStartByBindings = r.mAllowWIUByBindings;
+            }
+        }
     }
 
     /**
      * Reset various while-in-use and BFSL related information.
      */
     void resetFgsRestrictionLocked(ServiceRecord r) {
-        r.mAllowWhileInUsePermissionInFgs = false;
-        r.mAllowWhileInUsePermissionInFgsReason = REASON_DENIED;
-        r.mAllowStartForeground = REASON_DENIED;
+        r.clearFgsAllowWIU();
+        r.clearFgsAllowStart();
+
         r.mInfoAllowStartForeground = null;
         r.mInfoTempFgsAllowListReason = null;
         r.mLoggedInfoAllowStartForeground = false;
-        r.updateAllowUiJobScheduling(r.mAllowWhileInUsePermissionInFgs);
+        r.updateAllowUiJobScheduling(r.isFgsAllowedWIU());
     }
 
     boolean canStartForegroundServiceLocked(int callingPid, int callingUid, String callingPackage) {
@@ -8063,10 +8088,10 @@
         */
         if (!r.mLoggedInfoAllowStartForeground) {
             final String msg = "Background started FGS: "
-                    + ((r.mAllowStartForeground != REASON_DENIED) ? "Allowed " : "Disallowed ")
+                    + (r.isFgsAllowedStart() ? "Allowed " : "Disallowed ")
                     + r.mInfoAllowStartForeground
                     + (r.isShortFgs() ? " (Called on SHORT_SERVICE)" : "");
-            if (r.mAllowStartForeground != REASON_DENIED) {
+            if (r.isFgsAllowedStart()) {
                 if (ActivityManagerUtils.shouldSamplePackageForAtom(r.packageName,
                         mAm.mConstants.mFgsStartAllowedLogSampleRate)) {
                     Slog.wtfQuiet(TAG, msg);
@@ -8106,8 +8131,8 @@
             allowWhileInUsePermissionInFgs = r.mAllowWhileInUsePermissionInFgsAtEntering;
             fgsStartReasonCode = r.mAllowStartForegroundAtEntering;
         } else {
-            allowWhileInUsePermissionInFgs = r.mAllowWhileInUsePermissionInFgs;
-            fgsStartReasonCode = r.mAllowStartForeground;
+            allowWhileInUsePermissionInFgs = r.isFgsAllowedWIU();
+            fgsStartReasonCode = r.getFgsAllowStart();
         }
         final int callerTargetSdkVersion = r.mRecentCallerApplicationInfo != null
                 ? r.mRecentCallerApplicationInfo.targetSdkVersion : 0;
@@ -8296,8 +8321,7 @@
         r.mFgsEnterTime = SystemClock.uptimeMillis();
         r.foregroundServiceType = options.mForegroundServiceTypes;
         setFgsRestrictionLocked(callingPackage, callingPid, callingUid, intent, r, userId,
-                BackgroundStartPrivileges.NONE,  false /* isBindService */,
-                false /* isStartService */);
+                BackgroundStartPrivileges.NONE,  false /* isBindService */);
         final ProcessServiceRecord psr = callerApp.mServices;
         final boolean newService = psr.startService(r);
         // updateOomAdj.
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index eacc7c2..c9769b3 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -559,6 +559,10 @@
     // How long we wait for a launched process to attach to the activity manager
     // before we decide it's never going to come up for real.
     static final int PROC_START_TIMEOUT = 10 * 1000 * Build.HW_TIMEOUT_MULTIPLIER;
+
+    // How long we wait for a launched process to complete its app startup before we ANR.
+    static final int BIND_APPLICATION_TIMEOUT = 10 * 1000 * Build.HW_TIMEOUT_MULTIPLIER;
+
     // How long we wait to kill an application zygote, after the last process using
     // it has gone away.
     static final int KILL_APP_ZYGOTE_DELAY_MS = 5 * 1000;
@@ -1625,6 +1629,7 @@
     static final int UPDATE_CACHED_APP_HIGH_WATERMARK = 79;
     static final int ADD_UID_TO_OBSERVER_MSG = 80;
     static final int REMOVE_UID_FROM_OBSERVER_MSG = 81;
+    static final int BIND_APPLICATION_TIMEOUT_MSG = 82;
 
     static final int FIRST_BROADCAST_QUEUE_MSG = 200;
 
@@ -1977,6 +1982,16 @@
                 case UPDATE_CACHED_APP_HIGH_WATERMARK: {
                     mAppProfiler.mCachedAppsWatermarkData.updateCachedAppsSnapshot((long) msg.obj);
                 } break;
+                case BIND_APPLICATION_TIMEOUT_MSG: {
+                    ProcessRecord app = (ProcessRecord) msg.obj;
+
+                    final String anrMessage;
+                    synchronized (app) {
+                        anrMessage = "Process " + app + " failed to complete startup";
+                    }
+
+                    mAnrHelper.appNotResponding(app, TimeoutRecord.forAppStart(anrMessage));
+                } break;
             }
         }
     }
@@ -4737,6 +4752,12 @@
                         app.getDisabledCompatChanges(), serializedSystemFontMap,
                         app.getStartElapsedTime(), app.getStartUptime());
             }
+
+            Message msg = mHandler.obtainMessage(BIND_APPLICATION_TIMEOUT_MSG);
+            msg.obj = app;
+            mHandler.sendMessageDelayed(msg, BIND_APPLICATION_TIMEOUT);
+            mHandler.removeMessages(PROC_START_TIMEOUT_MSG, app);
+
             if (profilerInfo != null) {
                 profilerInfo.closeFd();
                 profilerInfo = null;
@@ -4811,7 +4832,7 @@
         }
 
         if (app != null && app.getStartUid() == uid && app.getStartSeq() == startSeq) {
-            mHandler.removeMessages(PROC_START_TIMEOUT_MSG, app);
+            mHandler.removeMessages(BIND_APPLICATION_TIMEOUT_MSG, app);
         } else {
             Slog.wtf(TAG, "Mismatched or missing ProcessRecord: " + app + ". Pid: " + pid
                     + ". Uid: " + uid);
@@ -9721,6 +9742,7 @@
             ResultReceiver resultReceiver) {
         final int callingUid = Binder.getCallingUid();
         if (callingUid != ROOT_UID && callingUid != Process.SHELL_UID) {
+            resultReceiver.send(-1, null);
             throw new SecurityException("Shell commands are only callable by root or shell");
         }
         (new ActivityManagerShellCommand(this, false)).exec(
@@ -19893,7 +19915,8 @@
                     return superImpl.apply(code, new AttributionSource(shellUid,
                             Process.INVALID_PID, "com.android.shell",
                             attributionSource.getAttributionTag(), attributionSource.getToken(),
-                            /*renouncedPermissions*/ null, attributionSource.getNext()),
+                            /*renouncedPermissions*/ null, attributionSource.getDeviceId(),
+                            attributionSource.getNext()),
                             shouldCollectAsyncNotedOp, message, shouldCollectMessage,
                             skiProxyOperation);
                 } finally {
@@ -19946,7 +19969,8 @@
                     return superImpl.apply(clientId, code, new AttributionSource(shellUid,
                             Process.INVALID_PID, "com.android.shell",
                             attributionSource.getAttributionTag(), attributionSource.getToken(),
-                            /*renouncedPermissions*/ null, attributionSource.getNext()),
+                            /*renouncedPermissions*/ null, attributionSource.getDeviceId(),
+                            attributionSource.getNext()),
                             startIfModeDefault, shouldCollectAsyncNotedOp, message,
                             shouldCollectMessage, skipProxyOperation, proxyAttributionFlags,
                             proxiedAttributionFlags, attributionChainId);
@@ -19972,7 +19996,8 @@
                     superImpl.apply(clientId, code, new AttributionSource(shellUid,
                             Process.INVALID_PID, "com.android.shell",
                             attributionSource.getAttributionTag(), attributionSource.getToken(),
-                            /*renouncedPermissions*/ null, attributionSource.getNext()),
+                            /*renouncedPermissions*/ null, attributionSource.getDeviceId(),
+                            attributionSource.getNext()),
                             skipProxyOperation);
                 } finally {
                     Binder.restoreCallingIdentity(identity);
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index f5e2eb5..8d2edaa 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -249,6 +249,7 @@
     private static final int MSG_CHECK_HEALTH = 5;
     private static final int MSG_CHECK_PENDING_COLD_START_VALIDITY = 6;
     private static final int MSG_PROCESS_FREEZABLE_CHANGED = 7;
+    private static final int MSG_UID_STATE_CHANGED = 8;
 
     private void enqueueUpdateRunningList() {
         mLocalHandler.removeMessages(MSG_UPDATE_RUNNING_LIST);
@@ -295,6 +296,19 @@
                 }
                 return true;
             }
+            case MSG_UID_STATE_CHANGED: {
+                final int uid = (int) msg.obj;
+                final int procState = msg.arg1;
+                synchronized (mService) {
+                    if (procState == ActivityManager.PROCESS_STATE_TOP) {
+                        mUidForeground.put(uid, true);
+                    } else {
+                        mUidForeground.delete(uid);
+                    }
+                    refreshProcessQueuesLocked(uid);
+                }
+                return true;
+            }
         }
         return false;
     };
@@ -672,7 +686,7 @@
     @Override
     public void onProcessFreezableChangedLocked(@NonNull ProcessRecord app) {
         mLocalHandler.removeMessages(MSG_PROCESS_FREEZABLE_CHANGED, app);
-        mLocalHandler.sendMessage(mHandler.obtainMessage(MSG_PROCESS_FREEZABLE_CHANGED, app));
+        mLocalHandler.obtainMessage(MSG_PROCESS_FREEZABLE_CHANGED, app).sendToTarget();
     }
 
     @Override
@@ -1601,14 +1615,9 @@
             @Override
             public void onUidStateChanged(int uid, int procState, long procStateSeq,
                     int capability) {
-                synchronized (mService) {
-                    if (procState == ActivityManager.PROCESS_STATE_TOP) {
-                        mUidForeground.put(uid, true);
-                    } else {
-                        mUidForeground.delete(uid);
-                    }
-                    refreshProcessQueuesLocked(uid);
-                }
+                mLocalHandler.removeMessages(MSG_UID_STATE_CHANGED, uid);
+                mLocalHandler.obtainMessage(MSG_UID_STATE_CHANGED, procState, 0, uid)
+                        .sendToTarget();
             }
         }, ActivityManager.UID_OBSERVER_PROCSTATE,
                 ActivityManager.PROCESS_STATE_TOP, "android");
diff --git a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java
index 38e7371..4f5b5e1 100644
--- a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java
+++ b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java
@@ -479,8 +479,8 @@
                 r.appInfo.uid,
                 r.shortInstanceName,
                 fgsState, // FGS State
-                r.mAllowWhileInUsePermissionInFgs, // allowWhileInUsePermissionInFgs
-                r.mAllowStartForeground, // fgsStartReasonCode
+                r.isFgsAllowedWIU(), // allowWhileInUsePermissionInFgs
+                r.getFgsAllowStart(), // fgsStartReasonCode
                 r.appInfo.targetSdkVersion,
                 r.mRecentCallingUid,
                 0, // callerTargetSdkVersion
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index a682c85..459c6ff 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -2222,7 +2222,7 @@
 
             if (s.isForeground) {
                 final int fgsType = s.foregroundServiceType;
-                if (s.mAllowWhileInUsePermissionInFgs) {
+                if (s.isFgsAllowedWIU()) {
                     capabilityFromFGS |=
                             (fgsType & FOREGROUND_SERVICE_TYPE_LOCATION)
                                     != 0 ? PROCESS_CAPABILITY_FOREGROUND_LOCATION : 0;
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index 50fe6d7..aabab61 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -21,9 +21,11 @@
 import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
 import static android.app.ProcessMemoryState.HOSTING_COMPONENT_TYPE_BOUND_SERVICE;
 import static android.os.PowerExemptionManager.REASON_DENIED;
+import static android.os.PowerExemptionManager.reasonCodeToString;
 import static android.os.Process.INVALID_UID;
 
 import static com.android.internal.util.Preconditions.checkArgument;
+import static com.android.server.am.ActiveServices.TAG_SERVICE;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_FOREGROUND_SERVICE;
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
@@ -172,11 +174,11 @@
     private BackgroundStartPrivileges mBackgroundStartPrivilegesByStartMerged =
             BackgroundStartPrivileges.NONE;
 
-    // allow while-in-use permissions in foreground service or not.
+    // Reason code for allow while-in-use permissions in foreground service.
+    // If it's not DENIED, while-in-use permissions are allowed.
     // while-in-use permissions in FGS started from background might be restricted.
-    boolean mAllowWhileInUsePermissionInFgs;
     @PowerExemptionManager.ReasonCode
-    int mAllowWhileInUsePermissionInFgsReason = REASON_DENIED;
+    int mAllowWhileInUsePermissionInFgsReasonNoBinding = REASON_DENIED;
 
     // A copy of mAllowWhileInUsePermissionInFgs's value when the service is entering FGS state.
     boolean mAllowWhileInUsePermissionInFgsAtEntering;
@@ -205,15 +207,114 @@
 
     // allow the service becomes foreground service? Service started from background may not be
     // allowed to become a foreground service.
-    @PowerExemptionManager.ReasonCode int mAllowStartForeground = REASON_DENIED;
+    @PowerExemptionManager.ReasonCode
+    int mAllowStartForegroundNoBinding = REASON_DENIED;
     // A copy of mAllowStartForeground's value when the service is entering FGS state.
-    @PowerExemptionManager.ReasonCode int mAllowStartForegroundAtEntering = REASON_DENIED;
+    @PowerExemptionManager.ReasonCode
+    int mAllowStartForegroundAtEntering = REASON_DENIED;
     // Debug info why mAllowStartForeground is allowed or denied.
     String mInfoAllowStartForeground;
     // Debug info if mAllowStartForeground is allowed because of a temp-allowlist.
     ActivityManagerService.FgsTempAllowListItem mInfoTempFgsAllowListReason;
     // Is the same mInfoAllowStartForeground string has been logged before? Used for dedup.
     boolean mLoggedInfoAllowStartForeground;
+
+    @PowerExemptionManager.ReasonCode
+    int mAllowWIUInBindService = REASON_DENIED;
+
+    @PowerExemptionManager.ReasonCode
+    int mAllowWIUByBindings = REASON_DENIED;
+
+    @PowerExemptionManager.ReasonCode
+    int mAllowStartInBindService = REASON_DENIED;
+
+    @PowerExemptionManager.ReasonCode
+    int mAllowStartByBindings = REASON_DENIED;
+
+    @PowerExemptionManager.ReasonCode
+    int getFgsAllowWIU() {
+        return mAllowWhileInUsePermissionInFgsReasonNoBinding != REASON_DENIED
+                ? mAllowWhileInUsePermissionInFgsReasonNoBinding
+                : mAllowWIUInBindService;
+    }
+
+    boolean isFgsAllowedWIU() {
+        return getFgsAllowWIU() != REASON_DENIED;
+    }
+
+    @PowerExemptionManager.ReasonCode
+    int getFgsAllowStart() {
+        return mAllowStartForegroundNoBinding != REASON_DENIED
+                ? mAllowStartForegroundNoBinding
+                : mAllowStartInBindService;
+    }
+
+    boolean isFgsAllowedStart() {
+        return getFgsAllowStart() != REASON_DENIED;
+    }
+
+    void clearFgsAllowWIU() {
+        mAllowWhileInUsePermissionInFgsReasonNoBinding = REASON_DENIED;
+        mAllowWIUInBindService = REASON_DENIED;
+        mAllowWIUByBindings = REASON_DENIED;
+    }
+
+    void clearFgsAllowStart() {
+        mAllowStartForegroundNoBinding = REASON_DENIED;
+        mAllowStartInBindService = REASON_DENIED;
+        mAllowStartByBindings = REASON_DENIED;
+    }
+
+    @PowerExemptionManager.ReasonCode
+    int reasonOr(@PowerExemptionManager.ReasonCode int first,
+            @PowerExemptionManager.ReasonCode int second) {
+        return first != REASON_DENIED ? first : second;
+    }
+
+    boolean allowedChanged(@PowerExemptionManager.ReasonCode int first,
+            @PowerExemptionManager.ReasonCode int second) {
+        return (first == REASON_DENIED) != (second == REASON_DENIED);
+    }
+
+    String changeMessage(@PowerExemptionManager.ReasonCode int first,
+            @PowerExemptionManager.ReasonCode int second) {
+        return reasonOr(first, second) == REASON_DENIED ? "DENIED"
+                : ("ALLOWED ("
+                + reasonCodeToString(first)
+                + "+"
+                + reasonCodeToString(second)
+                + ")");
+    }
+
+    void maybeLogFgsLogicChange() {
+        final int origWiu = reasonOr(mAllowWhileInUsePermissionInFgsReasonNoBinding,
+                mAllowWIUInBindService);
+        final int newWiu = reasonOr(mAllowWhileInUsePermissionInFgsReasonNoBinding,
+                mAllowWIUByBindings);
+        final int origStart = reasonOr(mAllowStartForegroundNoBinding, mAllowStartInBindService);
+        final int newStart = reasonOr(mAllowStartForegroundNoBinding, mAllowStartByBindings);
+
+        final boolean wiuChanged = allowedChanged(origWiu, newWiu);
+        final boolean startChanged = allowedChanged(origStart, newStart);
+
+        if (!wiuChanged && !startChanged) {
+            return;
+        }
+        final String message = "FGS logic changed:"
+                + (wiuChanged ? " [WIU changed]" : "")
+                + (startChanged ? " [BFSL changed]" : "")
+                + " OW:" // Orig-WIU
+                + changeMessage(mAllowWhileInUsePermissionInFgsReasonNoBinding,
+                        mAllowWIUInBindService)
+                + " NW:" // New-WIU
+                + changeMessage(mAllowWhileInUsePermissionInFgsReasonNoBinding, mAllowWIUByBindings)
+                + " OS:" // Orig-start
+                + changeMessage(mAllowStartForegroundNoBinding, mAllowStartInBindService)
+                + " NS:" // New-start
+                + changeMessage(mAllowStartForegroundNoBinding, mAllowStartByBindings);
+        Slog.wtf(TAG_SERVICE, message);
+    }
+
     // The number of times Service.startForeground() is called, after this service record is
     // created. (i.e. due to "bound" or "start".) It never decreases, even when stopForeground()
     // is called.
@@ -502,7 +603,7 @@
         ProtoUtils.toDuration(proto, ServiceRecordProto.RESTART_TIME, restartTime, now);
         proto.write(ServiceRecordProto.CREATED_FROM_FG, createdFromFg);
         proto.write(ServiceRecordProto.ALLOW_WHILE_IN_USE_PERMISSION_IN_FGS,
-                mAllowWhileInUsePermissionInFgs);
+                isFgsAllowedWIU());
 
         if (startRequested || delayedStop || lastStartId != 0) {
             long startToken = proto.start(ServiceRecordProto.START);
@@ -618,7 +719,13 @@
             pw.println(mBackgroundStartPrivilegesByStartMerged);
         }
         pw.print(prefix); pw.print("mAllowWhileInUsePermissionInFgsReason=");
-        pw.println(PowerExemptionManager.reasonCodeToString(mAllowWhileInUsePermissionInFgsReason));
+        pw.println(PowerExemptionManager.reasonCodeToString(
+                mAllowWhileInUsePermissionInFgsReasonNoBinding));
+
+        pw.print(prefix); pw.print("mAllowWIUInBindService=");
+        pw.println(PowerExemptionManager.reasonCodeToString(mAllowWIUInBindService));
+        pw.print(prefix); pw.print("mAllowWIUByBindings=");
+        pw.println(PowerExemptionManager.reasonCodeToString(mAllowWIUByBindings));
 
         pw.print(prefix); pw.print("allowUiJobScheduling="); pw.println(mAllowUiJobScheduling);
         pw.print(prefix); pw.print("recentCallingPackage=");
@@ -626,7 +733,12 @@
         pw.print(prefix); pw.print("recentCallingUid=");
         pw.println(mRecentCallingUid);
         pw.print(prefix); pw.print("allowStartForeground=");
-        pw.println(PowerExemptionManager.reasonCodeToString(mAllowStartForeground));
+        pw.println(PowerExemptionManager.reasonCodeToString(mAllowStartForegroundNoBinding));
+        pw.print(prefix); pw.print("mAllowStartInBindService=");
+        pw.println(PowerExemptionManager.reasonCodeToString(mAllowStartInBindService));
+        pw.print(prefix); pw.print("mAllowStartByBindings=");
+        pw.println(PowerExemptionManager.reasonCodeToString(mAllowStartByBindings));
+
         pw.print(prefix); pw.print("startForegroundCount=");
         pw.println(mStartForegroundCount);
         pw.print(prefix); pw.print("infoAllowStartForeground=");
diff --git a/services/core/java/com/android/server/appop/OWNERS b/services/core/java/com/android/server/appop/OWNERS
index 999ea0e..2fe78c5 100644
--- a/services/core/java/com/android/server/appop/OWNERS
+++ b/services/core/java/com/android/server/appop/OWNERS
@@ -1 +1,2 @@
+#Bug component: 137825
 include /core/java/android/permission/OWNERS
diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java
index e8ffe4f..279aaf9 100644
--- a/services/core/java/com/android/server/biometrics/BiometricService.java
+++ b/services/core/java/com/android/server/biometrics/BiometricService.java
@@ -17,6 +17,8 @@
 package com.android.server.biometrics;
 
 import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL;
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
 import static android.hardware.biometrics.BiometricManager.Authenticators;
 
 import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_IDLE;
@@ -369,7 +371,7 @@
         public boolean getConfirmationAlwaysRequired(@BiometricAuthenticator.Modality int modality,
                 int userId) {
             switch (modality) {
-                case BiometricAuthenticator.TYPE_FACE:
+                case TYPE_FACE:
                     if (!mFaceAlwaysRequireConfirmation.containsKey(userId)) {
                         onChange(true /* selfChange */,
                                 FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION,
@@ -567,22 +569,6 @@
 
             Utils.combineAuthenticatorBundles(promptInfo);
 
-            // Set the default title if necessary.
-            if (promptInfo.isUseDefaultTitle()) {
-                if (TextUtils.isEmpty(promptInfo.getTitle())) {
-                    promptInfo.setTitle(getContext()
-                            .getString(R.string.biometric_dialog_default_title));
-                }
-            }
-
-            // Set the default subtitle if necessary.
-            if (promptInfo.isUseDefaultSubtitle()) {
-                if (TextUtils.isEmpty(promptInfo.getSubtitle())) {
-                    promptInfo.setSubtitle(getContext()
-                            .getString(R.string.biometric_dialog_default_subtitle));
-                }
-            }
-
             final long requestId = mRequestCounter.get();
             mHandler.post(() -> handleAuthenticate(
                     token, requestId, operationId, userId, receiver, opPackageName, promptInfo));
@@ -1302,6 +1288,33 @@
                         opPackageName, promptInfo.isDisallowBiometricsIfPolicyExists(),
                         getContext(), mBiometricCameraManager);
 
+                // Set the default title if necessary.
+                if (promptInfo.isUseDefaultTitle()) {
+                    if (TextUtils.isEmpty(promptInfo.getTitle())) {
+                        promptInfo.setTitle(getContext()
+                                .getString(R.string.biometric_dialog_default_title));
+                    }
+                }
+
+                final int eligible = preAuthInfo.getEligibleModalities();
+                final boolean hasEligibleFingerprintSensor =
+                        (eligible & TYPE_FINGERPRINT) == TYPE_FINGERPRINT;
+                final boolean hasEligibleFaceSensor = (eligible & TYPE_FACE) == TYPE_FACE;
+
+                // Set the subtitle according to the modality.
+                if (promptInfo.isUseDefaultSubtitle()) {
+                    if (hasEligibleFingerprintSensor && hasEligibleFaceSensor) {
+                        promptInfo.setSubtitle(getContext()
+                                .getString(R.string.biometric_dialog_default_subtitle));
+                    } else if (hasEligibleFingerprintSensor) {
+                        promptInfo.setSubtitle(getContext()
+                                .getString(R.string.biometric_dialog_fingerprint_subtitle));
+                    } else if (hasEligibleFaceSensor) {
+                        promptInfo.setSubtitle(getContext()
+                                .getString(R.string.biometric_dialog_face_subtitle));
+                    }
+                }
+
                 final Pair<Integer, Integer> preAuthStatus = preAuthInfo.getPreAuthenticateStatus();
 
                 Slog.d(TAG, "handleAuthenticate: modality(" + preAuthStatus.first
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index 63bb026..cba5039 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -3346,7 +3346,7 @@
                 // Transforms do not need to be persisted; the IkeSession will keep
                 // them alive for us
                 mIpSecManager.applyTunnelModeTransform(mTunnelIface, direction, transform);
-            } catch (IOException e) {
+            } catch (IOException | IllegalArgumentException e) {
                 Log.d(TAG, "Transform application failed for token " + token, e);
                 onSessionLost(token, e);
             }
@@ -3440,7 +3440,7 @@
                         mTunnelIface, IpSecManager.DIRECTION_IN, inTransform);
                 mIpSecManager.applyTunnelModeTransform(
                         mTunnelIface, IpSecManager.DIRECTION_OUT, outTransform);
-            } catch (IOException e) {
+            } catch (IOException | IllegalArgumentException e) {
                 Log.d(TAG, "Transform application failed for token " + token, e);
                 onSessionLost(token, e);
             }
diff --git a/services/core/java/com/android/server/graphics/fonts/FontManagerService.java b/services/core/java/com/android/server/graphics/fonts/FontManagerService.java
index 080b947..c6a50ed 100644
--- a/services/core/java/com/android/server/graphics/fonts/FontManagerService.java
+++ b/services/core/java/com/android/server/graphics/fonts/FontManagerService.java
@@ -46,6 +46,7 @@
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.Preconditions;
 import com.android.server.LocalServices;
+import com.android.server.SystemServerInitThreadPool;
 import com.android.server.SystemService;
 
 import java.io.File;
@@ -61,6 +62,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
 
 /** A service for managing system fonts. */
 public final class FontManagerService extends IFontManager.Stub {
@@ -136,10 +138,11 @@
     /** Class to manage FontManagerService's lifecycle. */
     public static final class Lifecycle extends SystemService {
         private final FontManagerService mService;
+        private final CompletableFuture<Void> mServiceStarted = new CompletableFuture<>();
 
         public Lifecycle(@NonNull Context context, boolean safeMode) {
             super(context);
-            mService = new FontManagerService(context, safeMode);
+            mService = new FontManagerService(context, safeMode, mServiceStarted);
         }
 
         @Override
@@ -152,11 +155,20 @@
                             if (!Typeface.ENABLE_LAZY_TYPEFACE_INITIALIZATION) {
                                 return null;
                             }
+                            mServiceStarted.join();
                             return mService.getCurrentFontMap();
                         }
                     });
             publishBinderService(Context.FONT_SERVICE, mService);
         }
+
+        @Override
+        public void onBootPhase(int phase) {
+            if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) {
+                // Wait for FontManagerService to start since it will be needed after this point.
+                mServiceStarted.join();
+            }
+        }
     }
 
     private static class FsverityUtilImpl implements UpdatableFontDir.FsverityUtil {
@@ -219,25 +231,30 @@
     @Nullable
     private SharedMemory mSerializedFontMap = null;
 
-    private FontManagerService(Context context, boolean safeMode) {
+    private FontManagerService(
+            Context context, boolean safeMode, CompletableFuture<Void> serviceStarted) {
         if (safeMode) {
             Slog.i(TAG, "Entering safe mode. Deleting all font updates.");
             UpdatableFontDir.deleteAllFiles(new File(FONT_FILES_DIR), new File(CONFIG_XML_FILE));
         }
         mContext = context;
         mIsSafeMode = safeMode;
-        initialize();
 
-        // Set system font map only if there is updatable font directory.
-        // If there is no updatable font directory, `initialize` will have already loaded the
-        // system font map, so there's no need to set the system font map again here.
-        if  (mUpdatableFontDir != null) {
-            try {
-                Typeface.setSystemFontMap(getCurrentFontMap());
-            } catch (IOException | ErrnoException e) {
-                Slog.w(TAG, "Failed to set system font map of system_server");
+        SystemServerInitThreadPool.submit(() -> {
+            initialize();
+
+            // Set system font map only if there is updatable font directory.
+            // If there is no updatable font directory, `initialize` will have already loaded the
+            // system font map, so there's no need to set the system font map again here.
+            if  (mUpdatableFontDir != null) {
+                try {
+                    Typeface.setSystemFontMap(getCurrentFontMap());
+                } catch (IOException | ErrnoException e) {
+                    Slog.w(TAG, "Failed to set system font map of system_server");
+                }
             }
-        }
+            serviceStarted.complete(null);
+        }, "FontManagerService_create");
     }
 
     @Nullable
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index ecb21d0..b3ef869 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -118,7 +118,6 @@
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
-import android.util.EventLog;
 import android.util.Log;
 import android.util.LongSparseArray;
 import android.util.Slog;
@@ -836,9 +835,6 @@
 
     @Override // binder interface
     public void systemReady() {
-        if (mContext.checkCallingOrSelfPermission(PERMISSION) != PERMISSION_GRANTED) {
-            EventLog.writeEvent(0x534e4554, "28251513", getCallingUid(), "");  // SafetyNet
-        }
         checkWritePermission();
 
         mHasSecureLockScreen = mContext.getPackageManager()
@@ -1093,9 +1089,6 @@
     }
 
     private final void checkPasswordHavePermission() {
-        if (mContext.checkCallingOrSelfPermission(PERMISSION) != PERMISSION_GRANTED) {
-            EventLog.writeEvent(0x534e4554, "28251513", getCallingUid(), "");  // SafetyNet
-        }
         mContext.enforceCallingOrSelfPermission(PERMISSION, "LockSettingsHave");
     }
 
diff --git a/services/core/java/com/android/server/os/NativeTombstoneManager.java b/services/core/java/com/android/server/os/NativeTombstoneManager.java
index f8ee6b0..c7c8136 100644
--- a/services/core/java/com/android/server/os/NativeTombstoneManager.java
+++ b/services/core/java/com/android/server/os/NativeTombstoneManager.java
@@ -372,7 +372,7 @@
                 return false;
             }
 
-            if (Math.abs(exitInfo.getTimestamp() - mTimestampMs) > 5000) {
+            if (Math.abs(exitInfo.getTimestamp() - mTimestampMs) > 10000) {
                 return false;
             }
 
diff --git a/services/core/java/com/android/server/pm/AppsFilterImpl.java b/services/core/java/com/android/server/pm/AppsFilterImpl.java
index 6505e8b..21610c9 100644
--- a/services/core/java/com/android/server/pm/AppsFilterImpl.java
+++ b/services/core/java/com/android/server/pm/AppsFilterImpl.java
@@ -1225,6 +1225,7 @@
                                 setting.getPackageName(), siblingSetting, settings,
                                 users, USER_ALL, settings.size());
                     }
+                    break;
                 }
             }
 
diff --git a/services/core/java/com/android/server/pm/ArchiveManager.java b/services/core/java/com/android/server/pm/ArchiveManager.java
index 99479f0..5435206 100644
--- a/services/core/java/com/android/server/pm/ArchiveManager.java
+++ b/services/core/java/com/android/server/pm/ArchiveManager.java
@@ -77,9 +77,8 @@
         snapshot.enforceCrossUserPermission(callingUid, userId, true, true,
                 "archiveApp");
         verifyCaller(callerPackageName, callingPackageName);
-
         PackageStateInternal ps = getPackageState(packageName, snapshot, callingUid, user);
-        verifyInstallOwnership(packageName, callingPackageName, ps.getInstallSource());
+        verifyInstaller(packageName, ps.getInstallSource());
 
         List<LauncherActivityInfo> mainActivities = getLauncherApps().getActivityList(
                 ps.getPackageName(),
@@ -125,7 +124,7 @@
                     Path.of("/TODO"), null);
             activityInfos.add(activityInfo);
         }
-        // TODO(b/278553670) Adapt installer check verifyInstallOwnership and check for null there
+
         InstallSource installSource = ps.getInstallSource();
         String installerPackageName = installSource.mUpdateOwnerPackageName != null
                 ? installSource.mUpdateOwnerPackageName : installSource.mInstallerPackageName;
@@ -159,19 +158,13 @@
         }
     }
 
-    private static void verifyInstallOwnership(String packageName, String callingPackageName,
-            InstallSource installSource) {
-        if (!TextUtils.equals(installSource.mInstallerPackageName,
-                callingPackageName)) {
+    private static void verifyInstaller(String packageName, InstallSource installSource) {
+        // TODO(b/291060290) Verify installer supports unarchiving
+        if (installSource.mUpdateOwnerPackageName == null
+                && installSource.mInstallerPackageName == null) {
             throw new SecurityException(
-                    TextUtils.formatSimple("Caller is not the installer of record for %s.",
+                    TextUtils.formatSimple("No installer found to archive app %s.",
                             packageName));
         }
-        String updateOwnerPackageName = installSource.mUpdateOwnerPackageName;
-        if (updateOwnerPackageName != null
-                && !TextUtils.equals(updateOwnerPackageName, callingPackageName)) {
-            throw new SecurityException(
-                    TextUtils.formatSimple("Caller is not the update owner for %s.", packageName));
-        }
     }
 }
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 4f919de..134b041 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -1080,8 +1080,6 @@
         final boolean isApex = ((installFlags & PackageManager.INSTALL_APEX) != 0);
         final boolean isRollback =
                 request.getInstallReason() == PackageManager.INSTALL_REASON_ROLLBACK;
-        final boolean extractProfile =
-                ((installFlags & PackageManager.INSTALL_DONT_EXTRACT_BASELINE_PROFILES) == 0);
         @PackageManagerService.ScanFlags int scanFlags = SCAN_NEW_INSTALL | SCAN_UPDATE_SIGNATURE;
         if (request.isInstallMove()) {
             // moving a complete application; perform an initial scan on the new install location
@@ -1117,9 +1115,7 @@
         @ParsingPackageUtils.ParseFlags final int parseFlags =
                 mPm.getDefParseFlags() | ParsingPackageUtils.PARSE_CHATTY
                         | ParsingPackageUtils.PARSE_ENFORCE_CODE
-                        | (onExternal ? ParsingPackageUtils.PARSE_EXTERNAL_STORAGE : 0)
-                        | (extractProfile
-                        ? ParsingPackageUtils.PARSE_EXTRACT_BASELINE_PROFILES_FROM_APK : 0);
+                        | (onExternal ? ParsingPackageUtils.PARSE_EXTERNAL_STORAGE : 0);
 
         Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "parsePackage");
         final ParsedPackage parsedPackage;
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index 2fb1ed1..8d64bd9 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -3456,10 +3456,6 @@
                     sessionParams.installFlags |=
                             PackageManager.INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK;
                     break;
-                case "--no-profile":
-                    sessionParams.installFlags |=
-                            PackageManager.INSTALL_DONT_EXTRACT_BASELINE_PROFILES;
-                    break;
                 default:
                     throw new IllegalArgumentException("Unknown option " + opt);
             }
@@ -4348,7 +4344,7 @@
         pw.println("       [--install-reason 0/1/2/3/4] [--originating-uri URI]");
         pw.println("       [--referrer URI] [--abi ABI_NAME] [--force-sdk]");
         pw.println("       [--preload] [--instant] [--full] [--dont-kill]");
-        pw.println("       [--enable-rollback] [--no-profile]");
+        pw.println("       [--enable-rollback]");
         pw.println("       [--force-uuid internal|UUID] [--pkg PACKAGE] [-S BYTES]");
         pw.println("       [--apex] [--force-non-staged] [--staged-ready-timeout TIMEOUT]");
         pw.println("       [PATH [SPLIT...]|-]");
@@ -4381,7 +4377,6 @@
         pw.println("      --apex: install an .apex file, not an .apk");
         pw.println("      --force-non-staged: force the installation to run under a non-staged");
         pw.println("          session, which may complete without requiring a reboot");
-        pw.println("      --no-profile: don't extract the profiles from the apk");
         pw.println("      --staged-ready-timeout: By default, staged sessions wait "
                 + DEFAULT_STAGED_READY_TIMEOUT_MS);
         pw.println("          milliseconds for pre-reboot verification to complete when");
@@ -4403,7 +4398,7 @@
         pw.println("       [--referrer URI] [--abi ABI_NAME] [--force-sdk]");
         pw.println("       [--preload] [--instant] [--full] [--dont-kill]");
         pw.println("       [--force-uuid internal|UUID] [--pkg PACKAGE] [--apex] [-S BYTES]");
-        pw.println("       [--multi-package] [--staged] [--no-profile] [--update-ownership]");
+        pw.println("       [--multi-package] [--staged] [--update-ownership]");
         pw.println("    Like \"install\", but starts an install session.  Use \"install-write\"");
         pw.println("    to push data into the session, and \"install-commit\" to finish.");
         pw.println("");
diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java
index 9b92697..494709c 100644
--- a/services/core/java/com/android/server/pm/PackageSetting.java
+++ b/services/core/java/com/android/server/pm/PackageSetting.java
@@ -1195,8 +1195,11 @@
     }
 
     public PackageSetting setLoadingProgress(float progress) {
-        mLoadingProgress = progress;
-        onChanged();
+        // To prevent race conditions, we don't allow progress to ever go down
+        if (mLoadingProgress < progress) {
+            mLoadingProgress = progress;
+            onChanged();
+        }
         return this;
     }
 
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
index 7609073..b01a89e 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
@@ -1230,8 +1230,7 @@
             }
 
             if (!fromDatasource && !checkPermission(context, permissionManagerServiceInt,
-                    permission, attributionSource.getUid(),
-                    attributionSource.getRenouncedPermissions())) {
+                    permission, attributionSource)) {
                 return PermissionChecker.PERMISSION_HARD_DENIED;
             }
 
@@ -1292,12 +1291,11 @@
                     }
                     case AppOpsManager.MODE_DEFAULT: {
                         if (!skipCurrentChecks && !checkPermission(context,
-                                permissionManagerServiceInt, permission, attributionSource.getUid(),
-                                attributionSource.getRenouncedPermissions())) {
+                                permissionManagerServiceInt, permission, attributionSource)) {
                             return PermissionChecker.PERMISSION_HARD_DENIED;
                         }
                         if (next != null && !checkPermission(context, permissionManagerServiceInt,
-                                permission, next.getUid(), next.getRenouncedPermissions())) {
+                                permission, next)) {
                             return PermissionChecker.PERMISSION_HARD_DENIED;
                         }
                     }
@@ -1326,8 +1324,7 @@
             // We consider the chain trusted if the start node has UPDATE_APP_OPS_STATS, and
             // every attributionSource in the chain is registered with the system.
             final boolean isChainStartTrusted = !hasChain || checkPermission(context,
-                    permissionManagerServiceInt, UPDATE_APP_OPS_STATS, current.getUid(),
-                    current.getRenouncedPermissions());
+                    permissionManagerServiceInt, UPDATE_APP_OPS_STATS, current);
 
             while (true) {
                 final boolean skipCurrentChecks = (fromDatasource || next != null);
@@ -1342,12 +1339,12 @@
 
                 // If we already checked the permission for this one, skip the work
                 if (!skipCurrentChecks && !checkPermission(context, permissionManagerServiceInt,
-                        permission, current.getUid(), current.getRenouncedPermissions())) {
+                        permission, current)) {
                     return PermissionChecker.PERMISSION_HARD_DENIED;
                 }
 
                 if (next != null && !checkPermission(context, permissionManagerServiceInt,
-                        permission, next.getUid(), next.getRenouncedPermissions())) {
+                        permission, next)) {
                     return PermissionChecker.PERMISSION_HARD_DENIED;
                 }
 
@@ -1415,9 +1412,13 @@
 
         private static boolean checkPermission(@NonNull Context context,
                 @NonNull PermissionManagerServiceInternal permissionManagerServiceInt,
-                @NonNull String permission, int uid, @NonNull Set<String> renouncedPermissions) {
-            boolean permissionGranted = context.checkPermission(permission, /*pid*/ -1,
-                    uid) == PackageManager.PERMISSION_GRANTED;
+                @NonNull String permission, AttributionSource attributionSource) {
+            int uid = attributionSource.getUid();
+            int deviceId = attributionSource.getDeviceId();
+            final Context deviceContext = context.getDeviceId() == deviceId ? context
+                    : context.createDeviceContext(deviceId);
+            boolean permissionGranted = deviceContext.checkPermission(permission,
+                    Process.INVALID_PID, uid) == PackageManager.PERMISSION_GRANTED;
 
             // Override certain permissions checks for the shared isolated process for both
             // HotwordDetectionService and VisualQueryDetectionService, which ordinarily cannot hold
@@ -1433,10 +1434,10 @@
                 permissionGranted = hotwordServiceProvider != null
                         && uid == hotwordServiceProvider.getUid();
             }
-
+            Set<String> renouncedPermissions = attributionSource.getRenouncedPermissions();
             if (permissionGranted && renouncedPermissions.contains(permission)
-                    && context.checkPermission(Manifest.permission.RENOUNCE_PERMISSIONS,
-                    /*pid*/ -1, uid) == PackageManager.PERMISSION_GRANTED) {
+                    && deviceContext.checkPermission(Manifest.permission.RENOUNCE_PERMISSIONS,
+                        Process.INVALID_PID, uid) == PackageManager.PERMISSION_GRANTED) {
                 return false;
             }
             return permissionGranted;
@@ -1507,8 +1508,7 @@
             // We consider the chain trusted if the start node has UPDATE_APP_OPS_STATS, and
             // every attributionSource in the chain is registered with the system.
             final boolean isChainStartTrusted = !hasChain || checkPermission(context,
-                    permissionManagerServiceInt, UPDATE_APP_OPS_STATS, current.getUid(),
-                    current.getRenouncedPermissions());
+                    permissionManagerServiceInt, UPDATE_APP_OPS_STATS, current);
 
             while (true) {
                 final boolean skipCurrentChecks = (next != null);
diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
index d737b1c..e2cb87e 100644
--- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
+++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
@@ -50,7 +50,6 @@
 import android.content.pm.PackageManager.Property;
 import android.content.pm.Signature;
 import android.content.pm.SigningDetails;
-import android.content.pm.dex.DexMetadataHelper;
 import android.content.pm.parsing.ApkLiteParseUtils;
 import android.content.pm.parsing.FrameworkParsingPackageUtils;
 import android.content.pm.parsing.PackageLite;
@@ -74,7 +73,6 @@
 import android.os.Trace;
 import android.os.UserHandle;
 import android.os.ext.SdkExtensions;
-import android.os.incremental.IncrementalManager;
 import android.permission.PermissionManager;
 import android.text.TextUtils;
 import android.util.ArrayMap;
@@ -242,11 +240,6 @@
     public static final int PARSE_IGNORE_OVERLAY_REQUIRED_SYSTEM_PROPERTY = 1 << 7;
     public static final int PARSE_APK_IN_APEX = 1 << 9;
 
-    /**
-     * This flag is to determine whether to extract the baseline profiles from the apk or not.
-     */
-    public static final int PARSE_EXTRACT_BASELINE_PROFILES_FROM_APK = 1 << 10;
-
     public static final int PARSE_CHATTY = 1 << 31;
 
     /** The total maximum number of activities, services, providers and activity-aliases */
@@ -258,16 +251,14 @@
     private static final int MAX_PERMISSION_NAME_LENGTH = 512;
 
     @IntDef(flag = true, prefix = { "PARSE_" }, value = {
-            PARSE_APK_IN_APEX,
             PARSE_CHATTY,
             PARSE_COLLECT_CERTIFICATES,
             PARSE_ENFORCE_CODE,
             PARSE_EXTERNAL_STORAGE,
-            PARSE_EXTRACT_BASELINE_PROFILES_FROM_APK,
-            PARSE_IGNORE_OVERLAY_REQUIRED_SYSTEM_PROPERTY,
             PARSE_IGNORE_PROCESSES,
             PARSE_IS_SYSTEM_DIR,
             PARSE_MUST_BE_APK,
+            PARSE_IGNORE_OVERLAY_REQUIRED_SYSTEM_PROPERTY,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ParseFlags {}
@@ -568,26 +559,6 @@
                 pkg.setSigningDetails(SigningDetails.UNKNOWN);
             }
 
-            // 1. The apkFile is an apk file
-            // 2. The flags include PARSE_EXTRACT_PROFILE_FROM_APK
-            // 3. The apk patch is NOT an incremental path
-            // 4. If the .dm file exists in the current apk directory, it means the caller
-            // prepares the .dm file. Don't extract the profiles from the apk again.
-            if (ApkLiteParseUtils.isApkFile(apkFile)
-                    && (flags & PARSE_EXTRACT_BASELINE_PROFILES_FROM_APK) != 0
-                    && !IncrementalManager.isIncrementalPath(apkPath)
-                    && DexMetadataHelper.findDexMetadataForFile(apkFile) == null) {
-                // Extract the baseline profiles from the apk if the profiles exist in the assets
-                // directory in the apk.
-                boolean extractedResult =
-                        DexMetadataHelper.extractBaselineProfilesToDexMetadataFileFromApk(assets,
-                                apkPath);
-
-                if (DEBUG_JAR) {
-                    Slog.d(TAG, "Extract profiles " + (extractedResult ? "success" : "fail"));
-                }
-            }
-
             return input.success(pkg);
         } catch (Exception e) {
             return input.error(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 402bb59..7bbe878 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -6271,10 +6271,10 @@
                 pw.println(endcallBehaviorToString(mEndcallBehavior));
         pw.print(prefix);
         // TODO(b/117479243): handle it in InputPolicy
-        pw.print("mDisplayHomeButtonHandlers=");
+        pw.println("mDisplayHomeButtonHandlers=");
         for (int i = 0; i < mDisplayHomeButtonHandlers.size(); i++) {
             final int key = mDisplayHomeButtonHandlers.keyAt(i);
-            pw.println(mDisplayHomeButtonHandlers.get(key));
+            pw.print(prefix); pw.print("  "); pw.println(mDisplayHomeButtonHandlers.get(key));
         }
         pw.print(prefix); pw.print("mKeyguardOccluded="); pw.print(isKeyguardOccluded());
                 pw.print(" mKeyguardOccludedChanged="); pw.print(mKeyguardOccludedChanged);
@@ -6545,6 +6545,7 @@
 
     private class HdmiVideoExtconUEventObserver extends ExtconStateObserver<Boolean> {
         private static final String HDMI_EXIST = "HDMI=1";
+        private static final String DP_EXIST = "DP=1";
         private static final String NAME = "hdmi";
 
         private boolean init(ExtconInfo hdmi) {
@@ -6575,7 +6576,8 @@
         public Boolean parseState(ExtconInfo extconIfno, String state) {
             // extcon event state changes from kernel4.9
             // new state will be like STATE=HDMI=1
-            return state.contains(HDMI_EXIST);
+            // or like STATE=DP=1 for newer kernel
+            return state.contains(HDMI_EXIST) || state.contains(DP_EXIST);
         }
     }
 
diff --git a/services/core/java/com/android/server/security/FileIntegrityService.java b/services/core/java/com/android/server/security/FileIntegrityService.java
index 5ae6973..9529621 100644
--- a/services/core/java/com/android/server/security/FileIntegrityService.java
+++ b/services/core/java/com/android/server/security/FileIntegrityService.java
@@ -184,13 +184,7 @@
     }
 
     private void loadAllCertificates() {
-        // A better alternative to load certificates would be to read from .fs-verity kernel
-        // keyring, which fsverity_init loads to during earlier boot time from the same sources
-        // below. But since the read operation from keyring is not provided in kernel, we need to
-        // duplicate the same loading logic here.
-
         // Load certificates trusted by the device manufacturer.
-        // NB: Directories need to be synced with system/security/fsverity_init/fsverity_init.cpp.
         final String relativeDir = "etc/security/fsverity";
         loadCertificatesFromDirectory(Environment.getRootDirectory().toPath()
                 .resolve(relativeDir));
diff --git a/services/core/java/com/android/server/vibrator/TEST_MAPPING b/services/core/java/com/android/server/vibrator/TEST_MAPPING
new file mode 100644
index 0000000..f0a7e47
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/TEST_MAPPING
@@ -0,0 +1,21 @@
+{
+  "presubmit": [
+    {
+      "name": "FrameworksVibratorServicesTests",
+      "options": [
+        {"exclude-annotation": "android.platform.test.annotations.LargeTest"},
+        {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
+        {"exclude-annotation": "androidx.test.filters.FlakyTest"},
+        {"exclude-annotation": "org.junit.Ignore"}
+      ]
+    }
+  ],
+  "postsubmit": [
+    {
+      "name": "FrameworksVibratorServicesTests",
+      "options": [
+        {"exclude-annotation": "org.junit.Ignore"}
+      ]
+    }
+  ]
+}
diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
index c5f63ce..a6d5c19 100644
--- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
+++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
@@ -285,9 +285,9 @@
         final LaunchingState mLaunchingState;
 
         /** The type can be cold (new process), warm (new activity), or hot (bring to front). */
-        final int mTransitionType;
+        int mTransitionType;
         /** Whether the process was already running when the transition started. */
-        final boolean mProcessRunning;
+        boolean mProcessRunning;
         /** whether the process of the launching activity didn't have any active activity. */
         final boolean mProcessSwitch;
         /** The process state of the launching activity prior to the launch */
@@ -972,6 +972,19 @@
             // App isn't attached to record yet, so match with info.
             if (info.mLastLaunchedActivity.info.applicationInfo == appInfo) {
                 info.mBindApplicationDelayMs = info.calculateCurrentDelay();
+                if (info.mProcessRunning) {
+                    // It was HOT/WARM launch, but the process was died somehow right after the
+                    // launch request.
+                    info.mProcessRunning = false;
+                    info.mTransitionType = TYPE_TRANSITION_COLD_LAUNCH;
+                    final String msg = "Process " + info.mLastLaunchedActivity.info.processName
+                            + " restarted";
+                    Slog.i(TAG, msg);
+                    if (info.mLaunchingState.mTraceName != null) {
+                        Trace.instant(Trace.TRACE_TAG_ACTIVITY_MANAGER, msg + "#"
+                                + LaunchingState.sTraceSeqId);
+                    }
+                }
             }
         }
     }
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index e30673c..ea06b42 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -7991,6 +7991,9 @@
                 mLastReportedConfiguration.getMergedConfiguration())) {
             ensureActivityConfiguration(0 /* globalChanges */, false /* preserveWindow */,
                     false /* ignoreVisibility */, true /* isRequestedOrientationChanged */);
+            if (mTransitionController.inPlayingTransition(this)) {
+                mTransitionController.mValidateActivityCompat.add(this);
+            }
         }
 
         mAtmService.getTaskChangeNotificationController().notifyActivityRequestedOrientationChanged(
@@ -9410,6 +9413,9 @@
         if (info.applicationInfo == null) {
             return info.getMinAspectRatio();
         }
+        if (mLetterboxUiController.shouldApplyUserMinAspectRatioOverride()) {
+            return mLetterboxUiController.getUserMinAspectRatio();
+        }
         if (!mLetterboxUiController.shouldOverrideMinAspectRatio()) {
             return info.getMinAspectRatio();
         }
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index e261916..349d115 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -6404,9 +6404,9 @@
             // Don't do recursive work.
             return;
         }
-        mInEnsureActivitiesVisible = true;
         mAtmService.mTaskSupervisor.beginActivityVisibilityUpdate();
         try {
+            mInEnsureActivitiesVisible = true;
             forAllRootTasks(rootTask -> {
                 rootTask.ensureActivitiesVisible(starting, configChanges, preserveWindows,
                         notifyClients);
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index 7a201a7..e945bc1 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -294,16 +294,15 @@
     @NonNull private final SynchedDeviceConfig mDeviceConfig;
 
     LetterboxConfiguration(@NonNull final Context systemUiContext) {
-        this(systemUiContext,
-                new LetterboxConfigurationPersister(systemUiContext,
-                        () -> readLetterboxHorizontalReachabilityPositionFromConfig(
-                                systemUiContext, /* forBookMode */ false),
-                        () -> readLetterboxVerticalReachabilityPositionFromConfig(
-                                systemUiContext, /* forTabletopMode */ false),
-                        () -> readLetterboxHorizontalReachabilityPositionFromConfig(
-                                systemUiContext, /* forBookMode */ true),
-                        () -> readLetterboxVerticalReachabilityPositionFromConfig(
-                                systemUiContext, /* forTabletopMode */ true)));
+        this(systemUiContext, new LetterboxConfigurationPersister(
+                () -> readLetterboxHorizontalReachabilityPositionFromConfig(
+                        systemUiContext, /* forBookMode */ false),
+                () -> readLetterboxVerticalReachabilityPositionFromConfig(
+                        systemUiContext, /* forTabletopMode */ false),
+                () -> readLetterboxHorizontalReachabilityPositionFromConfig(
+                        systemUiContext, /* forBookMode */ true),
+                () -> readLetterboxVerticalReachabilityPositionFromConfig(
+                        systemUiContext, /* forTabletopMode */ true)));
     }
 
     @VisibleForTesting
diff --git a/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java b/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java
index 7563397..38aa903 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java
@@ -23,7 +23,6 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.content.Context;
 import android.os.Environment;
 import android.os.StrictMode;
 import android.os.StrictMode.ThreadPolicy;
@@ -53,10 +52,8 @@
     private static final String TAG =
             TAG_WITH_CLASS_NAME ? "LetterboxConfigurationPersister" : TAG_WM;
 
-    @VisibleForTesting
-    static final String LETTERBOX_CONFIGURATION_FILENAME = "letterbox_config";
+    private static final String LETTERBOX_CONFIGURATION_FILENAME = "letterbox_config";
 
-    private final Context mContext;
     private final Supplier<Integer> mDefaultHorizontalReachabilitySupplier;
     private final Supplier<Integer> mDefaultVerticalReachabilitySupplier;
     private final Supplier<Integer> mDefaultBookModeReachabilitySupplier;
@@ -97,36 +94,32 @@
     @NonNull
     private final PersisterQueue mPersisterQueue;
 
-    LetterboxConfigurationPersister(Context systemUiContext,
-            Supplier<Integer> defaultHorizontalReachabilitySupplier,
-            Supplier<Integer> defaultVerticalReachabilitySupplier,
-            Supplier<Integer> defaultBookModeReachabilitySupplier,
-            Supplier<Integer> defaultTabletopModeReachabilitySupplier) {
-        this(systemUiContext, defaultHorizontalReachabilitySupplier,
-                defaultVerticalReachabilitySupplier,
-                defaultBookModeReachabilitySupplier,
-                defaultTabletopModeReachabilitySupplier,
+    LetterboxConfigurationPersister(
+            @NonNull Supplier<Integer> defaultHorizontalReachabilitySupplier,
+            @NonNull Supplier<Integer> defaultVerticalReachabilitySupplier,
+            @NonNull Supplier<Integer> defaultBookModeReachabilitySupplier,
+            @NonNull Supplier<Integer> defaultTabletopModeReachabilitySupplier) {
+        this(defaultHorizontalReachabilitySupplier, defaultVerticalReachabilitySupplier,
+                defaultBookModeReachabilitySupplier, defaultTabletopModeReachabilitySupplier,
                 Environment.getDataSystemDirectory(), new PersisterQueue(),
-                /* completionCallback */ null);
+                /* completionCallback */ null, LETTERBOX_CONFIGURATION_FILENAME);
     }
 
     @VisibleForTesting
-    LetterboxConfigurationPersister(Context systemUiContext,
-            Supplier<Integer> defaultHorizontalReachabilitySupplier,
-            Supplier<Integer> defaultVerticalReachabilitySupplier,
-            Supplier<Integer> defaultBookModeReachabilitySupplier,
-            Supplier<Integer> defaultTabletopModeReachabilitySupplier,
-            File configFolder,
-            PersisterQueue persisterQueue, @Nullable Consumer<String> completionCallback) {
-        mContext = systemUiContext.createDeviceProtectedStorageContext();
+    LetterboxConfigurationPersister(
+            @NonNull Supplier<Integer> defaultHorizontalReachabilitySupplier,
+            @NonNull Supplier<Integer> defaultVerticalReachabilitySupplier,
+            @NonNull Supplier<Integer> defaultBookModeReachabilitySupplier,
+            @NonNull Supplier<Integer> defaultTabletopModeReachabilitySupplier,
+            @NonNull File configFolder, @NonNull PersisterQueue persisterQueue,
+            @Nullable Consumer<String> completionCallback,
+            @NonNull String letterboxConfigurationFileName) {
         mDefaultHorizontalReachabilitySupplier = defaultHorizontalReachabilitySupplier;
         mDefaultVerticalReachabilitySupplier = defaultVerticalReachabilitySupplier;
-        mDefaultBookModeReachabilitySupplier =
-                defaultBookModeReachabilitySupplier;
-        mDefaultTabletopModeReachabilitySupplier =
-                defaultTabletopModeReachabilitySupplier;
+        mDefaultBookModeReachabilitySupplier = defaultBookModeReachabilitySupplier;
+        mDefaultTabletopModeReachabilitySupplier = defaultTabletopModeReachabilitySupplier;
         mCompletionCallback = completionCallback;
-        final File prefFiles = new File(configFolder, LETTERBOX_CONFIGURATION_FILENAME);
+        final File prefFiles = new File(configFolder, letterboxConfigurationFileName);
         mConfigurationFile = new AtomicFile(prefFiles);
         mPersisterQueue = persisterQueue;
         runWithDiskReadsThreadPolicy(this::readCurrentConfiguration);
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index 39f7570..394105a 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -40,6 +40,12 @@
 import static android.content.pm.ActivityInfo.isFixedOrientation;
 import static android.content.pm.ActivityInfo.isFixedOrientationLandscape;
 import static android.content.pm.ActivityInfo.screenOrientationToString;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_16_9;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_3_2;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_4_3;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_DISPLAY_SIZE;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_SPLIT_SCREEN;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_UNSET;
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
@@ -103,6 +109,7 @@
 import android.graphics.Color;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.os.RemoteException;
 import android.util.Slog;
 import android.view.InsetsSource;
 import android.view.InsetsState;
@@ -237,6 +244,10 @@
     // Counter for ActivityRecord#setRequestedOrientation
     private int mSetOrientationRequestCounter = 0;
 
+    // The min aspect ratio override set by user
+    @PackageManager.UserMinAspectRatio
+    private int mUserAspectRatio = USER_MIN_ASPECT_RATIO_UNSET;
+
     // The CompatDisplayInsets of the opaque activity beneath the translucent one.
     private ActivityRecord.CompatDisplayInsets mInheritedCompatDisplayInsets;
 
@@ -1059,7 +1070,7 @@
 
     private float getDefaultMinAspectRatioForUnresizableApps() {
         if (!mLetterboxConfiguration.getIsSplitScreenAspectRatioForUnresizableAppsEnabled()
-                || mActivityRecord.getDisplayContent() == null) {
+                || mActivityRecord.getDisplayArea() == null) {
             return mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps()
                     > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO
                             ? mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps()
@@ -1071,8 +1082,8 @@
 
     float getSplitScreenAspectRatio() {
         // Getting the same aspect ratio that apps get in split screen.
-        final DisplayContent displayContent = mActivityRecord.getDisplayContent();
-        if (displayContent == null) {
+        final DisplayArea displayArea = mActivityRecord.getDisplayArea();
+        if (displayArea == null) {
             return getDefaultMinAspectRatioForUnresizableApps();
         }
         int dividerWindowWidth =
@@ -1080,7 +1091,7 @@
         int dividerInsets =
                 getResources().getDimensionPixelSize(R.dimen.docked_stack_divider_insets);
         int dividerSize = dividerWindowWidth - dividerInsets * 2;
-        final Rect bounds = new Rect(displayContent.getWindowConfiguration().getAppBounds());
+        final Rect bounds = new Rect(displayArea.getWindowConfiguration().getAppBounds());
         if (bounds.width() >= bounds.height()) {
             bounds.inset(/* dx */ dividerSize / 2, /* dy */ 0);
             bounds.right = bounds.centerX();
@@ -1091,14 +1102,57 @@
         return computeAspectRatio(bounds);
     }
 
+    boolean shouldApplyUserMinAspectRatioOverride() {
+        if (!mLetterboxConfiguration.isUserAppAspectRatioSettingsEnabled()) {
+            return false;
+        }
+
+        try {
+            final int userAspectRatio = mActivityRecord.mAtmService.getPackageManager()
+                    .getUserMinAspectRatio(mActivityRecord.packageName, mActivityRecord.mUserId);
+            mUserAspectRatio = userAspectRatio;
+            return userAspectRatio != USER_MIN_ASPECT_RATIO_UNSET;
+        } catch (RemoteException e) {
+            // Don't apply user aspect ratio override
+            Slog.w(TAG, "Exception thrown retrieving aspect ratio user override " + this, e);
+            return false;
+        }
+    }
+
+    float getUserMinAspectRatio() {
+        switch (mUserAspectRatio) {
+            case USER_MIN_ASPECT_RATIO_DISPLAY_SIZE:
+                return getDisplaySizeMinAspectRatio();
+            case USER_MIN_ASPECT_RATIO_SPLIT_SCREEN:
+                return getSplitScreenAspectRatio();
+            case USER_MIN_ASPECT_RATIO_16_9:
+                return 16 / 9f;
+            case USER_MIN_ASPECT_RATIO_4_3:
+                return 4 / 3f;
+            case USER_MIN_ASPECT_RATIO_3_2:
+                return 3 / 2f;
+            default:
+                throw new AssertionError("Unexpected user min aspect ratio override: "
+                        + mUserAspectRatio);
+        }
+    }
+
+    private float getDisplaySizeMinAspectRatio() {
+        final DisplayArea displayArea = mActivityRecord.getDisplayArea();
+        if (displayArea == null) {
+            return mActivityRecord.info.getMinAspectRatio();
+        }
+        final Rect bounds = new Rect(displayArea.getWindowConfiguration().getAppBounds());
+        return computeAspectRatio(bounds);
+    }
+
     private float getDefaultMinAspectRatio() {
-        final DisplayContent displayContent = mActivityRecord.getDisplayContent();
-        if (displayContent == null
+        if (mActivityRecord.getDisplayArea() == null
                 || !mLetterboxConfiguration
                     .getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox()) {
             return mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio();
         }
-        return computeAspectRatio(new Rect(displayContent.getBounds()));
+        return getDisplaySizeMinAspectRatio();
     }
 
     Resources getResources() {
diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java
index b71d918b..f33ecaa 100644
--- a/services/core/java/com/android/server/wm/RecentTasks.java
+++ b/services/core/java/com/android/server/wm/RecentTasks.java
@@ -684,6 +684,26 @@
         }
     }
 
+    /**
+     * Removes the oldest recent task that is compatible with the given one. This is possible if
+     * the task windowing mode changed after being added to the Recents.
+     */
+    void removeCompatibleRecentTask(Task task) {
+        final int taskIndex = mTasks.indexOf(task);
+        if (taskIndex < 0) {
+            return;
+        }
+
+        final int candidateIndex = findRemoveIndexForTask(task, false /* includingSelf */);
+        if (candidateIndex == -1) {
+            // Nothing to trim
+            return;
+        }
+
+        final Task taskToRemove = taskIndex > candidateIndex ? task : mTasks.get(candidateIndex);
+        remove(taskToRemove);
+    }
+
     void removeTasksByPackageName(String packageName, int userId) {
         for (int i = mTasks.size() - 1; i >= 0; --i) {
             final Task task = mTasks.get(i);
@@ -1546,6 +1566,10 @@
      * list (if any).
      */
     private int findRemoveIndexForAddTask(Task task) {
+        return findRemoveIndexForTask(task, true /* includingSelf */);
+    }
+
+    private int findRemoveIndexForTask(Task task, boolean includingSelf) {
         final int recentsCount = mTasks.size();
         final Intent intent = task.intent;
         final boolean document = intent != null && intent.isDocument();
@@ -1601,6 +1625,8 @@
                     // existing task
                     continue;
                 }
+            } else if (!includingSelf) {
+                continue;
             }
             return i;
         }
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 48420d2..2f0c303 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -1849,9 +1849,8 @@
             // Don't do recursive work.
             return;
         }
-
+        mTaskSupervisor.beginActivityVisibilityUpdate();
         try {
-            mTaskSupervisor.beginActivityVisibilityUpdate();
             // First the front root tasks. In case any are not fullscreen and are in front of home.
             for (int displayNdx = getChildCount() - 1; displayNdx >= 0; --displayNdx) {
                 final DisplayContent display = getChildAt(displayNdx);
@@ -3226,6 +3225,10 @@
                         + "not idle", rootTask.getRootTaskId(), resumedActivity);
                 return false;
             }
+            if (mTransitionController.isTransientLaunch(resumedActivity)) {
+                // Not idle if the transient transition animation is running.
+                return false;
+            }
         }
         // End power mode launch when idle.
         mService.endLaunchPowerMode(ActivityTaskManagerService.POWER_MODE_REASON_START_ACTIVITY);
diff --git a/services/core/java/com/android/server/wm/SafeActivityOptions.java b/services/core/java/com/android/server/wm/SafeActivityOptions.java
index c914fa1..fe3094e 100644
--- a/services/core/java/com/android/server/wm/SafeActivityOptions.java
+++ b/services/core/java/com/android/server/wm/SafeActivityOptions.java
@@ -293,26 +293,7 @@
             throw new SecurityException(msg);
         }
         // Check if the caller is allowed to launch on the specified display area.
-        final WindowContainerToken daToken = options.getLaunchTaskDisplayArea();
-        TaskDisplayArea taskDisplayArea = daToken != null
-                ? (TaskDisplayArea) WindowContainer.fromBinder(daToken.asBinder()) : null;
-
-        // If we do not have a task display area token, check if the launch task display area
-        // feature id is specified.
-        if (taskDisplayArea == null) {
-            final int launchTaskDisplayAreaFeatureId = options.getLaunchTaskDisplayAreaFeatureId();
-            if (launchTaskDisplayAreaFeatureId != FEATURE_UNDEFINED) {
-                final int launchDisplayId = options.getLaunchDisplayId() == INVALID_DISPLAY
-                        ? DEFAULT_DISPLAY : options.getLaunchDisplayId();
-                final DisplayContent dc = supervisor.mRootWindowContainer
-                        .getDisplayContent(launchDisplayId);
-                if (dc != null) {
-                    taskDisplayArea = dc.getItemFromTaskDisplayAreas(tda ->
-                            tda.mFeatureId == launchTaskDisplayAreaFeatureId ? tda : null);
-                }
-            }
-        }
-
+        final TaskDisplayArea taskDisplayArea = getLaunchTaskDisplayArea(options, supervisor);
         if (aInfo != null && taskDisplayArea != null
                 && !supervisor.isCallerAllowedToLaunchOnTaskDisplayArea(callingPid, callingUid,
                 taskDisplayArea, aInfo)) {
@@ -428,6 +409,32 @@
         }
     }
 
+    @VisibleForTesting
+    TaskDisplayArea getLaunchTaskDisplayArea(ActivityOptions options,
+            ActivityTaskSupervisor supervisor) {
+        final WindowContainerToken daToken = options.getLaunchTaskDisplayArea();
+        TaskDisplayArea taskDisplayArea = daToken != null
+                ? (TaskDisplayArea) WindowContainer.fromBinder(daToken.asBinder()) : null;
+        if (taskDisplayArea != null) {
+            return taskDisplayArea;
+        }
+
+        // If we do not have a task display area token, check if the launch task display area
+        // feature id is specified.
+        final int launchTaskDisplayAreaFeatureId = options.getLaunchTaskDisplayAreaFeatureId();
+        if (launchTaskDisplayAreaFeatureId != FEATURE_UNDEFINED) {
+            final int launchDisplayId = options.getLaunchDisplayId() == INVALID_DISPLAY
+                    ? DEFAULT_DISPLAY : options.getLaunchDisplayId();
+            final DisplayContent dc = supervisor.mRootWindowContainer
+                    .getDisplayContent(launchDisplayId);
+            if (dc != null) {
+                taskDisplayArea = dc.getItemFromTaskDisplayAreas(tda ->
+                        tda.mFeatureId == launchTaskDisplayAreaFeatureId ? tda : null);
+            }
+        }
+        return taskDisplayArea;
+    }
+
     private boolean isAssistant(ActivityTaskManagerService atmService, int callingUid) {
         if (atmService.mActiveVoiceInteractionServiceComponent == null) {
             return false;
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 0c1f33c..92c0987 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -1020,7 +1020,7 @@
         final WindowContainer<?> parent = getParent();
         final Task thisTask = asTask();
         if (thisTask != null && parent.asTask() == null
-                && mTransitionController.isTransientHide(thisTask)) {
+                && mTransitionController.isTransientVisible(thisTask)) {
             // Keep transient-hide root tasks visible. Non-root tasks still follow standard rule.
             return TASK_FRAGMENT_VISIBILITY_VISIBLE;
         }
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index a143540..eaea53d 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -407,6 +407,36 @@
         return false;
     }
 
+    /** Returns {@code true} if the task should keep visible if this is a transient transition. */
+    boolean isTransientVisible(@NonNull Task task) {
+        if (mTransientLaunches == null) return false;
+        int occludedCount = 0;
+        final int numTransient = mTransientLaunches.size();
+        for (int i = numTransient - 1; i >= 0; --i) {
+            final Task transientRoot = mTransientLaunches.keyAt(i).getRootTask();
+            if (transientRoot == null) continue;
+            final WindowContainer<?> rootParent = transientRoot.getParent();
+            if (rootParent == null || rootParent.getTopChild() == transientRoot) continue;
+            final ActivityRecord topOpaque = mController.mAtm.mTaskSupervisor
+                    .mOpaqueActivityHelper.getOpaqueActivity(rootParent);
+            if (transientRoot.compareTo(topOpaque.getRootTask()) < 0) {
+                occludedCount++;
+            }
+        }
+        if (occludedCount == numTransient) {
+            for (int i = mTransientLaunches.size() - 1; i >= 0; --i) {
+                if (mTransientLaunches.keyAt(i).isDescendantOf(task)) {
+                    // Keep transient activity visible until transition finished, so it won't pause
+                    // with transient-hide tasks that may delay resuming the next top.
+                    return true;
+                }
+            }
+            // Let transient-hide activities pause before transition is finished.
+            return false;
+        }
+        return isInTransientHide(task);
+    }
+
     boolean canApplyDim(@NonNull Task task) {
         if (mTransientLaunches == null) return true;
         final Dimmer dimmer = task.getDimmer();
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 881eddc..dfaa174 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -30,6 +30,7 @@
 import android.app.ActivityManager;
 import android.app.IApplicationThread;
 import android.app.WindowConfiguration;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.Handler;
 import android.os.IBinder;
@@ -141,6 +142,14 @@
     final ArrayList<ActivityRecord> mValidateCommitVis = new ArrayList<>();
 
     /**
+     * List of activity-level participants. ActivityRecord is not expected to change independently,
+     * however, recent compatibility logic can now cause this at arbitrary times determined by
+     * client code. If it happens during an animation, the surface can be left at the wrong spot.
+     * TODO(b/290237710) remove when compat logic is moved.
+     */
+    final ArrayList<ActivityRecord> mValidateActivityCompat = new ArrayList<>();
+
+    /**
      * Currently playing transitions (in the order they were started). When finished, records are
      * removed from this list.
      */
@@ -468,15 +477,22 @@
         if (mCollectingTransition != null && mCollectingTransition.isInTransientHide(task)) {
             return true;
         }
-        for (int i = mWaitingTransitions.size() - 1; i >= 0; --i) {
-            if (mWaitingTransitions.get(i).isInTransientHide(task)) return true;
-        }
         for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) {
             if (mPlayingTransitions.get(i).isInTransientHide(task)) return true;
         }
         return false;
     }
 
+    boolean isTransientVisible(@NonNull Task task) {
+        if (mCollectingTransition != null && mCollectingTransition.isTransientVisible(task)) {
+            return true;
+        }
+        for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) {
+            if (mPlayingTransitions.get(i).isTransientVisible(task)) return true;
+        }
+        return false;
+    }
+
     boolean canApplyDim(@Nullable Task task) {
         if (task == null) {
             // Always allow non-activity window.
@@ -896,6 +912,14 @@
             }
         }
         mValidateCommitVis.clear();
+        for (int i = 0; i < mValidateActivityCompat.size(); ++i) {
+            ActivityRecord ar = mValidateActivityCompat.get(i);
+            if (ar.getSurfaceControl() == null) continue;
+            final Point tmpPos = new Point();
+            ar.getRelativePosition(tmpPos);
+            ar.getSyncTransaction().setPosition(ar.getSurfaceControl(), tmpPos.x, tmpPos.y);
+        }
+        mValidateActivityCompat.clear();
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 0eb452d..164c8b0 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -1615,6 +1615,7 @@
         final int count = tasksToReparent.size();
         for (int i = 0; i < count; ++i) {
             final Task task = tasksToReparent.get(i);
+            final int prevWindowingMode = task.getWindowingMode();
             if (syncId >= 0) {
                 addToSyncSet(syncId, task);
             }
@@ -1628,6 +1629,12 @@
                         hop.getToTop() ? POSITION_TOP : POSITION_BOTTOM,
                         false /*moveParents*/, "processChildrenTaskReparentHierarchyOp");
             }
+            // Trim the compatible Recent task (if any) after the Task is reparented and now has
+            // a different windowing mode, in order to prevent redundant Recent tasks after
+            // reparenting.
+            if (prevWindowingMode != task.getWindowingMode()) {
+                mService.mTaskSupervisor.mRecentTasks.removeCompatibleRecentTask(task);
+            }
         }
 
         if (transition != null) transition.collect(newParent);
diff --git a/services/flags/Android.bp b/services/flags/Android.bp
new file mode 100644
index 0000000..2d0337dc
--- /dev/null
+++ b/services/flags/Android.bp
@@ -0,0 +1,17 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+java_library_static {
+    name: "services.flags",
+    defaults: ["platform_service_defaults"],
+    srcs: [
+        "java/**/*.java",
+    ],
+    libs: ["services.core"],
+}
diff --git a/services/flags/OWNERS b/services/flags/OWNERS
new file mode 100644
index 0000000..3925b5c
--- /dev/null
+++ b/services/flags/OWNERS
@@ -0,0 +1,6 @@
+# Bug component: 1306523
+
+mankoff@google.com
+
+pixel@google.com
+dsandler@android.com
diff --git a/services/flags/java/com/android/server/flags/DynamicFlagBinderDelegate.java b/services/flags/java/com/android/server/flags/DynamicFlagBinderDelegate.java
new file mode 100644
index 0000000..0db3287
--- /dev/null
+++ b/services/flags/java/com/android/server/flags/DynamicFlagBinderDelegate.java
@@ -0,0 +1,280 @@
+/*
+ * 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.flags;
+
+import android.annotation.NonNull;
+import android.flags.IFeatureFlagsCallback;
+import android.flags.SyncableFlag;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.provider.DeviceConfig;
+import android.util.Slog;
+
+import com.android.internal.os.BackgroundThread;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+
+/**
+ * Handles DynamicFlags for {@link FeatureFlagsBinder}.
+ *
+ * Dynamic flags are simultaneously simpler and more complicated than process stable flags. We can
+ * return whatever value is last known for a flag is, without too much worry about the flags
+ * changing (they are dynamic after all). However, we have to alert all the relevant clients
+ * about those flag changes, and need to be able to restore to a default value if the flag gets
+ * reset/erased during runtime.
+ */
+class DynamicFlagBinderDelegate {
+
+    private final FlagOverrideStore mFlagStore;
+    private final FlagCache<DynamicFlagData> mDynamicFlags = new FlagCache<>();
+    private final Map<Integer, Set<IFeatureFlagsCallback>> mCallbacks = new HashMap<>();
+    private static final Function<Integer, Set<IFeatureFlagsCallback>> NEW_CALLBACK_SET =
+            k -> new HashSet<>();
+
+    private final DeviceConfig.OnPropertiesChangedListener mDeviceConfigListener =
+            new DeviceConfig.OnPropertiesChangedListener() {
+                @Override
+                public void onPropertiesChanged(@NonNull DeviceConfig.Properties properties) {
+                    String ns = properties.getNamespace();
+                    for (String name : properties.getKeyset()) {
+                        // Don't alert for flags we don't care about.
+                        // Don't alert for flags that have been overridden locally.
+                        if (!mDynamicFlags.contains(ns, name) || mFlagStore.contains(ns, name)) {
+                            continue;
+                        }
+                        mFlagChangeCallback.onFlagChanged(
+                                ns, name, properties.getString(name, null));
+                    }
+                }
+            };
+
+    private final FlagOverrideStore.FlagChangeCallback mFlagChangeCallback =
+            (namespace, name, value) -> {
+                // Don't bother with callbacks for non-dynamic flags.
+                if (!mDynamicFlags.contains(namespace, name)) {
+                    return;
+                }
+
+                // Don't bother with callbacks if nothing changed.
+                // Handling erasure (null) is special, as we may be restoring back to a value
+                // we were already at.
+                DynamicFlagData data = mDynamicFlags.getOrNull(namespace, name);
+                if (data == null) {
+                    return;  // shouldn't happen, but better safe than sorry.
+                }
+                if (value == null) {
+                    if (data.getValue().equals(data.getDefaultValue())) {
+                        return;
+                    }
+                    value = data.getDefaultValue();
+                } else if (data.getValue().equals(value)) {
+                    return;
+                }
+                data.setValue(value);
+
+                final Set<IFeatureFlagsCallback> cbCopy;
+                synchronized (mCallbacks) {
+                    cbCopy = new HashSet<>();
+
+                    for (Integer pid : mCallbacks.keySet()) {
+                        if (data.containsPid(pid)) {
+                            cbCopy.addAll(mCallbacks.get(pid));
+                        }
+                    }
+                }
+                SyncableFlag sFlag = new SyncableFlag(namespace, name, value, true);
+                cbCopy.forEach(cb -> {
+                    try {
+                        cb.onFlagChange(sFlag);
+                    } catch (RemoteException e) {
+                        Slog.w(
+                                FeatureFlagsService.TAG,
+                                "Failed to communicate flag change to client.");
+                    }
+                });
+            };
+
+    DynamicFlagBinderDelegate(FlagOverrideStore flagStore) {
+        mFlagStore = flagStore;
+        mFlagStore.setChangeCallback(mFlagChangeCallback);
+    }
+
+    SyncableFlag syncDynamicFlag(int pid, SyncableFlag sf) {
+        if (!sf.isDynamic()) {
+            return sf;
+        }
+
+        String ns = sf.getNamespace();
+        String name = sf.getName();
+
+        // Dynamic flags don't need any special threading or synchronization considerations.
+        // We simply give them whatever the current value is.
+        // However, we do need to keep track of dynamic flags, so that we can alert
+        // about changes coming in from adb, DeviceConfig, or other sources.
+        // And also so that we can keep flags relatively consistent across processes.
+
+        DynamicFlagData data = mDynamicFlags.getOrNull(ns, name);
+        String value = getFlagValue(ns, name, sf.getValue());
+        // DeviceConfig listeners are per-namespace.
+        if (!mDynamicFlags.containsNamespace(ns)) {
+            DeviceConfig.addOnPropertiesChangedListener(
+                    ns, BackgroundThread.getExecutor(), mDeviceConfigListener);
+        }
+        data.addClientPid(pid);
+        data.setValue(value);
+        // Store the default value so that if an override gets erased, we can restore
+        // to something.
+        data.setDefaultValue(sf.getValue());
+
+        return new SyncableFlag(sf.getNamespace(), sf.getName(), value, true);
+    }
+
+
+    void registerCallback(int pid, IFeatureFlagsCallback callback) {
+        // Always add callback so that we don't end up with a possible race/leak.
+        // We remove the callback directly if we fail to call #linkToDeath.
+        // If we tried to add the callback after we linked, then we could end up in a
+        // scenario where we link, then the binder dies, firing our BinderGriever which tries
+        // to remove the callback (which has not yet been added), then finally we add the
+        // callback, creating a leak.
+        Set<IFeatureFlagsCallback> callbacks;
+        synchronized (mCallbacks) {
+            callbacks = mCallbacks.computeIfAbsent(pid, NEW_CALLBACK_SET);
+            callbacks.add(callback);
+        }
+        try {
+            callback.asBinder().linkToDeath(new BinderGriever(pid), 0);
+        } catch (RemoteException e) {
+            Slog.e(
+                    FeatureFlagsService.TAG,
+                    "Failed to link to binder death. Callback not registered.");
+            synchronized (mCallbacks) {
+                callbacks.remove(callback);
+            }
+        }
+    }
+
+    void unregisterCallback(int pid, IFeatureFlagsCallback callback) {
+        // No need to unlink, since the BinderGriever will essentially be a no-op.
+        // We would have to track our BinderGriever's in a map otherwise.
+        synchronized (mCallbacks) {
+            Set<IFeatureFlagsCallback> callbacks =
+                    mCallbacks.computeIfAbsent(pid, NEW_CALLBACK_SET);
+            callbacks.remove(callback);
+        }
+    }
+
+    String getFlagValue(String namespace, String name, String defaultValue) {
+        // If we already have a value cached, just use that.
+        String value = null;
+        DynamicFlagData data = mDynamicFlags.getOrNull(namespace, name);
+        if (data != null) {
+            value = data.getValue();
+        } else {
+            // Put the value in the cache for future reference.
+            data = new DynamicFlagData(namespace, name);
+            mDynamicFlags.setIfChanged(namespace, name, data);
+        }
+        // If we're not in a release build, flags can be overridden locally on device.
+        if (!Build.IS_USER && value == null) {
+            value = mFlagStore.get(namespace, name);
+        }
+        // If we still don't have a value, maybe DeviceConfig does?
+        // Fallback to sf.getValue() here as well.
+        if (value == null) {
+            value = DeviceConfig.getString(namespace, name, defaultValue);
+        }
+
+        return value;
+    }
+
+    private static class DynamicFlagData {
+        private final String mNamespace;
+        private final String mName;
+        private final Set<Integer> mPids = new HashSet<>();
+        private String mValue;
+        private String mDefaultValue;
+
+        private DynamicFlagData(String namespace, String name) {
+            mNamespace = namespace;
+            mName = name;
+        }
+
+        String getValue() {
+            return mValue;
+        }
+
+        void setValue(String value) {
+            mValue = value;
+        }
+
+        String getDefaultValue() {
+            return mDefaultValue;
+        }
+
+        void setDefaultValue(String value) {
+            mDefaultValue = value;
+        }
+
+        void addClientPid(int pid) {
+            mPids.add(pid);
+        }
+
+        boolean containsPid(int pid) {
+            return mPids.contains(pid);
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (other == null || !(other instanceof DynamicFlagData)) {
+                return false;
+            }
+
+            DynamicFlagData o = (DynamicFlagData) other;
+
+            return mName.equals(o.mName) && mNamespace.equals(o.mNamespace)
+                    && mValue.equals(o.mValue) && mDefaultValue.equals(o.mDefaultValue);
+        }
+
+        @Override
+        public int hashCode() {
+            return mName.hashCode() + mNamespace.hashCode()
+              + mValue.hashCode() + mDefaultValue.hashCode();
+        }
+    }
+
+
+    private class BinderGriever implements IBinder.DeathRecipient {
+        private final int mPid;
+
+        private BinderGriever(int pid) {
+            mPid = pid;
+        }
+
+        @Override
+        public void binderDied() {
+            synchronized (mCallbacks) {
+                mCallbacks.remove(mPid);
+            }
+        }
+    }
+}
diff --git a/services/flags/java/com/android/server/flags/FeatureFlagsBinder.java b/services/flags/java/com/android/server/flags/FeatureFlagsBinder.java
new file mode 100644
index 0000000..1fa8532
--- /dev/null
+++ b/services/flags/java/com/android/server/flags/FeatureFlagsBinder.java
@@ -0,0 +1,168 @@
+/*
+ * 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.flags;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.flags.IFeatureFlags;
+import android.flags.IFeatureFlagsCallback;
+import android.flags.SyncableFlag;
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+
+import com.android.internal.flags.CoreFlags;
+import com.android.server.flags.FeatureFlagsService.PermissionsChecker;
+
+import java.io.FileOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+class FeatureFlagsBinder extends IFeatureFlags.Stub {
+    private final FlagOverrideStore mFlagStore;
+    private final FlagsShellCommand mShellCommand;
+    private final FlagCache<String> mFlagCache = new FlagCache<>();
+    private final DynamicFlagBinderDelegate mDynamicFlagDelegate;
+    private final PermissionsChecker mPermissionsChecker;
+
+    FeatureFlagsBinder(
+            FlagOverrideStore flagStore,
+            FlagsShellCommand shellCommand,
+            PermissionsChecker permissionsChecker) {
+        mFlagStore = flagStore;
+        mShellCommand = shellCommand;
+        mDynamicFlagDelegate = new DynamicFlagBinderDelegate(flagStore);
+        mPermissionsChecker = permissionsChecker;
+    }
+
+    @Override
+    public void registerCallback(IFeatureFlagsCallback callback) {
+        mDynamicFlagDelegate.registerCallback(getCallingPid(), callback);
+    }
+
+    @Override
+    public void unregisterCallback(IFeatureFlagsCallback callback) {
+        mDynamicFlagDelegate.unregisterCallback(getCallingPid(), callback);
+    }
+
+    // Note: The internals of this method should be kept in sync with queryFlags
+    // as they both should return identical results. The difference is that this method
+    // caches any values it receives and/or reads, whereas queryFlags does not.
+
+    @Override
+    public List<SyncableFlag> syncFlags(List<SyncableFlag> incomingFlags) {
+        int pid = getCallingPid();
+        List<SyncableFlag> outputFlags = new ArrayList<>();
+
+        boolean hasFullSyncPrivileges = false;
+        SecurityException permissionFailureException = null;
+        try {
+            assertSyncPermission();
+            hasFullSyncPrivileges = true;
+        } catch (SecurityException e) {
+            permissionFailureException = e;
+        }
+
+        for (SyncableFlag sf : incomingFlags) {
+            if (!hasFullSyncPrivileges && !CoreFlags.isCoreFlag(sf)) {
+                throw permissionFailureException;
+            }
+
+            String ns = sf.getNamespace();
+            String name = sf.getName();
+            SyncableFlag outFlag;
+            if (sf.isDynamic()) {
+                outFlag = mDynamicFlagDelegate.syncDynamicFlag(pid, sf);
+            } else {
+                synchronized (mFlagCache) {
+                    String value = mFlagCache.getOrNull(ns, name);
+                    if (value == null) {
+                        String overrideValue = Build.IS_USER ? null : mFlagStore.get(ns, name);
+                        value = overrideValue != null ? overrideValue : sf.getValue();
+                        mFlagCache.setIfChanged(ns, name, value);
+                    }
+                    outFlag = new SyncableFlag(sf.getNamespace(), sf.getName(), value, false);
+                }
+            }
+            outputFlags.add(outFlag);
+        }
+        return outputFlags;
+    }
+
+    @Override
+    public void overrideFlag(SyncableFlag flag) {
+        assertWritePermission();
+        mFlagStore.set(flag.getNamespace(), flag.getName(), flag.getValue());
+    }
+
+    @Override
+    public void resetFlag(SyncableFlag flag) {
+        assertWritePermission();
+        mFlagStore.erase(flag.getNamespace(), flag.getName());
+    }
+
+    @Override
+    public List<SyncableFlag> queryFlags(List<SyncableFlag> incomingFlags) {
+        assertSyncPermission();
+        List<SyncableFlag> outputFlags = new ArrayList<>();
+        for (SyncableFlag sf : incomingFlags) {
+            String ns = sf.getNamespace();
+            String name = sf.getName();
+            String value;
+            String storeValue = mFlagStore.get(ns, name);
+            boolean overridden  = storeValue != null;
+
+            if (sf.isDynamic()) {
+                value = mDynamicFlagDelegate.getFlagValue(ns, name, sf.getValue());
+            } else {
+                value = mFlagCache.getOrNull(ns, name);
+                if (value == null) {
+                    value = Build.IS_USER ? null : storeValue;
+                    if (value == null) {
+                        value = sf.getValue();
+                    }
+                }
+            }
+            outputFlags.add(new SyncableFlag(
+                    sf.getNamespace(), sf.getName(), value, sf.isDynamic(), overridden));
+        }
+
+        return outputFlags;
+    }
+
+    private void assertSyncPermission() {
+        mPermissionsChecker.assertSyncPermission();
+        clearCallingIdentity();
+    }
+
+    private void assertWritePermission() {
+        mPermissionsChecker.assertWritePermission();
+        clearCallingIdentity();
+    }
+
+
+    @SystemApi
+    public int handleShellCommand(
+            @NonNull ParcelFileDescriptor in,
+            @NonNull ParcelFileDescriptor out,
+            @NonNull ParcelFileDescriptor err,
+            @NonNull String[] args) {
+        FileOutputStream fout = new FileOutputStream(out.getFileDescriptor());
+        FileOutputStream ferr = new FileOutputStream(err.getFileDescriptor());
+
+        return mShellCommand.process(args, fout, ferr);
+    }
+}
diff --git a/services/flags/java/com/android/server/flags/FeatureFlagsService.java b/services/flags/java/com/android/server/flags/FeatureFlagsService.java
new file mode 100644
index 0000000..93b9e9e
--- /dev/null
+++ b/services/flags/java/com/android/server/flags/FeatureFlagsService.java
@@ -0,0 +1,113 @@
+/*
+ * 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.flags;
+
+import static android.Manifest.permission.SYNC_FLAGS;
+import static android.Manifest.permission.WRITE_FLAGS;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.flags.FeatureFlags;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.SystemService;
+
+/**
+ * A service that manages syncing {@link android.flags.FeatureFlags} across processes.
+ *
+ * This service holds flags stable for at least the lifetime of a process, meaning that if
+ * a process comes online with a flag set to true, any other process that connects here and
+ * tries to read the same flag will also receive the flag as true. The flag will remain stable
+ * until either all of the interested processes have died, or the device restarts.
+ *
+ * TODO(279054964): Add to dumpsys
+ * @hide
+ */
+public class FeatureFlagsService extends SystemService {
+
+    static final String TAG = "FeatureFlagsService";
+    private final FlagOverrideStore mFlagStore;
+    private final FlagsShellCommand mShellCommand;
+
+    /**
+     * Initializes the system service.
+     *
+     * @param context The system server context.
+     */
+    public FeatureFlagsService(Context context) {
+        super(context);
+        mFlagStore = new FlagOverrideStore(
+                new GlobalSettingsProxy(context.getContentResolver()));
+        mShellCommand = new FlagsShellCommand(mFlagStore);
+    }
+
+    @Override
+    public void onStart() {
+        Slog.d(TAG, "Started Feature Flag Service");
+        FeatureFlagsBinder service = new FeatureFlagsBinder(
+                mFlagStore, mShellCommand, new PermissionsChecker(getContext()));
+        publishBinderService(
+                Context.FEATURE_FLAGS_SERVICE, service);
+        publishLocalService(FeatureFlags.class, new FeatureFlags(service));
+    }
+
+    @Override
+    public void onBootPhase(int phase) {
+        super.onBootPhase(phase);
+
+        if (phase == PHASE_SYSTEM_SERVICES_READY) {
+            // Immediately sync our core flags so that they get locked in. We don't want third-party
+            // apps to override them, and syncing immediately is the easiest way to prevent that.
+            FeatureFlags.getInstance().sync();
+        }
+    }
+
+    /**
+     * Delegate for checking flag permissions.
+     */
+    @VisibleForTesting
+    public static class PermissionsChecker {
+        private final Context mContext;
+
+        @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+        public PermissionsChecker(Context context) {
+            mContext = context;
+        }
+
+        /**
+         * Ensures that the caller has {@link SYNC_FLAGS} permission.
+         */
+        @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+        public void assertSyncPermission() {
+            if (mContext.checkCallingOrSelfPermission(SYNC_FLAGS)
+                    != PackageManager.PERMISSION_GRANTED) {
+                throw new SecurityException(
+                        "Non-core flag queried. Requires SYNC_FLAGS permission!");
+            }
+        }
+
+        /**
+         * Ensures that the caller has {@link WRITE_FLAGS} permission.
+         */
+        @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+        public void assertWritePermission() {
+            if (mContext.checkCallingPermission(WRITE_FLAGS) != PackageManager.PERMISSION_GRANTED) {
+                throw new SecurityException("Requires WRITE_FLAGS permission!");
+            }
+        }
+    }
+}
diff --git a/services/flags/java/com/android/server/flags/FlagCache.java b/services/flags/java/com/android/server/flags/FlagCache.java
new file mode 100644
index 0000000..cee1578
--- /dev/null
+++ b/services/flags/java/com/android/server/flags/FlagCache.java
@@ -0,0 +1,103 @@
+/*
+ * 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.flags;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+/**
+ * Threadsafe cache of values that stores the supplied default on cache miss.
+ *
+ * @param <V> The type of value to store.
+ */
+public class FlagCache<V> {
+    private final Function<String, HashMap<String, V>> mNewHashMap = k -> new HashMap<>();
+
+    // Cache is organized first by namespace, then by name. All values are stored as strings.
+    final Map<String, Map<String, V>> mCache = new HashMap<>();
+
+    FlagCache() {
+    }
+
+    /**
+     * Returns true if the namespace exists in the cache already.
+     */
+    boolean containsNamespace(String namespace) {
+        synchronized (mCache) {
+            return mCache.containsKey(namespace);
+        }
+    }
+
+    /**
+     * Returns true if the value is stored in the cache.
+     */
+    boolean contains(String namespace, String name) {
+        synchronized (mCache) {
+            Map<String, V> nsCache = mCache.get(namespace);
+            return nsCache != null && nsCache.containsKey(name);
+        }
+    }
+
+    /**
+     * Sets the value if it is different from what is currently stored.
+     *
+     * If the value is not set, or the current value is null, it will store the value and
+     * return true.
+     *
+     * @return True if the value was set. False if the value is the same.
+     */
+    boolean setIfChanged(String namespace, String name, V value) {
+        synchronized (mCache) {
+            Map<String, V> nsCache = mCache.computeIfAbsent(namespace, mNewHashMap);
+            V curValue = nsCache.get(name);
+            if (curValue == null || !curValue.equals(value)) {
+                nsCache.put(name, value);
+                return true;
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Gets the current value from the cache, setting it if it is currently absent.
+     *
+     * @return The value that is now in the cache after the call to the method.
+     */
+    V getOrSet(String namespace, String name, V defaultValue) {
+        synchronized (mCache) {
+            Map<String, V> nsCache = mCache.computeIfAbsent(namespace, mNewHashMap);
+            V value = nsCache.putIfAbsent(name, defaultValue);
+            return value == null ? defaultValue : value;
+        }
+    }
+
+    /**
+     * Gets the current value from the cache, returning null if not present.
+     *
+     * @return The value that is now in the cache if there is one.
+     */
+    V getOrNull(String namespace, String name) {
+        synchronized (mCache) {
+            Map<String, V> nsCache = mCache.get(namespace);
+            if (nsCache == null) {
+                return null;
+            }
+            return nsCache.get(name);
+        }
+    }
+}
diff --git a/services/flags/java/com/android/server/flags/FlagOverrideStore.java b/services/flags/java/com/android/server/flags/FlagOverrideStore.java
new file mode 100644
index 0000000..b1ddc7e6
--- /dev/null
+++ b/services/flags/java/com/android/server/flags/FlagOverrideStore.java
@@ -0,0 +1,122 @@
+/*
+ * 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.flags;
+
+import android.database.Cursor;
+import android.provider.Settings;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Persistent storage for the {@link FeatureFlagsService}.
+ *
+ * The implementation stores data in Settings.<store> (generally {@link Settings.Global}
+ * is expected).
+ */
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+public class FlagOverrideStore {
+    private static final String KEYNAME_PREFIX = "flag|";
+    private static final String NAMESPACE_NAME_SEPARATOR = ".";
+
+    private final SettingsProxy mSettingsProxy;
+
+    private FlagChangeCallback mCallback;
+
+    FlagOverrideStore(SettingsProxy settingsProxy) {
+        mSettingsProxy = settingsProxy;
+    }
+
+    void setChangeCallback(FlagChangeCallback callback) {
+        mCallback = callback;
+    }
+
+    /** Returns true if a non-null value is in the store. */
+    boolean contains(String namespace, String name) {
+        return get(namespace, name) != null;
+    }
+
+    /** Put a value in the store. */
+    @VisibleForTesting
+    public void set(String namespace, String name, String value) {
+        mSettingsProxy.putString(getPropName(namespace, name), value);
+        mCallback.onFlagChanged(namespace, name, value);
+    }
+
+    /** Read a value out of the store. */
+    @VisibleForTesting
+    public String get(String namespace, String name) {
+        return mSettingsProxy.getString(getPropName(namespace, name));
+    }
+
+    /** Erase a value from the store. */
+    @VisibleForTesting
+    public void erase(String namespace, String name) {
+        set(namespace, name, null);
+    }
+
+    Map<String, Map<String, String>> getFlags() {
+        return getFlagsForNamespace(null);
+    }
+
+    Map<String, Map<String, String>> getFlagsForNamespace(String namespace) {
+        Cursor c = mSettingsProxy.getContentResolver().query(
+                Settings.Global.CONTENT_URI,
+                new String[]{Settings.NameValueTable.NAME, Settings.NameValueTable.VALUE},
+                null, // Doesn't support a "LIKE" query
+                null,
+                null
+        );
+
+        if (c == null) {
+            return Map.of();
+        }
+        int keynamePrefixLength = KEYNAME_PREFIX.length();
+        Map<String, Map<String, String>> results = new HashMap<>();
+        while (c.moveToNext()) {
+            String key = c.getString(0);
+            if (!key.startsWith(KEYNAME_PREFIX)
+                    || key.indexOf(NAMESPACE_NAME_SEPARATOR, keynamePrefixLength) < 0) {
+                continue;
+            }
+            String value = c.getString(1);
+            if (value == null || value.isEmpty()) {
+                continue;
+            }
+            String ns = key.substring(keynamePrefixLength, key.indexOf(NAMESPACE_NAME_SEPARATOR));
+            if (namespace != null && !namespace.equals(ns)) {
+                continue;
+            }
+            String name = key.substring(key.indexOf(NAMESPACE_NAME_SEPARATOR) + 1);
+            results.putIfAbsent(ns, new HashMap<>());
+            results.get(ns).put(name, value);
+        }
+        c.close();
+        return results;
+    }
+
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    static String getPropName(String namespace, String name) {
+        return KEYNAME_PREFIX + namespace + NAMESPACE_NAME_SEPARATOR + name;
+    }
+
+    interface FlagChangeCallback {
+        void onFlagChanged(String namespace, String name, String value);
+    }
+}
diff --git a/services/flags/java/com/android/server/flags/FlagsShellCommand.java b/services/flags/java/com/android/server/flags/FlagsShellCommand.java
new file mode 100644
index 0000000..b7896ee
--- /dev/null
+++ b/services/flags/java/com/android/server/flags/FlagsShellCommand.java
@@ -0,0 +1,214 @@
+/*
+ * 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.flags;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.FastPrintWriter;
+
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Process command line input for the flags service.
+ */
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+public class FlagsShellCommand {
+    private final FlagOverrideStore mFlagStore;
+
+    FlagsShellCommand(FlagOverrideStore flagStore) {
+        mFlagStore = flagStore;
+    }
+
+    /**
+     * Interpret the command supplied in the constructor.
+     *
+     * @return Zero on success or non-zero on error.
+     */
+    public int process(
+            String[] args,
+            OutputStream out,
+            OutputStream err) {
+        PrintWriter outPw = new FastPrintWriter(out);
+        PrintWriter errPw = new FastPrintWriter(err);
+
+        if (args.length == 0) {
+            return printHelp(outPw);
+        }
+        switch (args[0].toLowerCase(Locale.ROOT)) {
+            case "help":
+                return printHelp(outPw);
+            case "list":
+                return listCmd(args, outPw, errPw);
+            case "set":
+                return setCmd(args, outPw, errPw);
+            case "get":
+                return getCmd(args, outPw, errPw);
+            case "erase":
+                return eraseCmd(args, outPw, errPw);
+            default:
+                return unknownCmd(outPw);
+        }
+    }
+
+    private int printHelp(PrintWriter outPw) {
+        outPw.println("Feature Flags command, allowing listing, setting, getting, and erasing of");
+        outPw.println("local flag overrides on a device.");
+        outPw.println();
+        outPw.println("Commands:");
+        outPw.println("  list [namespace]");
+        outPw.println("    List all flag overrides. Namespace is optional.");
+        outPw.println();
+        outPw.println("  get <namespace> <name>");
+        outPw.println("    Return the string value of a specific flag, or <unset>");
+        outPw.println();
+        outPw.println("  set <namespace> <name> <value>");
+        outPw.println("    Set a specific flag");
+        outPw.println();
+        outPw.println("  erase <namespace> <name>");
+        outPw.println("    Unset a specific flag");
+        outPw.flush();
+        return 0;
+    }
+
+    private int listCmd(String[] args, PrintWriter outPw, PrintWriter errPw) {
+        if (!validateNumArguments(args, 0, 1, args[0], errPw)) {
+            errPw.println("Expected `" + args[0] + " [namespace]`");
+            errPw.flush();
+            return -1;
+        }
+        Map<String, Map<String, String>> overrides;
+        if (args.length == 2) {
+            overrides = mFlagStore.getFlagsForNamespace(args[1]);
+        } else {
+            overrides = mFlagStore.getFlags();
+        }
+        if (overrides.isEmpty()) {
+            outPw.println("No overrides set");
+        } else {
+            int longestNamespaceLen = "namespace".length();
+            int longestFlagLen = "flag".length();
+            int longestValLen = "value".length();
+            for (Map.Entry<String, Map<String, String>> namespace : overrides.entrySet()) {
+                longestNamespaceLen = Math.max(longestNamespaceLen, namespace.getKey().length());
+                for (Map.Entry<String, String> flag : namespace.getValue().entrySet()) {
+                    longestFlagLen = Math.max(longestFlagLen, flag.getKey().length());
+                    longestValLen = Math.max(longestValLen, flag.getValue().length());
+                }
+            }
+            outPw.print(String.format("%-" + longestNamespaceLen + "s", "namespace"));
+            outPw.print(' ');
+            outPw.print(String.format("%-" + longestFlagLen + "s", "flag"));
+            outPw.print(' ');
+            outPw.println("value");
+            for (int i = 0; i < longestNamespaceLen; i++) {
+                outPw.print('=');
+            }
+            outPw.print(' ');
+            for (int i = 0; i < longestFlagLen; i++) {
+                outPw.print('=');
+            }
+            outPw.print(' ');
+            for (int i = 0; i < longestValLen; i++) {
+                outPw.print('=');
+            }
+            outPw.println();
+            for (Map.Entry<String, Map<String, String>> namespace : overrides.entrySet()) {
+                for (Map.Entry<String, String> flag : namespace.getValue().entrySet()) {
+                    outPw.print(
+                            String.format("%-" + longestNamespaceLen + "s", namespace.getKey()));
+                    outPw.print(' ');
+                    outPw.print(String.format("%-" + longestFlagLen + "s", flag.getKey()));
+                    outPw.print(' ');
+                    outPw.println(flag.getValue());
+                }
+            }
+        }
+        outPw.flush();
+        return 0;
+    }
+
+    private int setCmd(String[] args, PrintWriter outPw, PrintWriter errPw) {
+        if (!validateNumArguments(args, 3, args[0], errPw)) {
+            errPw.println("Expected `" + args[0] + " <namespace> <name> <value>`");
+            errPw.flush();
+            return -1;
+        }
+        mFlagStore.set(args[1], args[2], args[3]);
+        outPw.println("Flag " + args[1] + "." + args[2] + " is now " + args[3]);
+        outPw.flush();
+        return 0;
+    }
+
+    private int getCmd(String[] args, PrintWriter outPw, PrintWriter errPw) {
+        if (!validateNumArguments(args, 2, args[0], errPw)) {
+            errPw.println("Expected `" + args[0] + " <namespace> <name>`");
+            errPw.flush();
+            return -1;
+        }
+
+        String value = mFlagStore.get(args[1], args[2]);
+        outPw.print(args[1] + "." + args[2] + " is ");
+        if (value == null || value.isEmpty()) {
+            outPw.println("<unset>");
+        } else {
+            outPw.println("\"" + value.translateEscapes() + "\"");
+        }
+        outPw.flush();
+        return 0;
+    }
+
+    private int eraseCmd(String[] args, PrintWriter outPw, PrintWriter errPw) {
+        if (!validateNumArguments(args, 2, args[0], errPw)) {
+            errPw.println("Expected `" + args[0] + " <namespace> <name>`");
+            errPw.flush();
+            return -1;
+        }
+        mFlagStore.erase(args[1], args[2]);
+        outPw.println("Erased " + args[1] + "." + args[2]);
+        return 0;
+    }
+
+    private int unknownCmd(PrintWriter outPw) {
+        outPw.println("This command is unknown.");
+        printHelp(outPw);
+        outPw.flush();
+        return -1;
+    }
+
+    private boolean validateNumArguments(
+            String[] args, int exactly, String cmdName, PrintWriter errPw) {
+        return validateNumArguments(args, exactly, exactly, cmdName, errPw);
+    }
+
+    private boolean validateNumArguments(
+            String[] args, int min, int max, String cmdName, PrintWriter errPw) {
+        int len = args.length - 1; // Discount the command itself.
+        if (len < min) {
+            errPw.println(
+                    "Less than " + min + " arguments provided for \"" + cmdName + "\" command.");
+            return false;
+        } else if (len > max) {
+            errPw.println(
+                    "More than " + max + " arguments provided for \"" + cmdName + "\" command.");
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/services/flags/java/com/android/server/flags/GlobalSettingsProxy.java b/services/flags/java/com/android/server/flags/GlobalSettingsProxy.java
new file mode 100644
index 0000000..acb7bb5
--- /dev/null
+++ b/services/flags/java/com/android/server/flags/GlobalSettingsProxy.java
@@ -0,0 +1,68 @@
+/*
+ * 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.flags;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.provider.Settings;
+
+class GlobalSettingsProxy implements SettingsProxy {
+    private final ContentResolver mContentResolver;
+
+    GlobalSettingsProxy(ContentResolver contentResolver) {
+        mContentResolver = contentResolver;
+    }
+
+    @Override
+    public ContentResolver getContentResolver() {
+        return mContentResolver;
+    }
+
+    @Override
+    public Uri getUriFor(String name) {
+        return Settings.Global.getUriFor(name);
+    }
+
+    @Override
+    public String getStringForUser(String name, int userHandle) {
+        return Settings.Global.getStringForUser(mContentResolver, name, userHandle);
+    }
+
+    @Override
+    public boolean putString(String name, String value, boolean overrideableByRestore) {
+        throw new UnsupportedOperationException(
+                "This method only exists publicly for Settings.System and Settings.Secure");
+    }
+
+    @Override
+    public boolean putStringForUser(String name, String value, int userHandle) {
+        return Settings.Global.putStringForUser(mContentResolver, name, value, userHandle);
+    }
+
+    @Override
+    public boolean putStringForUser(String name, String value, String tag, boolean makeDefault,
+            int userHandle, boolean overrideableByRestore) {
+        return Settings.Global.putStringForUser(
+                mContentResolver, name, value, tag, makeDefault, userHandle,
+                overrideableByRestore);
+    }
+
+    @Override
+    public boolean putString(String name, String value, String tag, boolean makeDefault) {
+        return Settings.Global.putString(mContentResolver, name, value, tag, makeDefault);
+    }
+}
diff --git a/services/flags/java/com/android/server/flags/SettingsProxy.java b/services/flags/java/com/android/server/flags/SettingsProxy.java
new file mode 100644
index 0000000..c6e85d5
--- /dev/null
+++ b/services/flags/java/com/android/server/flags/SettingsProxy.java
@@ -0,0 +1,381 @@
+/*
+ * 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.flags;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.provider.Settings;
+
+/**
+ * Wrapper class meant to enable hermetic testing of {@link Settings}.
+ *
+ * Implementations of this class are expected to be constructed with a {@link ContentResolver} or,
+ * otherwise have access to an implicit one. All the proxy methods in this class exclude
+ * {@link ContentResolver} from their signature and rely on an internally defined one instead.
+ *
+ * Most methods in the {@link Settings} classes have default implementations defined.
+ * Implementations of this interfac need only concern themselves with getting and putting Strings.
+ * They should also override any methods for a class they are proxying that _are not_ defined, and
+ * throw an appropriate {@link UnsupportedOperationException}. For instance, {@link Settings.Global}
+ * does not define {@link #putString(String, String, boolean)}, so an implementation of this
+ * interface that proxies through to it should throw an exception when that method is called.
+ *
+ * This class adds in the following helpers as well:
+ *  - {@link #getBool(String)}
+ *  - {@link #putBool(String, boolean)}
+ *  - {@link #registerContentObserver(Uri, ContentObserver)}
+ *
+ * ... and similar variations for all of those.
+ */
+public interface SettingsProxy {
+
+    /**
+     * Returns the {@link ContentResolver} this instance uses.
+     */
+    ContentResolver getContentResolver();
+
+    /**
+     * Construct the content URI for a particular name/value pair,
+     * useful for monitoring changes with a ContentObserver.
+     * @param name to look up in the table
+     * @return the corresponding content URI, or null if not present
+     */
+    Uri getUriFor(String name);
+
+    /**See {@link Settings.Secure#getString(ContentResolver, String)} */
+    String getStringForUser(String name, int userHandle);
+
+    /**See {@link Settings.Secure#putString(ContentResolver, String, String, boolean)} */
+    boolean putString(String name, String value, boolean overrideableByRestore);
+
+    /** See {@link Settings.Secure#putStringForUser(ContentResolver, String, String, int)} */
+    boolean putStringForUser(String name, String value, int userHandle);
+
+    /**
+     * See {@link Settings.Secure#putStringForUser(ContentResolver, String, String, String, boolean,
+     * int, boolean)}
+     */
+    boolean putStringForUser(@NonNull String name, @Nullable String value, @Nullable String tag,
+            boolean makeDefault, @UserIdInt int userHandle, boolean overrideableByRestore);
+
+    /** See {@link Settings.Secure#putString(ContentResolver, String, String, String, boolean)} */
+    boolean putString(@NonNull String name, @Nullable String value, @Nullable String tag,
+            boolean makeDefault);
+
+    /**
+     * Returns the user id for the associated {@link ContentResolver}.
+     */
+    default int getUserId() {
+        return getContentResolver().getUserId();
+    }
+
+    /** See {@link Settings.Secure#getString(ContentResolver, String)} */
+    default String getString(String name) {
+        return getStringForUser(name, getUserId());
+    }
+
+    /** See {@link Settings.Secure#putString(ContentResolver, String, String)} */
+    default boolean putString(String name, String value) {
+        return putStringForUser(name, value, getUserId());
+    }
+    /** See {@link Settings.Secure#getIntForUser(ContentResolver, String, int, int)} */
+    default int getIntForUser(String name, int def, int userHandle) {
+        String v = getStringForUser(name, userHandle);
+        try {
+            return v != null ? Integer.parseInt(v) : def;
+        } catch (NumberFormatException e) {
+            return def;
+        }
+    }
+
+    /** See {@link Settings.Secure#getInt(ContentResolver, String)}  */
+    default int getInt(String name) throws Settings.SettingNotFoundException {
+        return getIntForUser(name, getUserId());
+    }
+
+    /** See {@link Settings.Secure#getIntForUser(ContentResolver, String, int)} */
+    default int getIntForUser(String name, int userHandle)
+            throws Settings.SettingNotFoundException {
+        String v = getStringForUser(name, userHandle);
+        try {
+            return Integer.parseInt(v);
+        } catch (NumberFormatException e) {
+            throw new Settings.SettingNotFoundException(name);
+        }
+    }
+
+    /** See {@link Settings.Secure#putInt(ContentResolver, String, int)} */
+    default boolean putInt(String name, int value) {
+        return putIntForUser(name, value, getUserId());
+    }
+
+    /** See {@link Settings.Secure#putIntForUser(ContentResolver, String, int, int)} */
+    default boolean putIntForUser(String name, int value, int userHandle) {
+        return putStringForUser(name, Integer.toString(value), userHandle);
+    }
+
+    /**
+     * Convenience function for retrieving a single settings value
+     * as a boolean.  Note that internally setting values are always
+     * stored as strings; this function converts the string to a boolean
+     * for you. The default value will be returned if the setting is
+     * not defined or not a boolean.
+     *
+     * @param name The name of the setting to retrieve.
+     * @param def Value to return if the setting is not defined.
+     *
+     * @return The setting's current value, or 'def' if it is not defined
+     * or not a valid boolean.
+     */
+    default boolean getBool(String name, boolean def) {
+        return getBoolForUser(name, def, getUserId());
+    }
+
+    /** See {@link #getBool(String, boolean)}. */
+    default boolean getBoolForUser(String name, boolean def, int userHandle) {
+        return getIntForUser(name, def ? 1 : 0, userHandle) != 0;
+    }
+
+    /**
+     * Convenience function for retrieving a single settings value
+     * as a boolean.  Note that internally setting values are always
+     * stored as strings; this function converts the string to a boolean
+     * for you.
+     * <p>
+     * This version does not take a default value.  If the setting has not
+     * been set, or the string value is not a number,
+     * it throws {@link Settings.SettingNotFoundException}.
+     *
+     * @param name The name of the setting to retrieve.
+     *
+     * @throws Settings.SettingNotFoundException Thrown if a setting by the given
+     * name can't be found or the setting value is not a boolean.
+     *
+     * @return The setting's current value.
+     */
+    default boolean getBool(String name) throws Settings.SettingNotFoundException {
+        return getBoolForUser(name, getUserId());
+    }
+
+    /** See {@link #getBool(String)}. */
+    default boolean getBoolForUser(String name, int userHandle)
+            throws Settings.SettingNotFoundException {
+        return getIntForUser(name, userHandle) != 0;
+    }
+
+    /**
+     * Convenience function for updating a single settings value as a
+     * boolean. This will either create a new entry in the table if the
+     * given name does not exist, or modify the value of the existing row
+     * with that name.  Note that internally setting values are always
+     * stored as strings, so this function converts the given value to a
+     * string before storing it.
+     *
+     * @param name The name of the setting to modify.
+     * @param value The new value for the setting.
+     * @return true if the value was set, false on database errors
+     */
+    default boolean putBool(String name, boolean value) {
+        return putBoolForUser(name, value, getUserId());
+    }
+
+    /** See {@link #putBool(String, boolean)}. */
+    default boolean putBoolForUser(String name, boolean value, int userHandle) {
+        return putIntForUser(name, value ? 1 : 0, userHandle);
+    }
+
+    /** See {@link Settings.Secure#getLong(ContentResolver, String, long)}  */
+    default long getLong(String name, long def) {
+        return getLongForUser(name, def, getUserId());
+    }
+
+    /** See {@link Settings.Secure#getLongForUser(ContentResolver, String, long, int)}  */
+    default long getLongForUser(String name, long def, int userHandle) {
+        String valString = getStringForUser(name, userHandle);
+        long value;
+        try {
+            value = valString != null ? Long.parseLong(valString) : def;
+        } catch (NumberFormatException e) {
+            value = def;
+        }
+        return value;
+    }
+
+    /** See {@link Settings.Secure#getLong(ContentResolver, String)}  */
+    default long getLong(String name) throws Settings.SettingNotFoundException {
+        return getLongForUser(name, getUserId());
+    }
+
+    /** See {@link Settings.Secure#getLongForUser(ContentResolver, String, int)}  */
+    default long getLongForUser(String name, int userHandle)
+            throws Settings.SettingNotFoundException {
+        String valString = getStringForUser(name, userHandle);
+        try {
+            return Long.parseLong(valString);
+        } catch (NumberFormatException e) {
+            throw new Settings.SettingNotFoundException(name);
+        }
+    }
+
+    /** See {@link Settings.Secure#putLong(ContentResolver, String, long)} */
+    default boolean putLong(String name, long value) {
+        return putLongForUser(name, value, getUserId());
+    }
+
+    /** See {@link Settings.Secure#putLongForUser(ContentResolver, String, long, int)}  */
+    default boolean putLongForUser(String name, long value, int userHandle) {
+        return putStringForUser(name, Long.toString(value), userHandle);
+    }
+
+    /** See {@link Settings.Secure#getFloat(ContentResolver, String, float)} */
+    default float getFloat(String name, float def) {
+        return getFloatForUser(name, def, getUserId());
+    }
+
+    /** See {@link Settings.Secure#getFloatForUser(ContentResolver, String, int)} */
+    default float getFloatForUser(String name, float def, int userHandle) {
+        String v = getStringForUser(name, userHandle);
+        try {
+            return v != null ? Float.parseFloat(v) : def;
+        } catch (NumberFormatException e) {
+            return def;
+        }
+    }
+
+
+    /** See {@link Settings.Secure#getFloat(ContentResolver, String)}  */
+    default float getFloat(String name) throws Settings.SettingNotFoundException {
+        return getFloatForUser(name, getUserId());
+    }
+
+    /** See {@link Settings.Secure#getFloatForUser(ContentResolver, String, int)}   */
+    default float getFloatForUser(String name, int userHandle)
+            throws Settings.SettingNotFoundException {
+        String v = getStringForUser(name, userHandle);
+        if (v == null) {
+            throw new Settings.SettingNotFoundException(name);
+        }
+        try {
+            return Float.parseFloat(v);
+        } catch (NumberFormatException e) {
+            throw new Settings.SettingNotFoundException(name);
+        }
+    }
+
+    /** See {@link Settings.Secure#putFloat(ContentResolver, String, float)} */
+    default boolean putFloat(String name, float value) {
+        return putFloatForUser(name, value, getUserId());
+    }
+
+    /** See {@link Settings.Secure#putFloatForUser(ContentResolver, String, float, int)} */
+    default boolean putFloatForUser(String name, float value, int userHandle) {
+        return putStringForUser(name, Float.toString(value), userHandle);
+    }
+
+    /**
+     * Convenience wrapper around
+     * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.'
+     *
+     * Implicitly calls {@link #getUriFor(String)} on the passed in name.
+     */
+    default void registerContentObserver(String name, ContentObserver settingsObserver) {
+        registerContentObserver(getUriFor(name), settingsObserver);
+    }
+
+    /**
+     * Convenience wrapper around
+     * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.'
+     */
+    default void registerContentObserver(Uri uri, ContentObserver settingsObserver) {
+        registerContentObserverForUser(uri, settingsObserver, getUserId());
+    }
+
+    /**
+     * Convenience wrapper around
+     * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.
+     *
+     * Implicitly calls {@link #getUriFor(String)} on the passed in name.
+     */
+    default void registerContentObserver(String name, boolean notifyForDescendants,
+            ContentObserver settingsObserver) {
+        registerContentObserver(getUriFor(name), notifyForDescendants, settingsObserver);
+    }
+
+    /**
+     * Convenience wrapper around
+     * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.'
+     */
+    default void registerContentObserver(Uri uri, boolean notifyForDescendants,
+            ContentObserver settingsObserver) {
+        registerContentObserverForUser(uri, notifyForDescendants, settingsObserver, getUserId());
+    }
+
+    /**
+     * Convenience wrapper around
+     * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)}
+     *
+     * Implicitly calls {@link #getUriFor(String)} on the passed in name.
+     */
+    default void registerContentObserverForUser(
+            String name, ContentObserver settingsObserver, int userHandle) {
+        registerContentObserverForUser(
+                getUriFor(name), settingsObserver, userHandle);
+    }
+
+    /**
+     * Convenience wrapper around
+     * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)}
+     */
+    default void registerContentObserverForUser(
+            Uri uri, ContentObserver settingsObserver, int userHandle) {
+        registerContentObserverForUser(
+                uri, false, settingsObserver, userHandle);
+    }
+
+    /**
+     * Convenience wrapper around
+     * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)}
+     *
+     * Implicitly calls {@link #getUriFor(String)} on the passed in name.
+     */
+    default void registerContentObserverForUser(
+            String name, boolean notifyForDescendants, ContentObserver settingsObserver,
+            int userHandle) {
+        registerContentObserverForUser(
+                getUriFor(name), notifyForDescendants, settingsObserver, userHandle);
+    }
+
+    /**
+     * Convenience wrapper around
+     * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)}
+     */
+    default void registerContentObserverForUser(
+            Uri uri, boolean notifyForDescendants, ContentObserver settingsObserver,
+            int userHandle) {
+        getContentResolver().registerContentObserver(
+                uri, notifyForDescendants, settingsObserver, userHandle);
+    }
+
+    /** See {@link ContentResolver#unregisterContentObserver(ContentObserver)}. */
+    default void unregisterContentObserver(ContentObserver settingsObserver) {
+        getContentResolver().unregisterContentObserver(settingsObserver);
+    }
+}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 39d33f0..ee4bc12 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -133,6 +133,7 @@
 import com.android.server.display.color.ColorDisplayService;
 import com.android.server.dreams.DreamManagerService;
 import com.android.server.emergency.EmergencyAffordanceService;
+import com.android.server.flags.FeatureFlagsService;
 import com.android.server.gpu.GpuService;
 import com.android.server.grammaticalinflection.GrammaticalInflectionService;
 import com.android.server.graphics.fonts.FontManagerService;
@@ -1117,6 +1118,12 @@
         mSystemServiceManager.startService(DeviceIdentifiersPolicyService.class);
         t.traceEnd();
 
+        // Starts a service for reading runtime flag overrides, and keeping processes
+        // in sync with one another.
+        t.traceBegin("StartFeatureFlagsService");
+        mSystemServiceManager.startService(FeatureFlagsService.class);
+        t.traceEnd();
+
         // Uri Grants Manager.
         t.traceBegin("UriGrantsManagerService");
         mSystemServiceManager.startService(UriGrantsManagerService.Lifecycle.class);
diff --git a/services/permission/OWNERS b/services/permission/OWNERS
index 6c6c9fc..e464038 100644
--- a/services/permission/OWNERS
+++ b/services/permission/OWNERS
@@ -1,4 +1,5 @@
-ashfall@google.com
+#Bug component: 137825
+
 joecastro@google.com
 ntmyren@google.com
 zhanghai@google.com
diff --git a/services/tests/mockingservicestests/assets/AppOpsUpgradeTest/OWNERS b/services/tests/mockingservicestests/assets/AppOpsUpgradeTest/OWNERS
index 999ea0e..2fe78c5 100644
--- a/services/tests/mockingservicestests/assets/AppOpsUpgradeTest/OWNERS
+++ b/services/tests/mockingservicestests/assets/AppOpsUpgradeTest/OWNERS
@@ -1 +1,2 @@
+#Bug component: 137825
 include /core/java/android/permission/OWNERS
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index 73eb237..410ae35 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -125,6 +125,7 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.UnaryOperator;
@@ -327,7 +328,7 @@
                 eq(ActivityManager.PROCESS_STATE_LAST_ACTIVITY), any());
 
         mConstants = new BroadcastConstants(Settings.Global.BROADCAST_FG_CONSTANTS);
-        mConstants.TIMEOUT = 100;
+        mConstants.TIMEOUT = 200;
         mConstants.ALLOW_BG_ACTIVITY_START_TIMEOUT = 0;
         mConstants.PENDING_COLD_START_CHECK_INTERVAL_MILLIS = 500;
 
@@ -707,6 +708,9 @@
     private void waitForIdle() throws Exception {
         mLooper.release();
         mQueue.waitForIdle(LOG_WRITER_INFO);
+        final CountDownLatch latch = new CountDownLatch(1);
+        mHandlerThread.getThreadHandler().post(latch::countDown);
+        latch.await();
         mLooper = Objects.requireNonNull(InstrumentationRegistry.getInstrumentation()
                 .acquireLooperManager(mHandlerThread.getLooper()));
     }
@@ -2342,6 +2346,7 @@
 
         mUidObserver.onUidStateChanged(receiverGreenApp.info.uid,
                 ActivityManager.PROCESS_STATE_TOP, 0, ActivityManager.PROCESS_CAPABILITY_NONE);
+        waitForIdle();
 
         final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
         final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK);
@@ -2375,6 +2380,7 @@
 
         mUidObserver.onUidStateChanged(receiverGreenApp.info.uid,
                 ActivityManager.PROCESS_STATE_TOP, 0, ActivityManager.PROCESS_CAPABILITY_NONE);
+        waitForIdle();
 
         final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK);
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/appop/OWNERS b/services/tests/mockingservicestests/src/com/android/server/appop/OWNERS
index 999ea0e..2fe78c5 100644
--- a/services/tests/mockingservicestests/src/com/android/server/appop/OWNERS
+++ b/services/tests/mockingservicestests/src/com/android/server/appop/OWNERS
@@ -1 +1,2 @@
+#Bug component: 137825
 include /core/java/android/permission/OWNERS
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/ArchiveManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/ArchiveManagerTest.java
index 7b16545..a8b0a7b 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/ArchiveManagerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/ArchiveManagerTest.java
@@ -40,6 +40,7 @@
 import android.os.Process;
 import android.os.UserHandle;
 import android.platform.test.annotations.Presubmit;
+import android.text.TextUtils;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -169,14 +170,14 @@
     }
 
     @Test
-    public void archiveApp_callerNotInstallerOfRecord() {
+    public void archiveApp_noInstallerFound() {
         InstallSource otherInstallSource =
                 InstallSource.create(
                         CALLER_PACKAGE,
                         CALLER_PACKAGE,
-                        /* installerPackageName= */ "different",
+                        /* installerPackageName= */ null,
                         Binder.getCallingUid(),
-                        CALLER_PACKAGE,
+                        /* updateOwnerPackageName= */ null,
                         /* installerAttributionTag= */ null,
                         /* packageSource= */ 0);
         when(mPackageState.getInstallSource()).thenReturn(otherInstallSource);
@@ -187,29 +188,8 @@
                         mIntentSender)
         );
         assertThat(e).hasMessageThat().isEqualTo(
-                String.format("Caller is not the installer of record for %s.", PACKAGE));
-    }
-
-    @Test
-    public void archiveApp_callerNotUpdateOwner() {
-        InstallSource otherInstallSource =
-                InstallSource.create(
-                        CALLER_PACKAGE,
-                        CALLER_PACKAGE,
-                        CALLER_PACKAGE,
-                        Binder.getCallingUid(),
-                        /* updateOwnerPackageName= */ "different",
-                        /* installerAttributionTag= */ null,
-                        /* packageSource= */ 0);
-        when(mPackageState.getInstallSource()).thenReturn(otherInstallSource);
-
-        Exception e = assertThrows(
-                SecurityException.class,
-                () -> mArchiveManager.archiveApp(PACKAGE, CALLER_PACKAGE, UserHandle.CURRENT,
-                        mIntentSender)
-        );
-        assertThat(e).hasMessageThat().isEqualTo(
-                String.format("Caller is not the update owner for %s.", PACKAGE));
+                TextUtils.formatSimple("No installer found to archive app %s.",
+                        PACKAGE));
     }
 
     @Test
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageMonitorCallbackHelperTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/PackageMonitorCallbackHelperTest.java
index 3ac59e9..f206333 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageMonitorCallbackHelperTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageMonitorCallbackHelperTest.java
@@ -33,6 +33,8 @@
 import android.os.Handler;
 import android.os.IRemoteCallback;
 import android.os.Looper;
+import android.os.Process;
+import android.util.SparseArray;
 
 import org.junit.After;
 import org.junit.Before;
@@ -244,6 +246,54 @@
         verify(callback, after(WAIT_CALLBACK_CALLED_IN_MS).never()).sendResult(any());
     }
 
+    @Test
+    public void testRegisterPackageMonitorCallbackInAllowList_callbackShouldCalled()
+            throws Exception {
+        IRemoteCallback callback = createMockPackageMonitorCallback();
+        SparseArray<int[]> broadcastAllowList = new SparseArray<>();
+        broadcastAllowList.put(0, new int[] {Binder.getCallingUid()});
+
+        mPackageMonitorCallbackHelper.registerPackageMonitorCallback(callback, 0 /* userId */,
+                Binder.getCallingUid());
+        mPackageMonitorCallbackHelper.notifyPackageMonitor(Intent.ACTION_PACKAGE_ADDED,
+                FAKE_PACKAGE_NAME, createFakeBundle(), new int[]{0} /* userIds */,
+                null /* instantUserIds */, broadcastAllowList);
+
+        verify(callback, after(WAIT_CALLBACK_CALLED_IN_MS).times(1)).sendResult(any());
+    }
+
+    @Test
+    public void testRegisterPackageMonitorCallbackNotInAllowList_callbackShouldNotCalled()
+            throws Exception {
+        IRemoteCallback callback = createMockPackageMonitorCallback();
+        SparseArray<int[]> broadcastAllowList = new SparseArray<>();
+        broadcastAllowList.put(0, new int[] {12345});
+
+        mPackageMonitorCallbackHelper.registerPackageMonitorCallback(callback, 0 /* userId */,
+                Binder.getCallingUid());
+        mPackageMonitorCallbackHelper.notifyPackageMonitor(Intent.ACTION_PACKAGE_ADDED,
+                FAKE_PACKAGE_NAME, createFakeBundle(), new int[]{0} /* userIds */,
+                null /* instantUserIds */, broadcastAllowList);
+
+        verify(callback, after(WAIT_CALLBACK_CALLED_IN_MS).never()).sendResult(any());
+    }
+
+    @Test
+    public void testRegisterPackageMonitorCallbackNotInAllowListSystemUid_callbackShouldCalled()
+            throws Exception {
+        IRemoteCallback callback = createMockPackageMonitorCallback();
+        SparseArray<int[]> broadcastAllowList = new SparseArray<>();
+        broadcastAllowList.put(0, new int[] {12345});
+
+        mPackageMonitorCallbackHelper.registerPackageMonitorCallback(callback, 0 /* userId */,
+                Process.SYSTEM_UID);
+        mPackageMonitorCallbackHelper.notifyPackageMonitor(Intent.ACTION_PACKAGE_ADDED,
+                FAKE_PACKAGE_NAME, createFakeBundle(), new int[]{0} /* userIds */,
+                null /* instantUserIds */, broadcastAllowList);
+
+        verify(callback, after(WAIT_CALLBACK_CALLED_IN_MS).times(1)).sendResult(any());
+    }
+
     private IRemoteCallback createMockPackageMonitorCallback() {
         return spy(new IRemoteCallback.Stub() {
             @Override
diff --git a/services/tests/mockingservicestests/src/com/android/server/power/PowerManagerServiceMockingTest.java b/services/tests/mockingservicestests/src/com/android/server/power/PowerManagerServiceMockingTest.java
deleted file mode 100644
index 0f2c27c..0000000
--- a/services/tests/mockingservicestests/src/com/android/server/power/PowerManagerServiceMockingTest.java
+++ /dev/null
@@ -1,316 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.power;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.app.ActivityManagerInternal;
-import android.attention.AttentionManagerInternal;
-import android.content.Context;
-import android.content.ContextWrapper;
-import android.content.res.Resources;
-import android.hardware.SensorManager;
-import android.hardware.devicestate.DeviceStateManager;
-import android.hardware.devicestate.DeviceStateManager.DeviceStateCallback;
-import android.hardware.display.AmbientDisplayConfiguration;
-import android.hardware.display.DisplayManagerInternal;
-import android.os.BatteryManagerInternal;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.PowerManager;
-import android.os.PowerSaveState;
-import android.os.test.TestLooper;
-import android.provider.Settings;
-import android.service.dreams.DreamManagerInternal;
-import android.test.mock.MockContentResolver;
-import android.view.Display;
-import android.view.DisplayInfo;
-
-import androidx.test.InstrumentationRegistry;
-
-import com.android.internal.app.IBatteryStats;
-import com.android.internal.util.test.FakeSettingsProvider;
-import com.android.server.LocalServices;
-import com.android.server.SystemService;
-import com.android.server.lights.LightsManager;
-import com.android.server.policy.WindowManagerPolicy;
-import com.android.server.power.PowerManagerService.BatteryReceiver;
-import com.android.server.power.PowerManagerService.Injector;
-import com.android.server.power.PowerManagerService.NativeWrapper;
-import com.android.server.power.PowerManagerService.UserSwitchedReceiver;
-import com.android.server.power.batterysaver.BatterySaverController;
-import com.android.server.power.batterysaver.BatterySaverPolicy;
-import com.android.server.power.batterysaver.BatterySaverStateMachine;
-import com.android.server.power.batterysaver.BatterySavingStats;
-import com.android.server.testutils.OffsettableClock;
-
-import java.util.concurrent.Executor;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-/**
- * Tests for {@link com.android.server.power.PowerManagerService}.
- *
- * Build/Install/Run:
- *  atest FrameworksServicesTests:PowerManagerServiceMockingTest
- */
-public class PowerManagerServiceMockingTest {
-    private static final String SYSTEM_PROPERTY_QUIESCENT = "ro.boot.quiescent";
-    private static final String SYSTEM_PROPERTY_REBOOT_REASON = "sys.boot.reason";
-
-    private static final float BRIGHTNESS_FACTOR = 0.7f;
-    private static final boolean BATTERY_SAVER_ENABLED = true;
-
-    @Mock private BatterySaverController mBatterySaverControllerMock;
-    @Mock private BatterySaverPolicy mBatterySaverPolicyMock;
-    @Mock private BatterySaverStateMachine mBatterySaverStateMachineMock;
-    @Mock private LightsManager mLightsManagerMock;
-    @Mock private DisplayManagerInternal mDisplayManagerInternalMock;
-    @Mock private BatteryManagerInternal mBatteryManagerInternalMock;
-    @Mock private ActivityManagerInternal mActivityManagerInternalMock;
-    @Mock private AttentionManagerInternal mAttentionManagerInternalMock;
-    @Mock private DreamManagerInternal mDreamManagerInternalMock;
-    @Mock private PowerManagerService.NativeWrapper mNativeWrapperMock;
-    @Mock private Notifier mNotifierMock;
-    @Mock private WirelessChargerDetector mWirelessChargerDetectorMock;
-    @Mock private AmbientDisplayConfiguration mAmbientDisplayConfigurationMock;
-    @Mock private SystemPropertiesWrapper mSystemPropertiesMock;
-    @Mock private DeviceStateManager mDeviceStateManagerMock;
-
-    @Mock
-    private InattentiveSleepWarningController mInattentiveSleepWarningControllerMock;
-
-    private PowerManagerService mService;
-    private PowerSaveState mPowerSaveState;
-    private ContextWrapper mContextSpy;
-    private BatteryReceiver mBatteryReceiver;
-    private UserSwitchedReceiver mUserSwitchedReceiver;
-    private Resources mResourcesSpy;
-    private OffsettableClock mClock;
-    private TestLooper mTestLooper;
-
-    @Before
-    public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
-        FakeSettingsProvider.clearSettingsProvider();
-
-        mPowerSaveState = new PowerSaveState.Builder()
-                .setBatterySaverEnabled(BATTERY_SAVER_ENABLED)
-                .setBrightnessFactor(BRIGHTNESS_FACTOR)
-                .build();
-        when(mBatterySaverPolicyMock.getBatterySaverPolicy(
-                eq(PowerManager.ServiceType.SCREEN_BRIGHTNESS)))
-                .thenReturn(mPowerSaveState);
-        when(mBatteryManagerInternalMock.isPowered(anyInt())).thenReturn(false);
-        when(mInattentiveSleepWarningControllerMock.isShown()).thenReturn(false);
-        when(mDisplayManagerInternalMock.requestPowerState(anyInt(), any(), anyBoolean()))
-                .thenReturn(true);
-        when(mSystemPropertiesMock.get(eq(SYSTEM_PROPERTY_QUIESCENT), anyString())).thenReturn("");
-        when(mAmbientDisplayConfigurationMock.ambientDisplayAvailable()).thenReturn(true);
-
-        addLocalServiceMock(LightsManager.class, mLightsManagerMock);
-        addLocalServiceMock(DisplayManagerInternal.class, mDisplayManagerInternalMock);
-        addLocalServiceMock(BatteryManagerInternal.class, mBatteryManagerInternalMock);
-        addLocalServiceMock(ActivityManagerInternal.class, mActivityManagerInternalMock);
-        addLocalServiceMock(AttentionManagerInternal.class, mAttentionManagerInternalMock);
-        addLocalServiceMock(DreamManagerInternal.class, mDreamManagerInternalMock);
-
-        mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getContext()));
-        mResourcesSpy = spy(mContextSpy.getResources());
-        when(mContextSpy.getResources()).thenReturn(mResourcesSpy);
-
-        MockContentResolver cr = new MockContentResolver(mContextSpy);
-        cr.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
-        when(mContextSpy.getContentResolver()).thenReturn(cr);
-
-        when(mContextSpy.getSystemService(DeviceStateManager.class))
-                .thenReturn(mDeviceStateManagerMock);
-
-        Settings.Global.putInt(mContextSpy.getContentResolver(),
-                Settings.Global.STAY_ON_WHILE_PLUGGED_IN, 0);
-
-        mClock = new OffsettableClock.Stopped();
-        mTestLooper = new TestLooper(mClock::now);
-    }
-
-    private PowerManagerService createService() {
-        mService = new PowerManagerService(mContextSpy, new Injector() {
-            @Override
-            Notifier createNotifier(Looper looper, Context context, IBatteryStats batteryStats,
-                    SuspendBlocker suspendBlocker, WindowManagerPolicy policy,
-                    FaceDownDetector faceDownDetector, ScreenUndimDetector screenUndimDetector,
-                    Executor executor) {
-                return mNotifierMock;
-            }
-
-            @Override
-            SuspendBlocker createSuspendBlocker(PowerManagerService service, String name) {
-                return super.createSuspendBlocker(service, name);
-            }
-
-            @Override
-            BatterySaverPolicy createBatterySaverPolicy(
-                    Object lock, Context context, BatterySavingStats batterySavingStats) {
-                return mBatterySaverPolicyMock;
-            }
-
-            @Override
-            BatterySaverController createBatterySaverController(
-                    Object lock, Context context, BatterySaverPolicy batterySaverPolicy,
-                    BatterySavingStats batterySavingStats) {
-                return mBatterySaverControllerMock;
-            }
-
-            @Override
-            BatterySaverStateMachine createBatterySaverStateMachine(Object lock, Context context,
-                    BatterySaverController batterySaverController) {
-                return mBatterySaverStateMachineMock;
-            }
-
-            @Override
-            NativeWrapper createNativeWrapper() {
-                return mNativeWrapperMock;
-            }
-
-            @Override
-            WirelessChargerDetector createWirelessChargerDetector(
-                    SensorManager sensorManager, SuspendBlocker suspendBlocker, Handler handler) {
-                return mWirelessChargerDetectorMock;
-            }
-
-            @Override
-            AmbientDisplayConfiguration createAmbientDisplayConfiguration(Context context) {
-                return mAmbientDisplayConfigurationMock;
-            }
-
-            @Override
-            InattentiveSleepWarningController createInattentiveSleepWarningController() {
-                return mInattentiveSleepWarningControllerMock;
-            }
-
-            @Override
-            public SystemPropertiesWrapper createSystemPropertiesWrapper() {
-                return mSystemPropertiesMock;
-            }
-
-            @Override
-            PowerManagerService.Clock createClock() {
-                return new PowerManagerService.Clock() {
-                    @Override
-                    public long uptimeMillis() {
-                        return mClock.now();
-                    }
-
-                    @Override
-                    public long elapsedRealtime() {
-                        return mClock.now();
-                    }
-                };
-            }
-
-            @Override
-            Handler createHandler(Looper looper, Handler.Callback callback) {
-                return new Handler(mTestLooper.getLooper(), callback);
-            }
-
-            @Override
-            void invalidateIsInteractiveCaches() {
-                // Avoids an SELinux failure.
-            }
-        });
-        return mService;
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        LocalServices.removeServiceForTest(LightsManager.class);
-        LocalServices.removeServiceForTest(DisplayManagerInternal.class);
-        LocalServices.removeServiceForTest(BatteryManagerInternal.class);
-        LocalServices.removeServiceForTest(ActivityManagerInternal.class);
-        LocalServices.removeServiceForTest(AttentionManagerInternal.class);
-        LocalServices.removeServiceForTest(DreamManagerInternal.class);
-        FakeSettingsProvider.clearSettingsProvider();
-    }
-
-    /**
-     * Creates a mock and registers it to {@link LocalServices}.
-     */
-    private static <T> void addLocalServiceMock(Class<T> clazz, T mock) {
-        LocalServices.removeServiceForTest(clazz);
-        LocalServices.addService(clazz, mock);
-    }
-
-    private void advanceTime(long timeMs) {
-        mClock.fastForward(timeMs);
-        mTestLooper.dispatchAll();
-    }
-
-    @Test
-    public void testUserActivityOnDeviceStateChange() {
-        createService();
-        mService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY);
-        mService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
-
-        final DisplayInfo info = new DisplayInfo();
-        info.displayGroupId = Display.DEFAULT_DISPLAY_GROUP;
-        when(mDisplayManagerInternalMock.getDisplayInfo(Display.DEFAULT_DISPLAY)).thenReturn(info);
-
-        final ArgumentCaptor<DeviceStateCallback> deviceStateCallbackCaptor =
-                ArgumentCaptor.forClass(DeviceStateCallback.class);
-        verify(mDeviceStateManagerMock).registerCallback(any(),
-                deviceStateCallbackCaptor.capture());
-
-        // Advance the time 10001 and verify that the device thinks it has been idle
-        // for just less than that.
-        mService.onUserActivity();
-        advanceTime(10001);
-        assertThat(mService.wasDeviceIdleForInternal(10000)).isTrue();
-
-        // Send a display state change event and advance the clock 10.
-        final DeviceStateCallback deviceStateCallback = deviceStateCallbackCaptor.getValue();
-        deviceStateCallback.onStateChanged(1);
-        final long timeToAdvance = 10;
-        advanceTime(timeToAdvance);
-
-        // Ensure that the device has been idle for only 10 (doesn't include the idle time
-        // before the display state event).
-        assertThat(mService.wasDeviceIdleForInternal(timeToAdvance - 1)).isTrue();
-        assertThat(mService.wasDeviceIdleForInternal(timeToAdvance)).isFalse();
-
-        // Send the same state and ensure that does not trigger an update.
-        deviceStateCallback.onStateChanged(1);
-        advanceTime(timeToAdvance);
-        final long newTime = timeToAdvance * 2;
-
-        assertThat(mService.wasDeviceIdleForInternal(newTime - 1)).isTrue();
-        assertThat(mService.wasDeviceIdleForInternal(newTime)).isFalse();
-    }
-}
diff --git a/services/tests/powerservicetests/Android.bp b/services/tests/powerservicetests/Android.bp
index 236f90c..9384015 100644
--- a/services/tests/powerservicetests/Android.bp
+++ b/services/tests/powerservicetests/Android.bp
@@ -12,9 +12,19 @@
 
     static_libs: [
         "frameworks-base-testutils",
+        "platform-compat-test-rules",
         "platform-test-annotations",
         "services.core",
         "servicestests-utils",
+        "testables",
+    ],
+
+    libs: [
+        "android.test.mock",
+    ],
+
+    defaults: [
+        "modules-utils-testable-device-config-defaults",
     ],
 
     platform_apis: true,
diff --git a/services/tests/powerservicetests/AndroidManifest.xml b/services/tests/powerservicetests/AndroidManifest.xml
index 3ace9d5..26d9eec 100644
--- a/services/tests/powerservicetests/AndroidManifest.xml
+++ b/services/tests/powerservicetests/AndroidManifest.xml
@@ -17,10 +17,19 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.frameworks.powerservicetests">
 
-    <!--
-    Insert permissions here. eg:
-    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
-    -->
+    <!-- Permissions -->
+    <uses-permission android:name="android.permission.DEVICE_POWER"/>
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL"/>
+    <uses-permission android:name="android.permission.READ_DEVICE_CONFIG"/>
+    <uses-permission android:name="android.permission.READ_DREAM_STATE"/>
+    <uses-permission android:name="android.permission.READ_DREAM_SUPPRESSION"/>
+    <uses-permission android:name="android.permission.STATUS_BAR_SERVICE"/>
+    <uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS"/>
+    <uses-permission android:name="android.permission.UPDATE_DEVICE_STATS"/>
+    <uses-permission android:name="android.permission.WAKE_LOCK"/>
+    <uses-permission android:name="android.permission.WRITE_DREAM_STATE"/>
+
     <application android:debuggable="true"
                  android:testOnly="true">
         <uses-library android:name="android.test.mock" android:required="true" />
diff --git a/services/tests/powerservicetests/OWNERS b/services/tests/powerservicetests/OWNERS
index e740577..aff0930 100644
--- a/services/tests/powerservicetests/OWNERS
+++ b/services/tests/powerservicetests/OWNERS
@@ -2,4 +2,3 @@
 
 include /services/core/java/com/android/server/power/OWNERS
 
-per-file ThermalManagerServiceTest.java=wvw@google.com, xwxw@google.com
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/power/LowPowerStandbyControllerTest.java b/services/tests/powerservicetests/src/com/android/server/power/LowPowerStandbyControllerTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/power/LowPowerStandbyControllerTest.java
rename to services/tests/powerservicetests/src/com/android/server/power/LowPowerStandbyControllerTest.java
diff --git a/services/tests/servicestests/src/com/android/server/power/NotifierTest.java b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/power/NotifierTest.java
rename to services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
diff --git a/services/tests/servicestests/src/com/android/server/power/PowerGroupTest.java b/services/tests/powerservicetests/src/com/android/server/power/PowerGroupTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/power/PowerGroupTest.java
rename to services/tests/powerservicetests/src/com/android/server/power/PowerGroupTest.java
diff --git a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
similarity index 97%
rename from services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
rename to services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
index c80ff45..9463b2d 100644
--- a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
+++ b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
@@ -60,6 +60,8 @@
 import android.content.PermissionChecker;
 import android.content.res.Resources;
 import android.hardware.SensorManager;
+import android.hardware.devicestate.DeviceStateManager;
+import android.hardware.devicestate.DeviceStateManager.DeviceStateCallback;
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.hardware.display.DisplayManagerInternal;
 import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
@@ -160,6 +162,7 @@
     @Mock private InattentiveSleepWarningController mInattentiveSleepWarningControllerMock;
     @Mock private PowerManagerService.PermissionCheckerWrapper mPermissionCheckerWrapperMock;
     @Mock private PowerManagerService.PowerPropertiesWrapper mPowerPropertiesWrapper;
+    @Mock private DeviceStateManager mDeviceStateManagerMock;
 
     @Rule public TestRule compatChangeRule = new PlatformCompatChangeRule();
 
@@ -2710,4 +2713,48 @@
         verify(mNotifierMock, never()).onUserActivity(anyInt(),  anyInt(), anyInt());
     }
 
+    @Test
+    public void testUserActivityOnDeviceStateChange() {
+        when(mContextSpy.getSystemService(DeviceStateManager.class))
+                .thenReturn(mDeviceStateManagerMock);
+
+        createService();
+        mService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY);
+        mService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
+
+        final DisplayInfo info = new DisplayInfo();
+        info.displayGroupId = Display.DEFAULT_DISPLAY_GROUP;
+        when(mDisplayManagerInternalMock.getDisplayInfo(Display.DEFAULT_DISPLAY)).thenReturn(info);
+
+        final ArgumentCaptor<DeviceStateCallback> deviceStateCallbackCaptor =
+                ArgumentCaptor.forClass(DeviceStateCallback.class);
+        verify(mDeviceStateManagerMock).registerCallback(any(),
+                deviceStateCallbackCaptor.capture());
+
+        // Advance the time 10001 and verify that the device thinks it has been idle
+        // for just less than that.
+        mService.onUserActivity();
+        advanceTime(10001);
+        assertThat(mService.wasDeviceIdleForInternal(10000)).isTrue();
+
+        // Send a display state change event and advance the clock 10.
+        final DeviceStateCallback deviceStateCallback = deviceStateCallbackCaptor.getValue();
+        deviceStateCallback.onStateChanged(1);
+        final long timeToAdvance = 10;
+        advanceTime(timeToAdvance);
+
+        // Ensure that the device has been idle for only 10 (doesn't include the idle time
+        // before the display state event).
+        assertThat(mService.wasDeviceIdleForInternal(timeToAdvance - 1)).isTrue();
+        assertThat(mService.wasDeviceIdleForInternal(timeToAdvance)).isFalse();
+
+        // Send the same state and ensure that does not trigger an update.
+        deviceStateCallback.onStateChanged(1);
+        advanceTime(timeToAdvance);
+        final long newTime = timeToAdvance * 2;
+
+        assertThat(mService.wasDeviceIdleForInternal(newTime - 1)).isTrue();
+        assertThat(mService.wasDeviceIdleForInternal(newTime)).isFalse();
+    }
+
 }
diff --git a/services/tests/servicestests/src/com/android/server/power/ShutdownCheckPointsTest.java b/services/tests/powerservicetests/src/com/android/server/power/ShutdownCheckPointsTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/power/ShutdownCheckPointsTest.java
rename to services/tests/powerservicetests/src/com/android/server/power/ShutdownCheckPointsTest.java
diff --git a/services/tests/servicestests/src/com/android/server/power/ShutdownThreadTest.java b/services/tests/powerservicetests/src/com/android/server/power/ShutdownThreadTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/power/ShutdownThreadTest.java
rename to services/tests/powerservicetests/src/com/android/server/power/ShutdownThreadTest.java
diff --git a/services/tests/servicestests/src/com/android/server/power/WakeLockLogTest.java b/services/tests/powerservicetests/src/com/android/server/power/WakeLockLogTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/power/WakeLockLogTest.java
rename to services/tests/powerservicetests/src/com/android/server/power/WakeLockLogTest.java
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 4edb167..173c5f5 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -34,6 +34,7 @@
         "services.core",
         "services.credentials",
         "services.devicepolicy",
+        "services.flags",
         "services.net",
         "services.people",
         "services.usage",
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
index 2d4bf14..32d0c98 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
@@ -44,8 +44,10 @@
 import android.graphics.PointF;
 import android.os.Handler;
 import android.os.Message;
+import android.os.UserHandle;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
+import android.provider.Settings;
 import android.testing.TestableContext;
 import android.util.DebugUtils;
 import android.view.InputDevice;
@@ -140,8 +142,6 @@
     @Mock
     WindowMagnificationPromptController mWindowMagnificationPromptController;
     @Mock
-    AccessibilityManagerService mMockAccessibilityManagerService;
-    @Mock
     AccessibilityTraceManager mMockTraceManager;
 
     @Rule
@@ -153,6 +153,8 @@
 
     private long mLastDownTime = Integer.MIN_VALUE;
 
+    private float mOriginalMagnificationPersistedScale;
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -166,6 +168,13 @@
         when(mockController.newValueAnimator()).thenReturn(new ValueAnimator());
         when(mockController.getAnimationDuration()).thenReturn(1000L);
         when(mockWindowManager.setMagnificationCallbacks(eq(DISPLAY_0), any())).thenReturn(true);
+        mOriginalMagnificationPersistedScale = Settings.Secure.getFloatForUser(
+                mContext.getContentResolver(),
+                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, 2.0f,
+                UserHandle.USER_SYSTEM);
+        Settings.Secure.putFloatForUser(mContext.getContentResolver(),
+                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, 2.0f,
+                UserHandle.USER_SYSTEM);
         mFullScreenMagnificationController = new FullScreenMagnificationController(
                 mockController,
                 new Object(),
@@ -192,6 +201,10 @@
         mMgh.onDestroy();
         mFullScreenMagnificationController.unregister(DISPLAY_0);
         verify(mWindowMagnificationPromptController).onDestroy();
+        Settings.Secure.putFloatForUser(mContext.getContentResolver(),
+                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE,
+                mOriginalMagnificationPersistedScale,
+                UserHandle.USER_SYSTEM);
     }
 
     @NonNull
@@ -525,10 +538,9 @@
         final float threshold = FullScreenMagnificationGestureHandler.PanningScalingState
                 .CHECK_DETECTING_PASS_PERSISTED_SCALE_THRESHOLD;
         final float persistedScale = (1.0f + threshold) * scale + 1.0f;
-        mFullScreenMagnificationController.setScale(DISPLAY_0, persistedScale, DEFAULT_X,
-                DEFAULT_Y, /* animate= */ false,
-                AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
-        mFullScreenMagnificationController.persistScale(DISPLAY_0);
+        Settings.Secure.putFloatForUser(mContext.getContentResolver(),
+                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, persistedScale,
+                UserHandle.USER_SYSTEM);
         mFullScreenMagnificationController.setScale(DISPLAY_0, scale, DEFAULT_X,
                 DEFAULT_Y, /* animate= */ false,
                 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
@@ -547,10 +559,9 @@
         final float threshold = FullScreenMagnificationGestureHandler.PanningScalingState
                 .CHECK_DETECTING_PASS_PERSISTED_SCALE_THRESHOLD;
         final float persistedScale = (1.0f + threshold) * scale - 0.1f;
-        mFullScreenMagnificationController.setScale(DISPLAY_0, persistedScale, DEFAULT_X,
-                DEFAULT_Y, /* animate= */ false,
-                AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
-        mFullScreenMagnificationController.persistScale(DISPLAY_0);
+        Settings.Secure.putFloatForUser(mContext.getContentResolver(),
+                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, persistedScale,
+                UserHandle.USER_SYSTEM);
         mFullScreenMagnificationController.setScale(DISPLAY_0, scale, DEFAULT_X,
                 DEFAULT_Y, /* animate= */ false,
                 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java
index 27e6ef1..bbbab21 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java
@@ -66,7 +66,6 @@
 import com.android.server.statusbar.StatusBarManagerInternal;
 
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.mockito.Mock;
 import org.mockito.Mockito;
@@ -308,7 +307,6 @@
                 MagnificationScaleProvider.MAX_SCALE);
     }
 
-    @Ignore("b/278816260: We could refer to b/182561174#comment4 for solution.")
     @Test
     public void logTrackingTypingFocus_processScroll_logDuration() {
         WindowMagnificationManager spyWindowMagnificationManager = spy(mWindowMagnificationManager);
diff --git a/services/tests/servicestests/src/com/android/server/appop/OWNERS b/services/tests/servicestests/src/com/android/server/appop/OWNERS
index 999ea0e..2fe78c5 100644
--- a/services/tests/servicestests/src/com/android/server/appop/OWNERS
+++ b/services/tests/servicestests/src/com/android/server/appop/OWNERS
@@ -1 +1,2 @@
+#Bug component: 137825
 include /core/java/android/permission/OWNERS
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
index 6821721..fc62e75 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.biometrics;
 
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
 import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
 import static android.hardware.biometrics.BiometricManager.Authenticators;
 import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS;
@@ -113,6 +114,10 @@
     private static final String ERROR_UNABLE_TO_PROCESS = "error_unable_to_process";
     private static final String ERROR_USER_CANCELED = "error_user_canceled";
     private static final String ERROR_LOCKOUT = "error_lockout";
+    private static final String FACE_SUBTITLE = "face_subtitle";
+    private static final String FINGERPRINT_SUBTITLE = "fingerprint_subtitle";
+    private static final String DEFAULT_SUBTITLE = "default_subtitle";
+
 
     private static final String FINGERPRINT_ACQUIRED_SENSOR_DIRTY = "sensor_dirty";
 
@@ -191,6 +196,12 @@
                 .thenReturn(ERROR_NOT_RECOGNIZED);
         when(mResources.getString(R.string.biometric_error_user_canceled))
                 .thenReturn(ERROR_USER_CANCELED);
+        when(mContext.getString(R.string.biometric_dialog_face_subtitle))
+                .thenReturn(FACE_SUBTITLE);
+        when(mContext.getString(R.string.biometric_dialog_fingerprint_subtitle))
+                .thenReturn(FINGERPRINT_SUBTITLE);
+        when(mContext.getString(R.string.biometric_dialog_default_subtitle))
+                .thenReturn(DEFAULT_SUBTITLE);
 
         when(mWindowManager.getDefaultDisplay()).thenReturn(
                 new Display(DisplayManagerGlobal.getInstance(), Display.DEFAULT_DISPLAY,
@@ -211,7 +222,7 @@
 
     @Test
     public void testClientBinderDied_whenPaused() throws Exception {
-        setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
+        setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
 
         invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
                 true /* requireConfirmation */, null /* authenticators */);
@@ -238,7 +249,7 @@
 
     @Test
     public void testClientBinderDied_whenAuthenticating() throws Exception {
-        setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
+        setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
 
         invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
                 true /* requireConfirmation */, null /* authenticators */);
@@ -374,7 +385,7 @@
 
         final int[] modalities = new int[] {
                 TYPE_FINGERPRINT,
-                BiometricAuthenticator.TYPE_FACE,
+                TYPE_FACE,
         };
 
         final int[] strengths = new int[] {
@@ -427,9 +438,56 @@
     }
 
     @Test
+    public void testAuthenticateFace_shouldShowSubtitleForFace() throws Exception {
+        setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
+
+        invokeAuthenticate(mBiometricService.mImpl, mReceiver1,
+                false /* requireConfirmation */,
+                null);
+        waitForIdle();
+
+        assertEquals(FACE_SUBTITLE, mBiometricService.mAuthSession.mPromptInfo.getSubtitle());
+    }
+
+    @Test
+    public void testAuthenticateFingerprint_shouldShowSubtitleForFingerprint() throws Exception {
+        setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
+
+        invokeAuthenticate(mBiometricService.mImpl, mReceiver1,
+                false /* requireConfirmation */,
+                null);
+        waitForIdle();
+
+        assertEquals(FINGERPRINT_SUBTITLE,
+                mBiometricService.mAuthSession.mPromptInfo.getSubtitle());
+    }
+
+    @Test
+    public void testAuthenticateBothFpAndFace_shouldShowDefaultSubtitle() throws Exception {
+        final int[] modalities = new int[] {
+                TYPE_FINGERPRINT,
+                TYPE_FACE,
+        };
+
+        final int[] strengths = new int[] {
+                Authenticators.BIOMETRIC_WEAK,
+                Authenticators.BIOMETRIC_STRONG,
+        };
+
+        setupAuthForMultiple(modalities, strengths);
+
+        invokeAuthenticate(mBiometricService.mImpl, mReceiver1,
+                false /* requireConfirmation */,
+                null);
+        waitForIdle();
+
+        assertEquals(DEFAULT_SUBTITLE, mBiometricService.mAuthSession.mPromptInfo.getSubtitle());
+    }
+
+    @Test
     public void testAuthenticateFace_respectsUserSetting()
             throws Exception {
-        setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
+        setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
 
         // Disabled in user settings receives onError
         when(mBiometricService.mSettingObserver.getEnabledForApps(anyInt())).thenReturn(false);
@@ -568,7 +626,7 @@
 
     @Test
     public void testAuthenticate_noBiometrics_credentialAllowed() throws Exception {
-        setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
+        setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
         when(mFaceAuthenticator.hasEnrolledTemplates(anyInt(), any())).thenReturn(false);
         when(mTrustManager.isDeviceSecure(anyInt(), anyInt()))
                 .thenReturn(true);
@@ -595,13 +653,13 @@
 
     @Test
     public void testAuthenticate_happyPathWithConfirmation_strongBiometric() throws Exception {
-        setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
+        setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
         testAuthenticate_happyPathWithConfirmation(true /* isStrongBiometric */);
     }
 
     @Test
     public void testAuthenticate_happyPathWithConfirmation_weakBiometric() throws Exception {
-        setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_WEAK);
+        setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_WEAK);
         testAuthenticate_happyPathWithConfirmation(false /* isStrongBiometric */);
     }
 
@@ -637,7 +695,7 @@
 
     @Test
     public void testAuthenticate_no_Biometrics_noCredential() throws Exception {
-        setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
+        setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
         when(mFaceAuthenticator.hasEnrolledTemplates(anyInt(), any())).thenReturn(false);
         when(mTrustManager.isDeviceSecure(anyInt(), anyInt()))
                 .thenReturn(false);
@@ -655,7 +713,7 @@
     @Test
     public void testRejectFace_whenAuthenticating_notifiesSystemUIAndClient_thenPaused()
             throws Exception {
-        setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
+        setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
         invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
                 false /* requireConfirmation */, null /* authenticators */);
 
@@ -663,7 +721,7 @@
         waitForIdle();
 
         verify(mBiometricService.mStatusBarService).onBiometricError(
-                eq(BiometricAuthenticator.TYPE_FACE),
+                eq(TYPE_FACE),
                 eq(BiometricConstants.BIOMETRIC_PAUSED_REJECTED),
                 eq(0 /* vendorCode */));
         verify(mReceiver1).onAuthenticationFailed();
@@ -691,7 +749,7 @@
 
     @Test
     public void testRequestAuthentication_whenAlreadyAuthenticating() throws Exception {
-        setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
+        setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
         invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
                 false /* requireConfirmation */, null /* authenticators */);
 
@@ -700,7 +758,7 @@
         waitForIdle();
 
         verify(mReceiver1).onError(
-                eq(BiometricAuthenticator.TYPE_FACE),
+                eq(TYPE_FACE),
                 eq(BiometricPrompt.BIOMETRIC_ERROR_CANCELED),
                 eq(0) /* vendorCode */);
         verify(mBiometricService.mStatusBarService).hideAuthenticationDialog(eq(TEST_REQUEST_ID));
@@ -710,7 +768,7 @@
 
     @Test
     public void testErrorHalTimeout_whenAuthenticating_entersPausedState() throws Exception {
-        setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
+        setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
         invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
                 false /* requireConfirmation */, null /* authenticators */);
 
@@ -723,7 +781,7 @@
 
         assertEquals(STATE_AUTH_PAUSED, mBiometricService.mAuthSession.getState());
         verify(mBiometricService.mStatusBarService).onBiometricError(
-                eq(BiometricAuthenticator.TYPE_FACE),
+                eq(TYPE_FACE),
                 eq(BiometricConstants.BIOMETRIC_ERROR_TIMEOUT),
                 eq(0 /* vendorCode */));
         // Timeout does not count as fail as per BiometricPrompt documentation.
@@ -759,7 +817,7 @@
 
     @Test
     public void testErrorFromHal_whenPaused_notifiesSystemUIAndClient() throws Exception {
-        setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
+        setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
         invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
                 false /* requireConfirmation */, null /* authenticators */);
 
@@ -777,7 +835,7 @@
 
         // Client receives error immediately
         verify(mReceiver1).onError(
-                eq(BiometricAuthenticator.TYPE_FACE),
+                eq(TYPE_FACE),
                 eq(BiometricConstants.BIOMETRIC_ERROR_CANCELED),
                 eq(0 /* vendorCode */));
         // Dialog is hidden immediately
@@ -926,7 +984,7 @@
             int biometricPromptError) throws Exception {
         final int[] modalities = new int[] {
                 TYPE_FINGERPRINT,
-                BiometricAuthenticator.TYPE_FACE,
+                TYPE_FACE,
         };
 
         final int[] strengths = new int[] {
@@ -1123,7 +1181,7 @@
 
     @Test
     public void testDismissedReasonNegative_whilePaused_invokeHalCancel() throws Exception {
-        setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
+        setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
         invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
                 false /* requireConfirmation */, null /* authenticators */);
 
@@ -1142,7 +1200,7 @@
 
     @Test
     public void testDismissedReasonUserCancel_whilePaused_invokesHalCancel() throws Exception {
-        setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
+        setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
         invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
                 false /* requireConfirmation */, null /* authenticators */);
 
@@ -1161,7 +1219,7 @@
 
     @Test
     public void testDismissedReasonUserCancel_whenPendingConfirmation() throws Exception {
-        setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
+        setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
         invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
                 true /* requireConfirmation */, null /* authenticators */);
 
@@ -1175,7 +1233,7 @@
         verify(mBiometricService.mSensors.get(0).impl)
                 .cancelAuthenticationFromService(any(), any(), anyLong());
         verify(mReceiver1).onError(
-                eq(BiometricAuthenticator.TYPE_FACE),
+                eq(TYPE_FACE),
                 eq(BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED),
                 eq(0 /* vendorCode */));
         verify(mBiometricService.mKeyStore, never()).addAuthToken(any(byte[].class));
@@ -1296,7 +1354,7 @@
 
     @Test
     public void testCanAuthenticate_whenBiometricsNotEnabledForApps() throws Exception {
-        setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
+        setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
         when(mBiometricService.mSettingObserver.getEnabledForApps(anyInt())).thenReturn(false);
         when(mTrustManager.isDeviceSecure(anyInt(), anyInt()))
                 .thenReturn(true);
@@ -1590,7 +1648,7 @@
     @Test
     public void testWorkAuthentication_faceWorksIfNotDisabledByDevicePolicyManager()
             throws Exception {
-        setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
+        setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
         when(mDevicePolicyManager
                 .getKeyguardDisabledFeatures(any() /* admin*/, anyInt() /* userHandle */))
                 .thenReturn(~DevicePolicyManager.KEYGUARD_DISABLE_FACE);
@@ -1683,7 +1741,7 @@
                     mFingerprintAuthenticator);
         }
 
-        if ((modality & BiometricAuthenticator.TYPE_FACE) != 0) {
+        if ((modality & TYPE_FACE) != 0) {
             when(mFaceAuthenticator.hasEnrolledTemplates(anyInt(), any())).thenReturn(enrolled);
             when(mFaceAuthenticator.isHardwareDetected(any())).thenReturn(true);
             when(mFaceAuthenticator.getLockoutModeForUser(anyInt()))
@@ -1715,7 +1773,7 @@
                         strength, mFingerprintAuthenticator);
             }
 
-            if ((modality & BiometricAuthenticator.TYPE_FACE) != 0) {
+            if ((modality & TYPE_FACE) != 0) {
                 when(mFaceAuthenticator.hasEnrolledTemplates(anyInt(), any())).thenReturn(true);
                 when(mFaceAuthenticator.isHardwareDetected(any())).thenReturn(true);
                 mBiometricService.mImpl.registerAuthenticator(SENSOR_ID_FACE, modality,
@@ -1798,6 +1856,7 @@
             boolean checkDevicePolicy) {
         final PromptInfo promptInfo = new PromptInfo();
         promptInfo.setConfirmationRequested(requireConfirmation);
+        promptInfo.setUseDefaultSubtitle(true);
 
         if (authenticators != null) {
             promptInfo.setAuthenticators(authenticators);
diff --git a/services/tests/servicestests/src/com/android/server/flags/FeatureFlagsServiceTest.java b/services/tests/servicestests/src/com/android/server/flags/FeatureFlagsServiceTest.java
new file mode 100644
index 0000000..df4731f
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/flags/FeatureFlagsServiceTest.java
@@ -0,0 +1,293 @@
+/*
+ * 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.flags;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.flags.IFeatureFlagsCallback;
+import android.flags.SyncableFlag;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.List;
+
+@Presubmit
+@SmallTest
+public class FeatureFlagsServiceTest {
+    private static final String NS = "ns";
+    private static final String NAME = "name";
+    private static final String PROP_NAME = FlagOverrideStore.getPropName(NS, NAME);
+
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock
+    private FlagOverrideStore mFlagStore;
+    @Mock
+    private FlagsShellCommand mFlagCommand;
+    @Mock
+    private IFeatureFlagsCallback mIFeatureFlagsCallback;
+    @Mock
+    private IBinder mIFeatureFlagsCallbackAsBinder;
+    @Mock
+    private FeatureFlagsService.PermissionsChecker mPermissionsChecker;
+
+    private FeatureFlagsBinder mFeatureFlagsService;
+
+    @Before
+    public void setup() {
+        when(mIFeatureFlagsCallback.asBinder()).thenReturn(mIFeatureFlagsCallbackAsBinder);
+        mFeatureFlagsService = new FeatureFlagsBinder(
+                mFlagStore, mFlagCommand, mPermissionsChecker);
+    }
+
+    @Test
+    public void testRegisterCallback() {
+        mFeatureFlagsService.registerCallback(mIFeatureFlagsCallback);
+        try {
+            verify(mIFeatureFlagsCallbackAsBinder).linkToDeath(any(), eq(0));
+        } catch (RemoteException e) {
+            fail("Our mock threw a Remote Exception?");
+        }
+    }
+
+    @Test
+    public void testOverrideFlag_requiresWritePermission() {
+        SecurityException exc = new SecurityException("not allowed");
+        doThrow(exc).when(mPermissionsChecker).assertWritePermission();
+
+        SyncableFlag f = new SyncableFlag(NS, "a", "false", false);
+
+        try {
+            mFeatureFlagsService.overrideFlag(f);
+            fail("Should have thrown exception");
+        } catch (SecurityException e) {
+            assertThat(exc).isEqualTo(e);
+        } catch (Exception e) {
+            fail("should have thrown a security exception");
+        }
+    }
+
+    @Test
+    public void testResetFlag_requiresWritePermission() {
+        SecurityException exc = new SecurityException("not allowed");
+        doThrow(exc).when(mPermissionsChecker).assertWritePermission();
+
+        SyncableFlag f = new SyncableFlag(NS, "a", "false", false);
+
+        try {
+            mFeatureFlagsService.resetFlag(f);
+            fail("Should have thrown exception");
+        } catch (SecurityException e) {
+            assertThat(exc).isEqualTo(e);
+        } catch (Exception e) {
+            fail("should have thrown a security exception");
+        }
+    }
+
+    @Test
+    public void testSyncFlags_noOverrides() {
+        List<SyncableFlag> inputFlags = List.of(
+                new SyncableFlag(NS, "a", "false", false),
+                new SyncableFlag(NS, "b", "true", false),
+                new SyncableFlag(NS, "c", "false", false)
+        );
+
+        List<SyncableFlag> outputFlags = mFeatureFlagsService.syncFlags(inputFlags);
+
+        assertThat(inputFlags.size()).isEqualTo(outputFlags.size());
+
+        for (SyncableFlag inpF: inputFlags) {
+            boolean found = false;
+            for (SyncableFlag outF : outputFlags) {
+                if (compareSyncableFlagsNames(inpF, outF)) {
+                    found = true;
+                    break;
+                }
+            }
+            assertWithMessage("Failed to find input flag " + inpF + " in the output")
+                    .that(found).isTrue();
+        }
+    }
+
+    @Test
+    public void testSyncFlags_withSomeOverrides() {
+        List<SyncableFlag> inputFlags = List.of(
+                new SyncableFlag(NS, "a", "false", false),
+                new SyncableFlag(NS, "b", "true", false),
+                new SyncableFlag(NS, "c", "false", false)
+        );
+
+        assertThat(mFlagStore).isNotNull();
+        when(mFlagStore.get(NS, "c")).thenReturn("true");
+        List<SyncableFlag> outputFlags = mFeatureFlagsService.syncFlags(inputFlags);
+
+        assertThat(inputFlags.size()).isEqualTo(outputFlags.size());
+
+        for (SyncableFlag inpF: inputFlags) {
+            boolean found = false;
+            for (SyncableFlag outF : outputFlags) {
+                if (compareSyncableFlagsNames(inpF, outF)) {
+                    found = true;
+
+                    // Once we've found "c", do an extra check
+                    if (outF.getName().equals("c")) {
+                        assertWithMessage("Flag " + outF + "was not returned with an override")
+                                .that(outF.getValue()).isEqualTo("true");
+                    }
+                    break;
+                }
+            }
+            assertWithMessage("Failed to find input flag " + inpF + " in the output")
+                    .that(found).isTrue();
+        }
+    }
+
+    @Test
+    public void testSyncFlags_twoCallsWithDifferentDefaults() {
+        List<SyncableFlag> inputFlagsFirst = List.of(
+                new SyncableFlag(NS, "a", "false", false)
+        );
+        List<SyncableFlag> inputFlagsSecond = List.of(
+                new SyncableFlag(NS, "a", "true", false),
+                new SyncableFlag(NS, "b", "false", false)
+        );
+
+        List<SyncableFlag> outputFlagsFirst = mFeatureFlagsService.syncFlags(inputFlagsFirst);
+        List<SyncableFlag> outputFlagsSecond = mFeatureFlagsService.syncFlags(inputFlagsSecond);
+
+        assertThat(inputFlagsFirst.size()).isEqualTo(outputFlagsFirst.size());
+        assertThat(inputFlagsSecond.size()).isEqualTo(outputFlagsSecond.size());
+
+        // This test only cares that the "a" flag passed in the second time came out with the
+        // same value that was passed in the first time.
+
+        boolean found = false;
+        for (SyncableFlag second : outputFlagsSecond) {
+            if (compareSyncableFlagsNames(second, inputFlagsFirst.get(0))) {
+                found = true;
+                assertThat(second.getValue()).isEqualTo(inputFlagsFirst.get(0).getValue());
+                break;
+            }
+        }
+
+        assertWithMessage(
+                "Failed to find flag " + inputFlagsFirst.get(0) + " in the second calls output")
+                .that(found).isTrue();
+    }
+
+    @Test
+    public void testQueryFlags_onlyOnce() {
+        List<SyncableFlag> inputFlags = List.of(
+                new SyncableFlag(NS, "a", "false", false),
+                new SyncableFlag(NS, "b", "true", false),
+                new SyncableFlag(NS, "c", "false", false)
+        );
+
+        List<SyncableFlag> outputFlags = mFeatureFlagsService.queryFlags(inputFlags);
+
+        assertThat(inputFlags.size()).isEqualTo(outputFlags.size());
+
+        for (SyncableFlag inpF: inputFlags) {
+            boolean found = false;
+            for (SyncableFlag outF : outputFlags) {
+                if (compareSyncableFlagsNames(inpF, outF)) {
+                    found = true;
+                    break;
+                }
+            }
+            assertWithMessage("Failed to find input flag " + inpF + " in the output")
+                    .that(found).isTrue();
+        }
+    }
+
+    @Test
+    public void testQueryFlags_twoCallsWithDifferentDefaults() {
+        List<SyncableFlag> inputFlagsFirst = List.of(
+                new SyncableFlag(NS, "a", "false", false)
+        );
+        List<SyncableFlag> inputFlagsSecond = List.of(
+                new SyncableFlag(NS, "a", "true", false),
+                new SyncableFlag(NS, "b", "false", false)
+        );
+
+        List<SyncableFlag> outputFlagsFirst = mFeatureFlagsService.queryFlags(inputFlagsFirst);
+        List<SyncableFlag> outputFlagsSecond = mFeatureFlagsService.queryFlags(inputFlagsSecond);
+
+        assertThat(inputFlagsFirst.size()).isEqualTo(outputFlagsFirst.size());
+        assertThat(inputFlagsSecond.size()).isEqualTo(outputFlagsSecond.size());
+
+        // This test only cares that the "a" flag passed in the second time came out with the
+        // same value that was passed in (i.e. it wasn't cached).
+
+        boolean found = false;
+        for (SyncableFlag second : outputFlagsSecond) {
+            if (compareSyncableFlagsNames(second, inputFlagsSecond.get(0))) {
+                found = true;
+                assertThat(second.getValue()).isEqualTo(inputFlagsSecond.get(0).getValue());
+                break;
+            }
+        }
+
+        assertWithMessage(
+                "Failed to find flag " + inputFlagsSecond.get(0) + " in the second calls output")
+                .that(found).isTrue();
+    }
+
+    @Test
+    public void testOverrideFlag() {
+        SyncableFlag f = new SyncableFlag(NS, "a", "false", false);
+
+        mFeatureFlagsService.overrideFlag(f);
+
+        verify(mFlagStore).set(f.getNamespace(), f.getName(), f.getValue());
+    }
+
+    @Test
+    public void testResetFlag() {
+        SyncableFlag f = new SyncableFlag(NS, "a", "false", false);
+
+        mFeatureFlagsService.resetFlag(f);
+
+        verify(mFlagStore).erase(f.getNamespace(), f.getName());
+    }
+
+
+    private static boolean compareSyncableFlagsNames(SyncableFlag a, SyncableFlag b) {
+        return a.getNamespace().equals(b.getNamespace())
+                && a.getName().equals(b.getName())
+                && a.isDynamic() == b.isDynamic();
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/flags/FlagCacheTest.java b/services/tests/servicestests/src/com/android/server/flags/FlagCacheTest.java
new file mode 100644
index 0000000..c2cf540
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/flags/FlagCacheTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.flags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class FlagCacheTest {
+    private static final String NS = "ns";
+    private static final String NAME = "name";
+
+    FlagCache mFlagCache = new FlagCache();
+
+    @Test
+    public void testGetOrNull_unset() {
+        assertThat(mFlagCache.getOrNull(NS, NAME)).isNull();
+    }
+
+    @Test
+    public void testGetOrSet_unset() {
+        assertThat(mFlagCache.getOrSet(NS, NAME, "value")).isEqualTo("value");
+    }
+
+    @Test
+    public void testGetOrSet_alreadySet() {
+        mFlagCache.setIfChanged(NS, NAME, "value");
+        assertThat(mFlagCache.getOrSet(NS, NAME, "newvalue")).isEqualTo("value");
+    }
+
+    @Test
+    public void testSetIfChanged_unset() {
+        assertThat(mFlagCache.setIfChanged(NS, NAME, "value")).isTrue();
+    }
+
+    @Test
+    public void testSetIfChanged_noChange() {
+        mFlagCache.setIfChanged(NS, NAME, "value");
+        assertThat(mFlagCache.setIfChanged(NS, NAME, "value")).isFalse();
+    }
+
+    @Test
+    public void testSetIfChanged_changing() {
+        mFlagCache.setIfChanged(NS, NAME, "value");
+        assertThat(mFlagCache.setIfChanged(NS, NAME, "newvalue")).isTrue();
+    }
+
+    @Test
+    public void testContainsNamespace_unset() {
+        assertThat(mFlagCache.containsNamespace(NS)).isFalse();
+    }
+
+    @Test
+    public void testContainsNamespace_set() {
+        mFlagCache.setIfChanged(NS, NAME, "value");
+        assertThat(mFlagCache.containsNamespace(NS)).isTrue();
+    }
+
+    @Test
+    public void testContains_unset() {
+        assertThat(mFlagCache.contains(NS, NAME)).isFalse();
+    }
+
+    @Test
+    public void testContains_set() {
+        mFlagCache.setIfChanged(NS, NAME, "value");
+        assertThat(mFlagCache.contains(NS, NAME)).isTrue();
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/flags/FlagOverrideStoreTest.java b/services/tests/servicestests/src/com/android/server/flags/FlagOverrideStoreTest.java
new file mode 100644
index 0000000..6cc3acf
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/flags/FlagOverrideStoreTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.flags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@Presubmit
+@SmallTest
+public class FlagOverrideStoreTest {
+    private static final String NS = "ns";
+    private static final String NAME = "name";
+    private static final String PROP_NAME = FlagOverrideStore.getPropName(NS, NAME);
+
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock
+    private SettingsProxy mSettingsProxy;
+    @Mock
+    private FlagOverrideStore.FlagChangeCallback mCallback;
+
+    private FlagOverrideStore mFlagStore;
+
+    @Before
+    public void setup() {
+        mFlagStore = new FlagOverrideStore(mSettingsProxy);
+        mFlagStore.setChangeCallback(mCallback);
+    }
+
+    @Test
+    public void testSet_unset() {
+        mFlagStore.set(NS, NAME, "value");
+        verify(mSettingsProxy).putString(PROP_NAME, "value");
+    }
+
+    @Test
+    public void testSet_setTwice() {
+        mFlagStore.set(NS, NAME, "value");
+        mFlagStore.set(NS, NAME, "newvalue");
+        verify(mSettingsProxy).putString(PROP_NAME, "value");
+        verify(mSettingsProxy).putString(PROP_NAME, "newvalue");
+    }
+
+    @Test
+    public void testGet_unset() {
+        assertThat(mFlagStore.get(NS, NAME)).isNull();
+    }
+
+    @Test
+    public void testGet_set() {
+        when(mSettingsProxy.getString(PROP_NAME)).thenReturn("value");
+        assertThat(mFlagStore.get(NS, NAME)).isEqualTo("value");
+    }
+
+    @Test
+    public void testErase() {
+        mFlagStore.erase(NS, NAME);
+        verify(mSettingsProxy).putString(PROP_NAME, null);
+    }
+
+    @Test
+    public void testContains_unset() {
+        assertThat(mFlagStore.contains(NS, NAME)).isFalse();
+    }
+
+    @Test
+    public void testContains_set() {
+        when(mSettingsProxy.getString(PROP_NAME)).thenReturn("value");
+        assertThat(mFlagStore.contains(NS, NAME)).isTrue();
+    }
+
+    @Test
+    public void testCallback_onSet() {
+        mFlagStore.set(NS, NAME, "value");
+        verify(mCallback).onFlagChanged(NS, NAME, "value");
+    }
+
+    @Test
+    public void testCallback_onErase() {
+        mFlagStore.erase(NS, NAME);
+        verify(mCallback).onFlagChanged(NS, NAME, null);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/flags/OWNERS b/services/tests/servicestests/src/com/android/server/flags/OWNERS
new file mode 100644
index 0000000..7ed369e
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/flags/OWNERS
@@ -0,0 +1 @@
+include /services/flags/OWNERS
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 3c882dc8..a109d5c 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -73,6 +73,7 @@
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ONGOING;
+import static android.service.notification.NotificationListenerService.REASON_CANCEL;
 import static android.service.notification.NotificationListenerService.REASON_LOCKDOWN;
 import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE;
 import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEUTRAL;
@@ -2009,10 +2010,10 @@
         final StatusBarNotification sbn = generateNotificationRecord(null).getSbn();
         mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(),
                 sbn.getNotification(), sbn.getUserId());
-        Thread.sleep(1);  // make sure the system clock advances before the next step
+        mTestableLooper.moveTimeForward(1);
         // THEN it is canceled
         mBinderService.cancelNotificationWithTag(PKG, PKG, "tag", sbn.getId(), sbn.getUserId());
-        Thread.sleep(1);  // here too
+        mTestableLooper.moveTimeForward(1);
         // THEN it is posted again (before the cancel has a chance to finish)
         mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(),
                 sbn.getNotification(), sbn.getUserId());
@@ -2303,7 +2304,7 @@
         notif.getNotification().flags |= Notification.FLAG_NO_CLEAR;
         mService.addNotification(notif);
         mService.cancelAllNotificationsInt(mUid, 0, PKG, null, 0, 0,
-                notif.getUserId(), 0);
+                notif.getUserId(), REASON_CANCEL);
         waitForIdle();
         StatusBarNotification[] notifs =
                 mBinderService.getActiveNotifications(notif.getSbn().getPackageName());
@@ -3041,7 +3042,7 @@
         notif.getNotification().flags |= Notification.FLAG_NO_CLEAR;
         mService.addNotification(notif);
         mService.cancelAllNotificationsInt(mUid, 0, PKG, null, 0,
-                Notification.FLAG_ONGOING_EVENT, notif.getUserId(), 0);
+                Notification.FLAG_ONGOING_EVENT, notif.getUserId(), REASON_CANCEL);
         waitForIdle();
         StatusBarNotification[] notifs =
                 mBinderService.getActiveNotifications(notif.getSbn().getPackageName());
@@ -3069,7 +3070,7 @@
         notif.getNotification().flags |= Notification.FLAG_ONGOING_EVENT;
         mService.addNotification(notif);
         mService.cancelAllNotificationsInt(mUid, 0, PKG, null, 0, 0,
-                notif.getUserId(), 0);
+                notif.getUserId(), REASON_CANCEL);
         waitForIdle();
         StatusBarNotification[] notifs =
                 mBinderService.getActiveNotifications(notif.getSbn().getPackageName());
@@ -12208,7 +12209,7 @@
         mOnPermissionChangeListener.onOpChanged(
                 AppOpsManager.OPSTR_POST_NOTIFICATION, PKG, 0);
         waitForIdle();
-        Thread.sleep(600);
+        mTestableLooper.moveTimeForward(500);
         waitForIdle();
 
         ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
@@ -12227,7 +12228,7 @@
         mOnPermissionChangeListener.onOpChanged(
                 AppOpsManager.OPSTR_POST_NOTIFICATION, PKG, 0);
         waitForIdle();
-        Thread.sleep(600);
+        mTestableLooper.moveTimeForward(500);
         waitForIdle();
 
         ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
@@ -12261,7 +12262,7 @@
 
         assertThat(mService.mNotificationList).hasSize(0);
 
-        Thread.sleep(600);
+        mTestableLooper.moveTimeForward(500);
         waitForIdle();
         verify(mContext).sendBroadcastAsUser(any(), eq(UserHandle.of(0)), eq(null));
     }
diff --git a/services/tests/vibrator/AndroidManifest.xml b/services/tests/vibrator/AndroidManifest.xml
index 2a15c15..a14ea55 100644
--- a/services/tests/vibrator/AndroidManifest.xml
+++ b/services/tests/vibrator/AndroidManifest.xml
@@ -31,8 +31,7 @@
     <!-- Required to set always-on vibrations -->
     <uses-permission android:name="android.permission.VIBRATE_ALWAYS_ON" />
 
-    <application android:debuggable="true"
-        android:testOnly="true">
+    <application android:debuggable="true">
         <uses-library android:name="android.test.mock" android:required="true" />
         <uses-library android:name="android.test.runner" />
     </application>
diff --git a/services/tests/vibrator/AndroidTest.xml b/services/tests/vibrator/AndroidTest.xml
deleted file mode 100644
index d5ee3af..0000000
--- a/services/tests/vibrator/AndroidTest.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<?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.
--->
-
-<configuration description="Runs Frameworks Vibrator Services Tests.">
-    <option name="test-suite-tag" value="apct" />
-    <option name="test-suite-tag" value="apct-instrumentation" />
-
-    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
-        <option name="cleanup-apks" value="true" />
-        <option name="install-arg" value="-t" />
-        <option name="test-file-name" value="FrameworksVibratorServicesTests.apk" />
-    </target_preparer>
-
-    <option name="test-tag" value="FrameworksVibratorServicesTests" />
-
-    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
-        <option name="package" value="com.android.framework.services.tests.vibrator" />
-        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
-        <option name="hidden-api-checks" value="false" />
-    </test>
-</configuration>
diff --git a/services/tests/vibrator/TEST_MAPPING b/services/tests/vibrator/TEST_MAPPING
index f0a7e47..22b72fa 100644
--- a/services/tests/vibrator/TEST_MAPPING
+++ b/services/tests/vibrator/TEST_MAPPING
@@ -1,21 +1,7 @@
 {
-  "presubmit": [
+  "imports": [
     {
-      "name": "FrameworksVibratorServicesTests",
-      "options": [
-        {"exclude-annotation": "android.platform.test.annotations.LargeTest"},
-        {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
-        {"exclude-annotation": "androidx.test.filters.FlakyTest"},
-        {"exclude-annotation": "org.junit.Ignore"}
-      ]
-    }
-  ],
-  "postsubmit": [
-    {
-      "name": "FrameworksVibratorServicesTests",
-      "options": [
-        {"exclude-annotation": "org.junit.Ignore"}
-      ]
+      "path": "frameworks/base/services/core/java/com/android/server/vibrator"
     }
   ]
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
index 5c3102d..65e77dc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
@@ -182,12 +182,12 @@
 
     @Test
     public void testLaunchState() {
-        final ToIntFunction<Boolean> launchTemplate = doRelaunch -> {
+        final ToIntFunction<Runnable> launchTemplate = action -> {
             clearInvocations(mLaunchObserver);
             onActivityLaunched(mTopActivity);
             notifyTransitionStarting(mTopActivity);
-            if (doRelaunch) {
-                mActivityMetricsLogger.notifyActivityRelaunched(mTopActivity);
+            if (action != null) {
+                action.run();
             }
             final ActivityMetricsLogger.TransitionInfoSnapshot info =
                     notifyWindowsDrawn(mTopActivity);
@@ -199,21 +199,27 @@
         // Assume that the process is started (ActivityBuilder has mocked the returned value of
         // ATMS#getProcessController) but the activity has not attached process.
         mTopActivity.app = null;
-        assertWithMessage("Warm launch").that(launchTemplate.applyAsInt(false /* doRelaunch */))
+        assertWithMessage("Warm launch").that(launchTemplate.applyAsInt(null))
                 .isEqualTo(WaitResult.LAUNCH_STATE_WARM);
 
         mTopActivity.app = app;
         mNewActivityCreated = false;
-        assertWithMessage("Hot launch").that(launchTemplate.applyAsInt(false /* doRelaunch */))
+        assertWithMessage("Hot launch").that(launchTemplate.applyAsInt(null))
                 .isEqualTo(WaitResult.LAUNCH_STATE_HOT);
 
-        assertWithMessage("Relaunch").that(launchTemplate.applyAsInt(true /* doRelaunch */))
+        assertWithMessage("Relaunch").that(launchTemplate.applyAsInt(
+                () -> mActivityMetricsLogger.notifyActivityRelaunched(mTopActivity)))
                 .isEqualTo(WaitResult.LAUNCH_STATE_RELAUNCH);
 
+        assertWithMessage("Cold launch by restart").that(launchTemplate.applyAsInt(
+                () -> mActivityMetricsLogger.notifyBindApplication(
+                        mTopActivity.info.applicationInfo)))
+                .isEqualTo(WaitResult.LAUNCH_STATE_COLD);
+
         mTopActivity.app = null;
         mNewActivityCreated = true;
         doReturn(null).when(mAtm).getProcessController(app.mName, app.mUid);
-        assertWithMessage("Cold launch").that(launchTemplate.applyAsInt(false /* doRelaunch */))
+        assertWithMessage("Cold launch").that(launchTemplate.applyAsInt(null))
                 .isEqualTo(WaitResult.LAUNCH_STATE_COLD);
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java b/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java
index fe1ea0d..f6f3f03 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java
@@ -22,11 +22,17 @@
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.verify;
 
 import android.app.Activity;
 import android.app.ActivityManager.RunningTaskInfo;
@@ -41,6 +47,7 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.platform.test.annotations.Presubmit;
+import android.util.Log;
 import android.util.Rational;
 import android.view.SurfaceControl;
 import android.window.TaskOrganizer;
@@ -48,7 +55,10 @@
 import androidx.test.filters.MediumTest;
 
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
@@ -166,6 +176,122 @@
         }
     }
 
+    /**
+     * Tests if any unknown key is being used in the ActivityOptions bundle. If so, please review
+     * if the newly added bundle should be protected with permissions to avoid malicious attacks.
+     *
+     * @see SafeActivityOptionsTest#test_getOptions
+     */
+    @Test
+    public void testActivityOptionsFromBundle() {
+        // Spy on a bundle that is generated from a basic ActivityOptions.
+        final ActivityOptions options = ActivityOptions.makeBasic();
+        Bundle bundle = options.toBundle();
+        spyOn(bundle);
+
+        // Create a new ActivityOptions from the bundle
+        new ActivityOptions(bundle);
+
+        // Verify the keys that are being used.
+        final ArgumentCaptor<String> stringCaptor =  ArgumentCaptor.forClass(String.class);
+        verify(bundle, atLeastOnce()).getString(stringCaptor.capture());
+        verify(bundle, atLeastOnce()).getBoolean(stringCaptor.capture());
+        verify(bundle, atLeastOnce()).getParcelable(stringCaptor.capture(), any());
+        verify(bundle, atLeastOnce()).getInt(stringCaptor.capture(), anyInt());
+        verify(bundle, atLeastOnce()).getBinder(stringCaptor.capture());
+        verify(bundle, atLeastOnce()).getBundle(stringCaptor.capture());
+        final List<String> keys = stringCaptor.getAllValues();
+        final List<String> unknownKeys = new ArrayList<>();
+        for (String key : keys) {
+            switch (key) {
+                case ActivityOptions.KEY_PACKAGE_NAME:
+                case ActivityOptions.KEY_LAUNCH_BOUNDS:
+                case ActivityOptions.KEY_ANIM_TYPE:
+                case ActivityOptions.KEY_ANIM_ENTER_RES_ID:
+                case ActivityOptions.KEY_ANIM_EXIT_RES_ID:
+                case ActivityOptions.KEY_ANIM_IN_PLACE_RES_ID:
+                case ActivityOptions.KEY_ANIM_BACKGROUND_COLOR:
+                case ActivityOptions.KEY_ANIM_THUMBNAIL:
+                case ActivityOptions.KEY_ANIM_START_X:
+                case ActivityOptions.KEY_ANIM_START_Y:
+                case ActivityOptions.KEY_ANIM_WIDTH:
+                case ActivityOptions.KEY_ANIM_HEIGHT:
+                case ActivityOptions.KEY_ANIM_START_LISTENER:
+                case ActivityOptions.KEY_SPLASH_SCREEN_THEME:
+                case ActivityOptions.KEY_LEGACY_PERMISSION_PROMPT_ELIGIBLE:
+                case ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN:
+                case ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN:
+                case ActivityOptions.KEY_TRANSIENT_LAUNCH:
+                case "android:activity.animationFinishedListener":
+                    // KEY_ANIMATION_FINISHED_LISTENER
+                case "android:activity.animSpecs": // KEY_ANIM_SPECS
+                case "android:activity.lockTaskMode": // KEY_LOCK_TASK_MODE
+                case "android:activity.shareIdentity": // KEY_SHARE_IDENTITY
+                case "android.activity.launchDisplayId": // KEY_LAUNCH_DISPLAY_ID
+                case "android.activity.callerDisplayId": // KEY_CALLER_DISPLAY_ID
+                case "android.activity.launchTaskDisplayAreaToken":
+                    // KEY_LAUNCH_TASK_DISPLAY_AREA_TOKEN
+                case "android.activity.launchTaskDisplayAreaFeatureId":
+                    // KEY_LAUNCH_TASK_DISPLAY_AREA_FEATURE_ID
+                case "android.activity.windowingMode": // KEY_LAUNCH_WINDOWING_MODE
+                case "android.activity.activityType": // KEY_LAUNCH_ACTIVITY_TYPE
+                case "android.activity.launchTaskId": // KEY_LAUNCH_TASK_ID
+                case "android.activity.disableStarting": // KEY_DISABLE_STARTING_WINDOW
+                case "android.activity.pendingIntentLaunchFlags":
+                    // KEY_PENDING_INTENT_LAUNCH_FLAGS
+                case "android.activity.alwaysOnTop": // KEY_TASK_ALWAYS_ON_TOP
+                case "android.activity.taskOverlay": // KEY_TASK_OVERLAY
+                case "android.activity.taskOverlayCanResume": // KEY_TASK_OVERLAY_CAN_RESUME
+                case "android.activity.avoidMoveToFront": // KEY_AVOID_MOVE_TO_FRONT
+                case "android.activity.freezeRecentTasksReordering":
+                    // KEY_FREEZE_RECENT_TASKS_REORDERING
+                case "android:activity.disallowEnterPictureInPictureWhileLaunching":
+                    // KEY_DISALLOW_ENTER_PICTURE_IN_PICTURE_WHILE_LAUNCHING
+                case "android:activity.applyActivityFlagsForBubbles":
+                    // KEY_APPLY_ACTIVITY_FLAGS_FOR_BUBBLES
+                case "android:activity.applyMultipleTaskFlagForShortcut":
+                    // KEY_APPLY_MULTIPLE_TASK_FLAG_FOR_SHORTCUT
+                case "android:activity.applyNoUserActionFlagForShortcut":
+                    // KEY_APPLY_NO_USER_ACTION_FLAG_FOR_SHORTCUT
+                case "android:activity.transitionCompleteListener":
+                    // KEY_TRANSITION_COMPLETE_LISTENER
+                case "android:activity.transitionIsReturning": // KEY_TRANSITION_IS_RETURNING
+                case "android:activity.sharedElementNames": // KEY_TRANSITION_SHARED_ELEMENTS
+                case "android:activity.resultData": // KEY_RESULT_DATA
+                case "android:activity.resultCode": // KEY_RESULT_CODE
+                case "android:activity.exitCoordinatorIndex": // KEY_EXIT_COORDINATOR_INDEX
+                case "android.activity.sourceInfo": // KEY_SOURCE_INFO
+                case "android:activity.usageTimeReport": // KEY_USAGE_TIME_REPORT
+                case "android:activity.rotationAnimationHint": // KEY_ROTATION_ANIMATION_HINT
+                case "android:instantapps.installerbundle": // KEY_INSTANT_APP_VERIFICATION_BUNDLE
+                case "android:activity.specsFuture": // KEY_SPECS_FUTURE
+                case "android:activity.remoteAnimationAdapter": // KEY_REMOTE_ANIMATION_ADAPTER
+                case "android:activity.remoteTransition": // KEY_REMOTE_TRANSITION
+                case "android:activity.overrideTaskTransition": // KEY_OVERRIDE_TASK_TRANSITION
+                case "android.activity.removeWithTaskOrganizer": // KEY_REMOVE_WITH_TASK_ORGANIZER
+                case "android.activity.launchTypeBubble": // KEY_LAUNCHED_FROM_BUBBLE
+                case "android.activity.splashScreenStyle": // KEY_SPLASH_SCREEN_STYLE
+                case "android.activity.launchIntoPipParams": // KEY_LAUNCH_INTO_PIP_PARAMS
+                case "android.activity.dismissKeyguard": // KEY_DISMISS_KEYGUARD
+                case "android.activity.pendingIntentCreatorBackgroundActivityStartMode":
+                    // KEY_PENDING_INTENT_CREATOR_BACKGROUND_ACTIVITY_START_MODE
+                case "android.activity.launchCookie": // KEY_LAUNCH_COOKIE
+                    // Existing keys
+                    break;
+                default:
+                    unknownKeys.add(key);
+                    break;
+            }
+        }
+
+        // Report if any unknown key exists.
+        for (String key : unknownKeys) {
+            Log.e("ActivityOptionsTests", "Unknown key " + key + " is found. "
+                    + "Please review if the given bundle should be protected with permissions.");
+        }
+        assertTrue(unknownKeys.isEmpty());
+    }
+
     public static class TrampolineActivity extends Activity {
         static int sTaskId;
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java
index 51a7e74..06033c7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java
@@ -20,7 +20,6 @@
 
 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT;
 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
-import static com.android.server.wm.LetterboxConfigurationPersister.LETTERBOX_CONFIGURATION_FILENAME;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -42,13 +41,26 @@
 import java.io.File;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Consumer;
+import java.util.function.Supplier;
 
+/**
+ * Tests for the {@link LetterboxConfigurationPersister} class.
+ *
+ * Build/Install/Run:
+ *  atest WmTests:LetterboxConfigurationPersisterTest
+ */
 @SmallTest
 @Presubmit
 public class LetterboxConfigurationPersisterTest {
 
     private static final long TIMEOUT = 2000L; // 2 secs
 
+    private static final int DEFAULT_REACHABILITY_TEST = -1;
+    private static final Supplier<Integer> DEFAULT_REACHABILITY_SUPPLIER_TEST =
+            () -> DEFAULT_REACHABILITY_TEST;
+
+    private static final String LETTERBOX_CONFIGURATION_TEST_FILENAME = "letterbox_config_test";
+
     private LetterboxConfigurationPersister mLetterboxConfigurationPersister;
     private Context mContext;
     private PersisterQueue mPersisterQueue;
@@ -62,7 +74,7 @@
         mConfigFolder = mContext.getFilesDir();
         mPersisterQueue = new PersisterQueue();
         mQueueState = new QueueState();
-        mLetterboxConfigurationPersister = new LetterboxConfigurationPersister(mContext,
+        mLetterboxConfigurationPersister = new LetterboxConfigurationPersister(
                 () -> mContext.getResources().getInteger(
                         R.integer.config_letterboxDefaultPositionForHorizontalReachability),
                 () -> mContext.getResources().getInteger(
@@ -72,7 +84,8 @@
                 () -> mContext.getResources().getInteger(
                         R.integer.config_letterboxDefaultPositionForTabletopModeReachability
                 ),
-                mConfigFolder, mPersisterQueue, mQueueState);
+                mConfigFolder, mPersisterQueue, mQueueState,
+                LETTERBOX_CONFIGURATION_TEST_FILENAME);
         mQueueListener = queueEmpty -> mQueueState.onItemAdded();
         mPersisterQueue.addListener(mQueueListener);
         mLetterboxConfigurationPersister.start();
@@ -127,8 +140,10 @@
     public void test_whenUpdatedWithNewValues_valuesAreReadAfterRestart() {
         final PersisterQueue firstPersisterQueue = new PersisterQueue();
         final LetterboxConfigurationPersister firstPersister = new LetterboxConfigurationPersister(
-                mContext, () -> -1, () -> -1, () -> -1, () -> -1, mContext.getFilesDir(),
-                firstPersisterQueue, mQueueState);
+                DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST,
+                DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST,
+                mContext.getFilesDir(), firstPersisterQueue, mQueueState,
+                LETTERBOX_CONFIGURATION_TEST_FILENAME);
         firstPersister.start();
         firstPersister.setLetterboxPositionForHorizontalReachability(false,
                 LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT);
@@ -138,8 +153,10 @@
         stopPersisterSafe(firstPersisterQueue);
         final PersisterQueue secondPersisterQueue = new PersisterQueue();
         final LetterboxConfigurationPersister secondPersister = new LetterboxConfigurationPersister(
-                mContext, () -> -1, () -> -1, () -> -1, () -> -1, mContext.getFilesDir(),
-                secondPersisterQueue, mQueueState);
+                DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST,
+                DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST,
+                mContext.getFilesDir(), secondPersisterQueue, mQueueState,
+                LETTERBOX_CONFIGURATION_TEST_FILENAME);
         secondPersister.start();
         final int newPositionForHorizontalReachability =
                 secondPersister.getLetterboxPositionForHorizontalReachability(false);
@@ -156,37 +173,46 @@
 
     @Test
     public void test_whenUpdatedWithNewValuesAndDeleted_valuesAreDefaults() {
-        mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(false,
+        final PersisterQueue firstPersisterQueue = new PersisterQueue();
+        final LetterboxConfigurationPersister firstPersister = new LetterboxConfigurationPersister(
+                DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST,
+                DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST,
+                mContext.getFilesDir(), firstPersisterQueue, mQueueState,
+                LETTERBOX_CONFIGURATION_TEST_FILENAME);
+        firstPersister.start();
+        firstPersister.setLetterboxPositionForHorizontalReachability(false,
                 LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT);
-        mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(false,
+        firstPersister.setLetterboxPositionForVerticalReachability(false,
                 LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP);
         waitForCompletion(mPersisterQueue);
         final int newPositionForHorizontalReachability =
-                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
-                        false);
+                firstPersister.getLetterboxPositionForHorizontalReachability(false);
         final int newPositionForVerticalReachability =
-                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(false);
+                firstPersister.getLetterboxPositionForVerticalReachability(false);
         Assert.assertEquals(LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
                 newPositionForHorizontalReachability);
         Assert.assertEquals(LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
                 newPositionForVerticalReachability);
-        deleteConfiguration(mLetterboxConfigurationPersister, mPersisterQueue);
-        waitForCompletion(mPersisterQueue);
+        deleteConfiguration(firstPersister, firstPersisterQueue);
+        waitForCompletion(firstPersisterQueue);
+        stopPersisterSafe(firstPersisterQueue);
+
+        final PersisterQueue secondPersisterQueue = new PersisterQueue();
+        final LetterboxConfigurationPersister secondPersister = new LetterboxConfigurationPersister(
+                DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST,
+                DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST,
+                mContext.getFilesDir(), secondPersisterQueue, mQueueState,
+                LETTERBOX_CONFIGURATION_TEST_FILENAME);
+        secondPersister.start();
         final int positionForHorizontalReachability =
-                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
-                        false);
-        final int defaultPositionForHorizontalReachability =
-                mContext.getResources().getInteger(
-                        R.integer.config_letterboxDefaultPositionForHorizontalReachability);
-        Assert.assertEquals(defaultPositionForHorizontalReachability,
-                positionForHorizontalReachability);
+                secondPersister.getLetterboxPositionForHorizontalReachability(false);
         final int positionForVerticalReachability =
-                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(false);
-        final int defaultPositionForVerticalReachability =
-                mContext.getResources().getInteger(
-                        R.integer.config_letterboxDefaultPositionForVerticalReachability);
-        Assert.assertEquals(defaultPositionForVerticalReachability,
-                positionForVerticalReachability);
+                secondPersister.getLetterboxPositionForVerticalReachability(false);
+        Assert.assertEquals(DEFAULT_REACHABILITY_TEST, positionForHorizontalReachability);
+        Assert.assertEquals(DEFAULT_REACHABILITY_TEST, positionForVerticalReachability);
+        deleteConfiguration(secondPersister, secondPersisterQueue);
+        waitForCompletion(secondPersisterQueue);
+        stopPersisterSafe(secondPersisterQueue);
     }
 
     private void stopPersisterSafe(PersisterQueue persisterQueue) {
@@ -222,7 +248,7 @@
     private void deleteConfiguration(LetterboxConfigurationPersister persister,
             PersisterQueue persisterQueue) {
         final AtomicFile fileToDelete = new AtomicFile(
-                new File(mConfigFolder, LETTERBOX_CONFIGURATION_FILENAME));
+                new File(mConfigFolder, LETTERBOX_CONFIGURATION_TEST_FILENAME));
         persisterQueue.addItem(
                 new DeleteFileCommand(fileToDelete, mQueueState.andThen(
                         s -> persister.useDefaultValue())), true);
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
index f23e56d..7cb5802 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
@@ -1303,6 +1303,26 @@
         assertTrue(info.supportsMultiWindow);
     }
 
+    @Test
+    public void testRemoveCompatibleRecentTask() {
+        final Task task1 = createTaskBuilder(".Task").setWindowingMode(
+                WINDOWING_MODE_FULLSCREEN).build();
+        mRecentTasks.add(task1);
+        final Task task2 = createTaskBuilder(".Task").setWindowingMode(
+                WINDOWING_MODE_MULTI_WINDOW).build();
+        mRecentTasks.add(task2);
+        assertEquals(2, mRecentTasks.getRecentTasks(MAX_VALUE, 0 /* flags */,
+                true /* getTasksAllowed */, TEST_USER_0_ID, 0).getList().size());
+
+        // Set windowing mode and ensure the same fullscreen task that created earlier is removed.
+        task2.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+        mRecentTasks.removeCompatibleRecentTask(task2);
+        assertEquals(1, mRecentTasks.getRecentTasks(MAX_VALUE, 0 /* flags */,
+                true /* getTasksAllowed */, TEST_USER_0_ID, 0).getList().size());
+        assertEquals(task2.mTaskId, mRecentTasks.getRecentTasks(MAX_VALUE, 0 /* flags */,
+                true /* getTasksAllowed */, TEST_USER_0_ID, 0).getList().get(0).taskId);
+    }
+
     private TaskSnapshot createSnapshot(Point taskSize, Point bufferSize) {
         HardwareBuffer buffer = null;
         if (bufferSize != null) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java b/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java
index abf21a5..7eab06a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java
@@ -656,7 +656,7 @@
                 topSplitPrimary.getVisibility(null /* starting */));
         // Make primary split root transient-hide.
         spyOn(splitPrimary.mTransitionController);
-        doReturn(true).when(splitPrimary.mTransitionController).isTransientHide(
+        doReturn(true).when(splitPrimary.mTransitionController).isTransientVisible(
                 organizer.mPrimary);
         // The split root and its top become visible.
         assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE,
diff --git a/services/tests/wmtests/src/com/android/server/wm/SafeActivityOptionsTest.java b/services/tests/wmtests/src/com/android/server/wm/SafeActivityOptionsTest.java
index 24e932f..6c48a69 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SafeActivityOptionsTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SafeActivityOptionsTest.java
@@ -16,17 +16,36 @@
 
 package com.android.server.wm;
 
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.mock;
 
 import android.app.ActivityOptions;
+import android.content.pm.ActivityInfo;
+import android.os.Looper;
 import android.platform.test.annotations.Presubmit;
+import android.view.RemoteAnimationAdapter;
+import android.window.RemoteTransition;
 import android.window.WindowContainerToken;
 
 import androidx.test.filters.MediumTest;
 
 import org.junit.Test;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
 
 /**
  * Build/Install/Run:
@@ -73,4 +92,119 @@
 
         assertSame(clone.getOriginalOptions().getLaunchRootTask(), token);
     }
+
+    @Test
+    public void test_getOptions() {
+        // Mock everything necessary
+        MockitoSession mockingSession = mockitoSession()
+                .mockStatic(ActivityTaskManagerService.class)
+                .strictness(Strictness.LENIENT)
+                .startMocking();
+        doReturn(PERMISSION_DENIED).when(() -> ActivityTaskManagerService.checkPermission(
+                any(), anyInt(), anyInt()));
+
+        final LockTaskController lockTaskController = mock(LockTaskController.class);
+        doReturn(false).when(lockTaskController).isPackageAllowlisted(anyInt(), any());
+
+        final ActivityTaskManagerService atm = mock(ActivityTaskManagerService.class);
+        doReturn(lockTaskController).when(atm).getLockTaskController();
+
+        final ActivityTaskSupervisor taskSupervisor =
+                new ActivityTaskSupervisor(atm, mock(Looper.class));
+        spyOn(taskSupervisor);
+        doReturn(false).when(taskSupervisor).isCallerAllowedToLaunchOnDisplay(anyInt(),
+                anyInt(), anyInt(), any());
+        doReturn(false).when(taskSupervisor).isCallerAllowedToLaunchOnTaskDisplayArea(anyInt(),
+                anyInt(), any(), any());
+
+        taskSupervisor.mRecentTasks = mock(RecentTasks.class);
+        doReturn(false).when(taskSupervisor.mRecentTasks).isCallerRecents(anyInt());
+
+        // Ensure exceptions are thrown when lack of permissions.
+        ActivityOptions activityOptions = ActivityOptions.makeBasic();
+        try {
+            activityOptions.setLaunchTaskId(100);
+            verifySecureExceptionThrown(activityOptions, taskSupervisor);
+
+            activityOptions = ActivityOptions.makeBasic();
+            activityOptions.setDisableStartingWindow(true);
+            verifySecureExceptionThrown(activityOptions, taskSupervisor);
+
+            activityOptions = ActivityOptions.makeBasic();
+            activityOptions.setTransientLaunch();
+            verifySecureExceptionThrown(activityOptions, taskSupervisor);
+
+            activityOptions = ActivityOptions.makeBasic();
+            activityOptions.setDismissKeyguard();
+            verifySecureExceptionThrown(activityOptions, taskSupervisor);
+
+            activityOptions = ActivityOptions.makeBasic();
+            activityOptions.setLaunchActivityType(ACTIVITY_TYPE_STANDARD);
+            verifySecureExceptionThrown(activityOptions, taskSupervisor);
+
+            activityOptions = ActivityOptions.makeBasic();
+            activityOptions.setLaunchedFromBubble(true);
+            verifySecureExceptionThrown(activityOptions, taskSupervisor);
+
+            activityOptions = ActivityOptions.makeBasic();
+            activityOptions.setLaunchDisplayId(DEFAULT_DISPLAY);
+            verifySecureExceptionThrown(activityOptions, taskSupervisor);
+
+            activityOptions = ActivityOptions.makeBasic();
+            activityOptions.setLockTaskEnabled(true);
+            verifySecureExceptionThrown(activityOptions, taskSupervisor);
+
+            activityOptions = ActivityOptions.makeCustomTaskAnimation(
+                    getInstrumentation().getContext(), 0, 0, null, null, null);
+            verifySecureExceptionThrown(activityOptions, taskSupervisor);
+
+            RemoteAnimationAdapter remoteAnimationAdapter = mock(RemoteAnimationAdapter.class);
+            RemoteTransition remoteTransition = mock(RemoteTransition.class);
+            activityOptions = ActivityOptions.makeRemoteAnimation(remoteAnimationAdapter);
+            verifySecureExceptionThrown(activityOptions, taskSupervisor);
+
+            activityOptions = ActivityOptions.makeRemoteAnimation(remoteAnimationAdapter,
+                    remoteTransition);
+            verifySecureExceptionThrown(activityOptions, taskSupervisor);
+
+            activityOptions = ActivityOptions.makeBasic();
+            activityOptions.setRemoteAnimationAdapter(remoteAnimationAdapter);
+            verifySecureExceptionThrown(activityOptions, taskSupervisor);
+
+            activityOptions = ActivityOptions.makeRemoteTransition(remoteTransition);
+            verifySecureExceptionThrown(activityOptions, taskSupervisor);
+
+            activityOptions = ActivityOptions.makeBasic();
+            activityOptions.setRemoteTransition(remoteTransition);
+            verifySecureExceptionThrown(activityOptions, taskSupervisor);
+
+            verifySecureExceptionThrown(activityOptions, taskSupervisor,
+                    mock(TaskDisplayArea.class));
+        } finally {
+            mockingSession.finishMocking();
+        }
+    }
+
+    private void verifySecureExceptionThrown(ActivityOptions activityOptions,
+            ActivityTaskSupervisor taskSupervisor) {
+        verifySecureExceptionThrown(activityOptions, taskSupervisor, null /* mockTda */);
+    }
+
+    private void verifySecureExceptionThrown(ActivityOptions activityOptions,
+            ActivityTaskSupervisor taskSupervisor, TaskDisplayArea mockTda) {
+        SafeActivityOptions safeActivityOptions = new SafeActivityOptions(activityOptions);
+        if (mockTda != null) {
+            spyOn(safeActivityOptions);
+            doReturn(mockTda).when(safeActivityOptions).getLaunchTaskDisplayArea(any(), any());
+        }
+
+        boolean isExceptionThrow = false;
+        final ActivityInfo aInfo = mock(ActivityInfo.class);
+        try {
+            safeActivityOptions.getOptions(null, aInfo, null, taskSupervisor);
+        } catch (SecurityException ex) {
+            isExceptionThrow = true;
+        }
+        assertTrue(isExceptionThrow);
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 3908947..d5afe3b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -27,6 +27,11 @@
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_16_9;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_3_2;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_4_3;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_DISPLAY_SIZE;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_SPLIT_SCREEN;
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 import static android.provider.DeviceConfig.NAMESPACE_CONSTRAIN_DISPLAY_APIS;
@@ -91,9 +96,12 @@
 import android.content.ComponentName;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ActivityInfo.ScreenOrientation;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
 import android.content.res.Configuration;
 import android.graphics.Rect;
 import android.os.Binder;
+import android.os.RemoteException;
 import android.os.UserHandle;
 import android.platform.test.annotations.Presubmit;
 import android.provider.DeviceConfig;
@@ -2255,6 +2263,169 @@
     }
 
     @Test
+    public void testUserOverrideSplitScreenAspectRatioForLandscapeDisplay() {
+        final int displayWidth = 1600;
+        final int displayHeight = 1400;
+        setUpDisplaySizeWithApp(displayWidth, displayHeight);
+
+        float expectedAspectRatio = 1f * displayHeight / getExpectedSplitSize(displayWidth);
+
+        testUserOverrideAspectRatio(expectedAspectRatio, USER_MIN_ASPECT_RATIO_SPLIT_SCREEN);
+    }
+
+    @Test
+    public void testUserOverrideSplitScreenAspectRatioForPortraitDisplay() {
+        final int displayWidth = 1400;
+        final int displayHeight = 1600;
+        setUpDisplaySizeWithApp(displayWidth, displayHeight);
+
+        float expectedAspectRatio = 1f * displayWidth / getExpectedSplitSize(displayHeight);
+
+        testUserOverrideAspectRatio(expectedAspectRatio, USER_MIN_ASPECT_RATIO_SPLIT_SCREEN);
+    }
+
+    @Test
+    public void testUserOverrideDisplaySizeAspectRatioForLandscapeDisplay() {
+        final int displayWidth = 1600;
+        final int displayHeight = 1400;
+        setUpDisplaySizeWithApp(displayWidth, displayHeight);
+
+        float expectedAspectRatio = 1f * displayWidth / displayHeight;
+
+        testUserOverrideAspectRatio(expectedAspectRatio, USER_MIN_ASPECT_RATIO_DISPLAY_SIZE);
+    }
+
+    @Test
+    public void testUserOverrideDisplaySizeAspectRatioForPortraitDisplay() {
+        final int displayWidth = 1400;
+        final int displayHeight = 1600;
+        setUpDisplaySizeWithApp(displayWidth, displayHeight);
+
+        float expectedAspectRatio = 1f * displayHeight / displayWidth;
+
+        testUserOverrideAspectRatio(expectedAspectRatio, USER_MIN_ASPECT_RATIO_DISPLAY_SIZE);
+    }
+
+    @Test
+    public void testUserOverride32AspectRatioForPortraitDisplay() {
+        setUpDisplaySizeWithApp(/* dw */ 1400, /* dh */ 1600);
+        testUserOverrideAspectRatio(3 / 2f, USER_MIN_ASPECT_RATIO_3_2);
+    }
+
+    @Test
+    public void testUserOverride32AspectRatioForLandscapeDisplay() {
+        setUpDisplaySizeWithApp(/* dw */ 1600, /* dh */ 1400);
+        testUserOverrideAspectRatio(3 / 2f, USER_MIN_ASPECT_RATIO_3_2);
+    }
+
+    @Test
+    public void testUserOverride43AspectRatioForPortraitDisplay() {
+        setUpDisplaySizeWithApp(/* dw */ 1400, /* dh */ 1600);
+        testUserOverrideAspectRatio(4 / 3f, USER_MIN_ASPECT_RATIO_4_3);
+    }
+
+    @Test
+    public void testUserOverride43AspectRatioForLandscapeDisplay() {
+        setUpDisplaySizeWithApp(/* dw */ 1600, /* dh */ 1400);
+        testUserOverrideAspectRatio(4 / 3f, USER_MIN_ASPECT_RATIO_4_3);
+    }
+
+    @Test
+    public void testUserOverride169AspectRatioForPortraitDisplay() {
+        setUpDisplaySizeWithApp(/* dw */ 1800, /* dh */ 1500);
+        testUserOverrideAspectRatio(16 / 9f, USER_MIN_ASPECT_RATIO_16_9);
+    }
+
+    @Test
+    public void testUserOverride169AspectRatioForLandscapeDisplay() {
+        setUpDisplaySizeWithApp(/* dw */ 1500, /* dh */ 1800);
+        testUserOverrideAspectRatio(16 / 9f, USER_MIN_ASPECT_RATIO_16_9);
+    }
+
+    @Test
+    @EnableCompatChanges({ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO,
+            ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_LARGE})
+    public void testUserOverrideAspectRatioOverSystemOverride() {
+        setUpDisplaySizeWithApp(/* dw */ 1600, /* dh */ 1400);
+
+        testUserOverrideAspectRatio(false,
+                SCREEN_ORIENTATION_PORTRAIT,
+                3 / 2f,
+                USER_MIN_ASPECT_RATIO_3_2,
+                true);
+    }
+
+    @Test
+    public void testUserOverrideAspectRatioNotEnabled() {
+        setUpDisplaySizeWithApp(/* dw */ 1600, /* dh */ 1400);
+
+        // App aspect ratio doesn't change
+        testUserOverrideAspectRatio(false,
+                SCREEN_ORIENTATION_PORTRAIT,
+                1f * 1600 / 1400,
+                USER_MIN_ASPECT_RATIO_3_2,
+                false);
+    }
+
+    private void testUserOverrideAspectRatio(float expectedAspectRatio,
+            @PackageManager.UserMinAspectRatio int aspectRatio) {
+        testUserOverrideAspectRatio(true,
+                SCREEN_ORIENTATION_PORTRAIT,
+                expectedAspectRatio,
+                aspectRatio,
+                true);
+
+        testUserOverrideAspectRatio(false,
+                SCREEN_ORIENTATION_PORTRAIT,
+                expectedAspectRatio,
+                aspectRatio,
+                true);
+
+        testUserOverrideAspectRatio(true,
+                SCREEN_ORIENTATION_LANDSCAPE,
+                expectedAspectRatio,
+                aspectRatio,
+                true);
+
+        testUserOverrideAspectRatio(false,
+                SCREEN_ORIENTATION_LANDSCAPE,
+                expectedAspectRatio,
+                aspectRatio,
+                true);
+    }
+
+    private void testUserOverrideAspectRatio(boolean isUnresizable, int screenOrientation,
+            float expectedAspectRatio, @PackageManager.UserMinAspectRatio int aspectRatio,
+            boolean enabled) {
+        final ActivityRecord activity = new ActivityBuilder(mAtm)
+                .setTask(mTask)
+                .setComponent(ComponentName.createRelative(mContext,
+                        SizeCompatTests.class.getName()))
+                .setUid(android.os.Process.myUid())
+                .build();
+        activity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        activity.mWmService.mLetterboxConfiguration
+                .setUserAppAspectRatioSettingsOverrideEnabled(enabled);
+        // Set user aspect ratio override
+        final IPackageManager pm = mAtm.getPackageManager();
+        try {
+            doReturn(aspectRatio).when(pm)
+                    .getUserMinAspectRatio(activity.packageName, activity.mUserId);
+        } catch (RemoteException ignored) {
+        }
+
+        prepareLimitedBounds(activity, screenOrientation, isUnresizable);
+
+        final Rect afterBounds = activity.getBounds();
+        final int width = afterBounds.width();
+        final int height = afterBounds.height();
+        final float afterAspectRatio =
+                (float) Math.max(width, height) / (float) Math.min(width, height);
+
+        assertEquals(expectedAspectRatio, afterAspectRatio, 0.001f);
+    }
+
+    @Test
     @EnableCompatChanges({ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO,
             ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_TO_ALIGN_WITH_SPLIT_SCREEN})
     public void testOverrideSplitScreenAspectRatioForUnresizablePortraitApps() {
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 7351e81..5154d17 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -61,6 +61,7 @@
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
@@ -1429,6 +1430,15 @@
         // No need to wait for the activity in transient hide task.
         assertEquals(WindowContainer.SYNC_STATE_NONE, activity1.mSyncState);
 
+        // An active transient launch overrides idle state to avoid clearing power mode before the
+        // transition is finished.
+        spyOn(mRootWindowContainer.mTransitionController);
+        doAnswer(invocation -> controller.isTransientLaunch(invocation.getArgument(0))).when(
+                mRootWindowContainer.mTransitionController).isTransientLaunch(any());
+        activity2.getTask().setResumedActivity(activity2, "test");
+        activity2.idle = true;
+        assertFalse(mRootWindowContainer.allResumedActivitiesIdle());
+
         activity1.setVisibleRequested(false);
         activity2.setVisibleRequested(true);
         activity2.setVisible(true);
@@ -1478,6 +1488,47 @@
     }
 
     @Test
+    public void testIsTransientVisible() {
+        final ActivityRecord appB = new ActivityBuilder(mAtm).setCreateTask(true)
+                .setVisible(false).build();
+        final ActivityRecord recent = new ActivityBuilder(mAtm).setCreateTask(true)
+                .setVisible(false).build();
+        final ActivityRecord appA = new ActivityBuilder(mAtm).setCreateTask(true).build();
+        final Task taskA = appA.getTask();
+        final Task taskB = appB.getTask();
+        final Task taskRecent = recent.getTask();
+        registerTestTransitionPlayer();
+        final TransitionController controller = mRootWindowContainer.mTransitionController;
+        final Transition transition = createTestTransition(TRANSIT_OPEN, controller);
+        controller.moveToCollecting(transition);
+        transition.collect(recent);
+        transition.collect(taskA);
+        transition.setTransientLaunch(recent, taskA);
+        taskRecent.moveToFront("move-recent-to-front");
+
+        // During collecting and playing, the recent is on top so it is visible naturally.
+        // While B needs isTransientVisible to keep visibility because it is occluded by recents.
+        assertFalse(controller.isTransientVisible(taskB));
+        assertTrue(controller.isTransientVisible(taskA));
+        assertFalse(controller.isTransientVisible(taskRecent));
+        // Switch to playing state.
+        transition.onTransactionReady(transition.getSyncId(), mMockT);
+        assertTrue(controller.isTransientVisible(taskA));
+
+        // Switch to another task. For example, use gesture navigation to switch tasks.
+        taskB.moveToFront("move-b-to-front");
+        // The previous app (taskA) should be paused first so it loses transient visible. Because
+        // visually it is taskA -> taskB, the pause -> resume order should be the same.
+        assertFalse(controller.isTransientVisible(taskA));
+        // Keep the recent visible so there won't be 2 activities pausing at the same time. It is
+        // to avoid the latency to resume the current top, i.e. appB.
+        assertTrue(controller.isTransientVisible(taskRecent));
+        // The recent is paused after the transient transition is finished.
+        controller.finishTransition(transition);
+        assertFalse(controller.isTransientVisible(taskRecent));
+    }
+
+    @Test
     public void testNotReadyPushPop() {
         final TransitionController controller = new TestTransitionController(mAtm);
         controller.setSyncEngine(mWm.mSyncEngine);
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index 64e4356..c0f4008 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -35,6 +35,9 @@
 import android.annotation.SystemService;
 import android.app.PendingIntent;
 import android.app.PropertyInvalidatedCache;
+import android.compat.Compatibility;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.content.Intent;
@@ -85,6 +88,7 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
@@ -1322,41 +1326,61 @@
      * for #onSubscriptionsChanged to be invoked.
      */
     public static class OnSubscriptionsChangedListener {
-        private class OnSubscriptionsChangedListenerHandler extends Handler {
-            OnSubscriptionsChangedListenerHandler() {
-                super();
-            }
-
-            OnSubscriptionsChangedListenerHandler(Looper looper) {
-                super(looper);
-            }
-        }
 
         /**
-         * Posted executor callback on the handler associated with a given looper.
-         * The looper can be the calling thread's looper or the looper passed from the
-         * constructor {@link #OnSubscriptionsChangedListener(Looper)}.
+         * After {@link Build.VERSION_CODES.Q}, it is no longer necessary to instantiate a
+         * Handler inside of the OnSubscriptionsChangedListener in all cases, so it will only
+         * be done for callers that do not supply an Executor.
          */
-        private final HandlerExecutor mExecutor;
+        @ChangeId
+        @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q)
+        private static final long LAZY_INITIALIZE_SUBSCRIPTIONS_CHANGED_HANDLER = 278814050L;
+
+        /**
+         * For backwards compatibility reasons, stashes the Looper associated with the thread
+         * context in which this listener was created.
+         */
+        private final Looper mCreatorLooper;
 
         /**
          * @hide
          */
-        public HandlerExecutor getHandlerExecutor() {
-            return mExecutor;
+        public Looper getCreatorLooper() {
+            return mCreatorLooper;
         }
 
+        /**
+         * Create an OnSubscriptionsChangedListener.
+         *
+         * For callers targeting {@link Build.VERSION_CODES.P} or earlier, this can only be called
+         * on a thread that already has a prepared Looper. Callers targeting Q or later should
+         * subsequently use {@link SubscriptionManager#addOnSubscriptionsChangedListener(
+         * Executor, OnSubscriptionsChangedListener)}.
+         *
+         * On OS versions prior to {@link Build.VERSION_CODES.V} callers should assume that this
+         * call will fail if invoked on a thread that does not already have a prepared looper.
+         */
         public OnSubscriptionsChangedListener() {
-            mExecutor = new HandlerExecutor(new OnSubscriptionsChangedListenerHandler());
+            mCreatorLooper = Looper.myLooper();
+            if (mCreatorLooper == null
+                    && !Compatibility.isChangeEnabled(
+                            LAZY_INITIALIZE_SUBSCRIPTIONS_CHANGED_HANDLER)) {
+                // matches the implementation of Handler
+                throw new RuntimeException(
+                        "Can't create handler inside thread "
+                        + Thread.currentThread()
+                        + " that has not called Looper.prepare()");
+            }
         }
 
         /**
          * Allow a listener to be created with a custom looper
-         * @param looper the looper that the underlining handler should run on
+         * @param looper the non-null Looper that the underlining handler should run on
          * @hide
          */
-        public OnSubscriptionsChangedListener(Looper looper) {
-            mExecutor = new HandlerExecutor(new OnSubscriptionsChangedListenerHandler(looper));
+        public OnSubscriptionsChangedListener(@NonNull Looper looper) {
+            Objects.requireNonNull(looper);
+            mCreatorLooper = looper;
         }
 
         /**
@@ -1423,7 +1447,9 @@
     @Deprecated
     public void addOnSubscriptionsChangedListener(OnSubscriptionsChangedListener listener) {
         if (listener == null) return;
-        addOnSubscriptionsChangedListener(listener.mExecutor, listener);
+
+        addOnSubscriptionsChangedListener(
+                new HandlerExecutor(new Handler(listener.getCreatorLooper())), listener);
     }
 
     /**
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index efcbd31..f1ee76e 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -5268,7 +5268,11 @@
         try {
             ITelephony telephony = getITelephony();
             if (telephony != null) {
-                return telephony.getMergedImsisFromGroup(getSubId(), getOpPackageName());
+                String[] mergedImsisFromGroup =
+                        telephony.getMergedImsisFromGroup(getSubId(), getOpPackageName());
+                if (mergedImsisFromGroup != null) {
+                    return mergedImsisFromGroup;
+                }
             }
         } catch (RemoteException ex) {
         }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt
index 144a7312..6722cc8 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt
@@ -62,14 +62,6 @@
         }
     }
 
-    /** Assert the background animation layer is never visible during bounds change transition. */
-    @Presubmit
-    @Test
-    fun backgroundLayerNeverVisible() {
-        val backgroundColorLayer = ComponentNameMatcher("", "Animation Background")
-        flicker.assertLayers { isInvisible(backgroundColorLayer) }
-    }
-
     /** Trampoline activity should finish itself before the end of this test. */
     @Presubmit
     @Test
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/pip/SecondaryActivityEnterPipTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/pip/SecondaryActivityEnterPipTest.kt
new file mode 100644
index 0000000..0417f9d
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/pip/SecondaryActivityEnterPipTest.kt
@@ -0,0 +1,188 @@
+/*
+ * 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.wm.flicker.activityembedding
+
+import android.platform.test.annotations.Presubmit
+import android.tools.common.datatypes.Rect
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.common.traces.component.ComponentNameMatcher.Companion.TRANSITION_SNAPSHOT
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test launching a secondary Activity into Picture-In-Picture mode.
+ *
+ * Setup: Start from a split A|B.
+ * Transition: B enters PIP, observe the window shrink to the bottom right corner on screen.
+ *
+ * To run this test: `atest FlickerTests:SecondaryActivityEnterPipTest`
+ *
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class SecondaryActivityEnterPipTest (flicker: LegacyFlickerTest) :
+        ActivityEmbeddingTestBase(flicker) {
+    override val transition: FlickerBuilder.() -> Unit = {
+        setup {
+            tapl.setExpectedRotationCheckEnabled(false)
+            testApp.launchViaIntent(wmHelper)
+            testApp.launchSecondaryActivity(wmHelper)
+            startDisplayBounds =
+                    wmHelper.currentState.layerState.physicalDisplayBounds
+                            ?: error("Can't get display bounds")
+        }
+        transitions {
+            testApp.secondaryActivityEnterPip(wmHelper)
+        }
+        teardown {
+            tapl.goHome()
+            testApp.exit(wmHelper)
+        }
+    }
+
+    /**
+     * Main and secondary activity start from a split each taking half of the screen.
+     */
+    @Presubmit
+    @Test
+    fun layersStartFromEqualSplit() {
+        flicker.assertLayersStart {
+            val leftLayerRegion =
+                    visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+            val rightLayerRegion =
+                    visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+            // Compare dimensions of two splits, given we're using default split attributes,
+            // both activities take up the same visible size on the display.
+            check { "height" }
+                    .that(leftLayerRegion.region.height).isEqual(rightLayerRegion.region.height)
+            check { "width" }
+                    .that(leftLayerRegion.region.width).isEqual(rightLayerRegion.region.width)
+            leftLayerRegion.notOverlaps(rightLayerRegion.region)
+            leftLayerRegion.plus(rightLayerRegion.region).coversExactly(startDisplayBounds)
+        }
+        flicker.assertLayersEnd {
+            visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+                    .coversExactly(startDisplayBounds)
+        }
+    }
+
+    /**
+     * Main Activity is visible throughout the transition and becomes fullscreen.
+     */
+    @Presubmit
+    @Test
+    fun mainActivityWindowBecomesFullScreen() {
+        flicker.assertWm { isAppWindowVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) }
+        flicker.assertWmEnd {
+            visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+                    .coversExactly(startDisplayBounds)
+        }
+    }
+
+    /**
+     * Main Activity is visible throughout the transition and becomes fullscreen.
+     */
+    @Presubmit
+    @Test
+    fun mainActivityLayerBecomesFullScreen() {
+        flicker.assertLayers {
+            isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+                    .then()
+                    .isVisible(TRANSITION_SNAPSHOT)
+                    .isInvisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+                    .then()
+                    .isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+        }
+        flicker.assertLayersEnd {
+            visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+                    .coversExactly(startDisplayBounds)
+        }
+    }
+
+    /**
+     * Secondary Activity is visible throughout the transition and shrinks to the bottom right
+     * corner.
+     */
+    @Presubmit
+    @Test
+    fun secondaryWindowShrinks() {
+        flicker.assertWm {
+            isAppWindowVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+        }
+        flicker.assertWmEnd {
+            val pipWindowRegion =
+                    visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+            check{"height"}
+                    .that(pipWindowRegion.region.height)
+                    .isLower(startDisplayBounds.height / 2)
+            check{"width"}
+                    .that(pipWindowRegion.region.width).isLower(startDisplayBounds.width)
+        }
+    }
+
+    /**
+     * During the transition Secondary Activity shrinks to the bottom right corner.
+     */
+    @Presubmit
+    @Test
+    fun secondaryLayerShrinks() {
+        flicker.assertLayers {
+            val pipLayerList = layers {
+                ComponentNameMatcher.PIP_CONTENT_OVERLAY.layerMatchesAnyOf(it) && it.isVisible
+            }
+            pipLayerList.zipWithNext { previous, current ->
+                // TODO(b/290987990): Add checks for visibleRegion.
+                current.screenBounds.isToTheRightBottom(previous.screenBounds.region, 3)
+                current.screenBounds.notBiggerThan(previous.screenBounds.region)
+            }
+        }
+        flicker.assertLayersEnd {
+            val pipRegion = visibleRegion(
+                    ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+            check { "height" }
+                    .that(pipRegion.region.height)
+                    .isLower(startDisplayBounds.height / 2)
+            check { "width" }
+                    .that(pipRegion.region.width).isLower(startDisplayBounds.width)
+        }
+    }
+
+    companion object {
+        /** {@inheritDoc} */
+        private var startDisplayBounds = Rect.EMPTY
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+    }
+}
\ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
index a72ec1e..2d3bc2d 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
@@ -113,6 +113,21 @@
             .waitForAndVerify()
     }
 
+    fun secondaryActivityEnterPip(wmHelper: WindowManagerStateHelper) {
+        val pipButton =
+                uiDevice.wait(
+                        Until.findObject(By.res(getPackage(), "secondary_enter_pip_button")),
+                        FIND_TIMEOUT
+                )
+        require(pipButton != null) { "Can't find enter pip button on screen." }
+        pipButton.click()
+        wmHelper
+                .StateSyncBuilder()
+                .withAppTransitionIdle()
+                .withPipShown()
+                .waitForAndVerify()
+    }
+
     /**
      * Clicks the button to launch a secondary activity with alwaysExpand enabled, which will launch
      * a fullscreen window on top of the visible region.
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt
index c1db423..ea7c7c4 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt
@@ -25,6 +25,7 @@
 import android.tools.device.flicker.legacy.FlickerBuilder
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import androidx.test.filters.FlakyTest
 import com.android.server.wm.flicker.BaseTest
 import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper
 import com.android.server.wm.flicker.helpers.setRotation
@@ -78,6 +79,7 @@
         flicker.snapshotStartingWindowLayerCoversExactlyOnApp(imeTestApp)
     }
 
+    @FlakyTest(bugId = 290767483)
     @Postsubmit
     @Test
     fun imeLayerAlphaOneAfterSnapshotStartingWindowRemoval() {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt
index 7a16060..94b090f 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt
@@ -200,7 +200,7 @@
     override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
         super.visibleWindowsShownMoreThanOneConsecutiveEntry()
 
-    @FlakyTest(bugId = 251217585)
+    @FlakyTest(bugId = 285980483)
     @Test
     override fun focusChanges() {
         super.focusChanges()
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt
index 4164c0d..df9780e 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt
@@ -21,6 +21,7 @@
 import android.content.res.Resources
 import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.Presubmit
+import android.tools.common.flicker.subject.layers.LayersTraceSubject.Companion.VISIBLE_FOR_MORE_THAN_ONE_ENTRY_IGNORE_LAYERS
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.common.traces.component.ComponentNameMatcher.Companion.SPLASH_SCREEN
 import android.tools.common.traces.component.ComponentNameMatcher.Companion.WALLPAPER_BBQ_WRAPPER
@@ -190,6 +191,16 @@
         }
     }
 
+    @Presubmit
+    @Test
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
+        flicker.assertLayers {
+            this.visibleLayersShownMoreThanOneConsecutiveEntry(
+                VISIBLE_FOR_MORE_THAN_ONE_ENTRY_IGNORE_LAYERS + listOf(launchNewTaskApp)
+            )
+        }
+    }
+
     companion object {
         private fun getWallpaperPackage(instrumentation: Instrumentation): IComponentMatcher {
             val wallpaperManager = WallpaperManager.getInstance(instrumentation.targetContext)
diff --git a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
index 68ae806..ff9799a 100644
--- a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
@@ -102,6 +102,24 @@
                 <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
+        <activity android:name=".LaunchTransparentActivity"
+                  android:resizeableActivity="false"
+                  android:screenOrientation="portrait"
+                  android:theme="@android:style/Theme"
+                  android:taskAffinity="com.android.server.wm.flicker.testapp.LaunchTransparentActivity"
+                  android:label="LaunchTransparentActivity"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+        <activity android:name=".TransparentActivity"
+                  android:theme="@style/TransparentTheme"
+                  android:taskAffinity="com.android.server.wm.flicker.testapp.TransparentActivity"
+                  android:label="TransparentActivity"
+                  android:exported="false">
+        </activity>
         <activity android:name=".LaunchNewActivity"
                   android:taskAffinity="com.android.server.wm.flicker.testapp.LaunchNewActivity"
                   android:theme="@style/CutoutShortEdges"
@@ -206,6 +224,7 @@
             android:taskAffinity="com.android.server.wm.flicker.testapp.ActivityEmbedding"
             android:theme="@style/CutoutShortEdges"
             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
+            android:supportsPictureInPicture="true"
             android:exported="false"/>
         <activity
             android:name=".ActivityEmbeddingThirdActivity"
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml
index 6731446..135140a 100644
--- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml
@@ -35,4 +35,10 @@
       android:onClick="launchThirdActivity"
       android:text="Launch a third activity" />
 
+  <Button
+      android:id="@+id/secondary_enter_pip_button"
+      android:layout_width="wrap_content"
+      android:layout_height="48dp"
+      android:text="Enter pip" />
+
 </LinearLayout>
\ No newline at end of file
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_transparent.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_transparent.xml
new file mode 100644
index 0000000..0730ded
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_transparent.xml
@@ -0,0 +1,21 @@
+<?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.
+  -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+</FrameLayout>
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_transparent_launch.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_transparent_launch.xml
new file mode 100644
index 0000000..ff4ead9
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_transparent_launch.xml
@@ -0,0 +1,37 @@
+<?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.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:background="@android:color/black">
+
+        <Button
+            android:id="@+id/button_launch_transparent"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerHorizontal="true"
+            android:layout_centerVertical="true"
+            android:text="Launch Transparent" />
+        <Button
+            android:id="@+id/button_request_permission"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerHorizontal="true"
+            android:layout_centerVertical="true"
+            android:text="Request Permission" />
+</LinearLayout>
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/values/styles.xml b/tests/FlickerTests/test-apps/flickerapp/res/values/styles.xml
index 1d21fd5..e51ed29 100644
--- a/tests/FlickerTests/test-apps/flickerapp/res/values/styles.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/res/values/styles.xml
@@ -43,6 +43,13 @@
         <item name="android:windowSoftInputMode">stateUnchanged</item>
     </style>
 
+    <style name="TransparentTheme" parent="@android:style/Theme.DeviceDefault">
+        <item name="android:windowIsTranslucent">true</item>
+        <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:windowContentOverlay">@null</item>
+        <item name="android:backgroundDimEnabled">false</item>
+    </style>
+
     <style name="no_starting_window" parent="@android:style/Theme.DeviceDefault">
         <item name="android:windowDisablePreview">true</item>
     </style>
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
index dc21027..ee087ef 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
@@ -18,6 +18,7 @@
 
 import android.app.Activity;
 import android.content.Intent;
+import android.app.PictureInPictureParams;
 import android.graphics.Color;
 import android.os.Bundle;
 import android.view.View;
@@ -40,6 +41,16 @@
                         finish();
                     }
             });
+        findViewById(R.id.secondary_enter_pip_button).setOnClickListener(
+                new View.OnClickListener() {
+                    @Override
+                    public void onClick(View v) {
+                        PictureInPictureParams.Builder picInPicParamsBuilder =
+                                new PictureInPictureParams.Builder();
+                        enterPictureInPictureMode(picInPicParamsBuilder.build());
+                    }
+                }
+        );
     }
 
     public void launchThirdActivity(View view) {
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
index 95c86ac..2795a6c 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
@@ -73,6 +73,18 @@
                 FLICKER_APP_PACKAGE + ".NonResizeablePortraitActivity");
     }
 
+    public static class TransparentActivity {
+        public static final String LABEL = "TransparentActivity";
+        public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE,
+                FLICKER_APP_PACKAGE + ".TransparentActivity");
+    }
+
+    public static class LaunchTransparentActivity {
+        public static final String LABEL = "LaunchTransparentActivity";
+        public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE,
+                FLICKER_APP_PACKAGE + ".LaunchTransparentActivity");
+    }
+
     public static class DialogThemedActivity {
         public static final String LABEL = "DialogThemedActivity";
         public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE,
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/LaunchTransparentActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/LaunchTransparentActivity.java
new file mode 100644
index 0000000..7c161fd
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/LaunchTransparentActivity.java
@@ -0,0 +1,36 @@
+/*
+ * 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.wm.flicker.testapp;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+
+public class LaunchTransparentActivity extends Activity {
+
+    @Override
+    protected void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        setContentView(R.layout.activity_transparent_launch);
+        findViewById(R.id.button_launch_transparent)
+                .setOnClickListener(v -> launchTransparentActivity());
+    }
+
+    private void launchTransparentActivity() {
+        startActivity(new Intent(this, TransparentActivity.class));
+    }
+}
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/TransparentActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/TransparentActivity.java
new file mode 100644
index 0000000..1bac8bd
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/TransparentActivity.java
@@ -0,0 +1,29 @@
+/*
+ * 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.wm.flicker.testapp;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class TransparentActivity extends Activity {
+
+    @Override
+    protected void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        setContentView(R.layout.activity_transparent);
+    }
+}
diff --git a/tests/Internal/src/android/service/wallpaper/OWNERS b/tests/Internal/src/android/service/wallpaper/OWNERS
new file mode 100644
index 0000000..5a26d0e
--- /dev/null
+++ b/tests/Internal/src/android/service/wallpaper/OWNERS
@@ -0,0 +1,4 @@
+dupin@google.com
+santie@google.com
+pomini@google.com
+poultney@google.com
\ No newline at end of file
diff --git a/tests/Internal/src/android/service/wallpaper/WallpaperServiceTest.java b/tests/Internal/src/android/service/wallpaper/WallpaperServiceTest.java
index 153ca79..0c5e8d48 100644
--- a/tests/Internal/src/android/service/wallpaper/WallpaperServiceTest.java
+++ b/tests/Internal/src/android/service/wallpaper/WallpaperServiceTest.java
@@ -85,4 +85,17 @@
         assertEquals("onAmbientModeChanged should have been called", 2, zoomChangedCount[0]);
     }
 
+    @Test
+    public void testNotifyColorsOfDestroyedEngine_doesntCrash() {
+        WallpaperService service = new WallpaperService() {
+            @Override
+            public Engine onCreateEngine() {
+                return new Engine();
+            }
+        };
+        WallpaperService.Engine engine = service.onCreateEngine();
+        engine.detach();
+
+        engine.notifyColorsChanged();
+    }
 }
diff --git a/tests/permission/OWNERS b/tests/permission/OWNERS
index 999ea0e..2fe78c5 100644
--- a/tests/permission/OWNERS
+++ b/tests/permission/OWNERS
@@ -1 +1,2 @@
+#Bug component: 137825
 include /core/java/android/permission/OWNERS
diff --git a/tests/testables/src/android/testing/TestableLooper.java b/tests/testables/src/android/testing/TestableLooper.java
index edd6dd3..82e40b1 100644
--- a/tests/testables/src/android/testing/TestableLooper.java
+++ b/tests/testables/src/android/testing/TestableLooper.java
@@ -32,6 +32,7 @@
 import java.lang.annotation.Target;
 import java.lang.reflect.Field;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * This is a wrapper around {@link TestLooperManager} to make it easier to manage
@@ -55,7 +56,6 @@
     private MessageHandler mMessageHandler;
 
     private Handler mHandler;
-    private Runnable mEmptyMessage;
     private TestLooperManager mQueueWrapper;
 
     static {
@@ -121,8 +121,12 @@
      * @param num Number of messages to parse
      */
     public int processMessages(int num) {
+        return processMessagesInternal(num, null);
+    }
+
+    private int processMessagesInternal(int num, Runnable barrierRunnable) {
         for (int i = 0; i < num; i++) {
-            if (!parseMessageInt()) {
+            if (!processSingleMessage(barrierRunnable)) {
                 return i + 1;
             }
         }
@@ -130,6 +134,27 @@
     }
 
     /**
+     * Process up to a certain number of messages, not blocking if the queue has less messages than
+     * that
+     * @param num the maximum number of messages to process
+     * @return the number of messages processed. This will be at most {@code num}.
+     */
+
+    public int processMessagesNonBlocking(int num) {
+        final AtomicBoolean reachedBarrier = new AtomicBoolean(false);
+        Runnable barrierRunnable = () -> {
+            reachedBarrier.set(true);
+        };
+        mHandler.post(barrierRunnable);
+        waitForMessage(mQueueWrapper, mHandler, barrierRunnable);
+        try {
+            return processMessagesInternal(num, barrierRunnable) + (reachedBarrier.get() ? -1 : 0);
+        } finally {
+            mHandler.removeCallbacks(barrierRunnable);
+        }
+    }
+
+    /**
      * Process messages in the queue until no more are found.
      */
     public void processAllMessages() {
@@ -165,19 +190,20 @@
 
     private int processQueuedMessages() {
         int count = 0;
-        mEmptyMessage = () -> { };
-        mHandler.post(mEmptyMessage);
-        waitForMessage(mQueueWrapper, mHandler, mEmptyMessage);
-        while (parseMessageInt()) count++;
+        Runnable barrierRunnable = () -> { };
+        mHandler.post(barrierRunnable);
+        waitForMessage(mQueueWrapper, mHandler, barrierRunnable);
+        while (processSingleMessage(barrierRunnable)) count++;
         return count;
     }
 
-    private boolean parseMessageInt() {
+    private boolean processSingleMessage(Runnable barrierRunnable) {
         try {
             Message result = mQueueWrapper.next();
             if (result != null) {
                 // This is a break message.
-                if (result.getCallback() == mEmptyMessage) {
+                if (result.getCallback() == barrierRunnable) {
+                    mQueueWrapper.execute(result);
                     mQueueWrapper.recycle(result);
                     return false;
                 }
diff --git a/tests/testables/tests/src/android/testing/TestableLooperTest.java b/tests/testables/tests/src/android/testing/TestableLooperTest.java
index 0f491b8..a02eb6b 100644
--- a/tests/testables/tests/src/android/testing/TestableLooperTest.java
+++ b/tests/testables/tests/src/android/testing/TestableLooperTest.java
@@ -27,12 +27,6 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.InOrder;
-
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
@@ -40,6 +34,11 @@
 import android.testing.TestableLooper.MessageHandler;
 import android.testing.TestableLooper.RunWithLooper;
 
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper
@@ -240,4 +239,33 @@
         inOrder.verify(handler).dispatchMessage(messageC);
     }
 
+    @Test
+    public void testProcessMessagesNonBlocking_onlyArgNumber() {
+        Handler h = new Handler(mTestableLooper.getLooper());
+        Runnable r = mock(Runnable.class);
+
+        h.post(r);
+        h.post(r);
+        h.post(r);
+
+        int processed = mTestableLooper.processMessagesNonBlocking(2);
+
+        verify(r, times(2)).run();
+        assertEquals(2, processed);
+    }
+
+    @Test
+    public void testProcessMessagesNonBlocking_lessMessagesThanArg() {
+        Handler h = new Handler(mTestableLooper.getLooper());
+        Runnable r = mock(Runnable.class);
+
+        h.post(r);
+        h.post(r);
+        h.post(r);
+
+        int processed = mTestableLooper.processMessagesNonBlocking(5);
+
+        verify(r, times(3)).run();
+        assertEquals(3, processed);
+    }
 }
diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/Constants.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/Constants.kt
index e03d92ab..f1727b7 100644
--- a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/Constants.kt
+++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/Constants.kt
@@ -30,6 +30,7 @@
 const val IINTERFACE_INTERFACE = "android.os.IInterface"
 
 const val AIDL_PERMISSION_HELPER_SUFFIX = "_enforcePermission"
+const val PERMISSION_PREFIX_LITERAL = "android.permission."
 
 /**
  * If a non java (e.g. c++) backend is enabled, the @EnforcePermission
diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt
index 0a66cd5..3a95df9 100644
--- a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt
+++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt
@@ -95,7 +95,7 @@
             val v1 = ConstantEvaluator.evaluate(context, value1)
             val v2 = ConstantEvaluator.evaluate(context, value2)
             if (v1 != null && v2 != null) {
-                if (v1 != v2) {
+                if (v1 != v2 && !isOneShortPermissionOfOther(v1, v2)) {
                     return false
                 }
             } else {
@@ -107,7 +107,7 @@
                 for (j in children1.indices) {
                     val c1 = ConstantEvaluator.evaluate(context, children1[j])
                     val c2 = ConstantEvaluator.evaluate(context, children2[j])
-                    if (c1 != c2) {
+                    if (c1 != c2 && !isOneShortPermissionOfOther(c1, c2)) {
                         return false
                     }
                 }
@@ -116,6 +116,12 @@
         return true
     }
 
+    private fun isOneShortPermissionOfOther(
+        permission1: Any?,
+        permission2: Any?
+    ): Boolean = permission1 == (permission2 as? String)?.removePrefix(PERMISSION_PREFIX_LITERAL) ||
+            permission2 == (permission1 as? String)?.removePrefix(PERMISSION_PREFIX_LITERAL)
+
     private fun compareMethods(
         context: JavaContext,
         element: UElement,
diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt
index 75b0073..b3dacbd 100644
--- a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt
+++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt
@@ -316,20 +316,55 @@
                 overrides the method Stub.testMethod which is annotated with @EnforcePermission. The same annotation must be used on Default.testMethod [MissingEnforcePermissionAnnotation]
                     public void testMethod() {}
                                 ~~~~~~~~~~
-                1 errors, 0 warnings 
+                1 errors, 0 warnings
                 """.addLineContinuation()
             )
     }
 
-    fun testDoesDetectIssuesShortStringsNotAllowed() {
+    fun testDoesNotDetectIssuesShortStringsAllowedInChildAndParent() {
         lint().files(java(
             """
             package test.pkg;
             import android.annotation.EnforcePermission;
             public class TestClass121 extends IFooMethod.Stub {
                 @Override
+                @EnforcePermission("READ_PHONE_STATE")
+                public void testMethod() {}
+                @Override
+                @EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
+                public void testMethodParentShortPermission() {}
+                @Override
                 @EnforcePermission(anyOf={"INTERNET", "READ_PHONE_STATE"})
                 public void testMethodAnyLiteral() {}
+                @Override
+                @EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE})
+                public void testMethodAnyLiteralParentsShortPermission() {}
+            }
+            """).indented(),
+            *stubs
+        )
+            .run()
+            .expectClean()
+    }
+
+    fun testDoesDetectIssuesWrongShortStringsInChildAndParent() {
+        lint().files(java(
+            """
+            package test.pkg;
+            import android.annotation.EnforcePermission;
+            public class TestClass121 extends IFooMethod.Stub {
+                @Override
+                @EnforcePermission("READ_WRONG_PHONE_STATE")
+                public void testMethod() {}
+                @Override
+                @EnforcePermission(android.Manifest.permission.READ_WRONG_PHONE_STATE)
+                public void testMethodParentShortPermission() {}
+                @Override
+                @EnforcePermission(anyOf={"WRONG_INTERNET", "READ_PHONE_STATE"})
+                public void testMethodAnyLiteral() {}
+                @Override
+                @EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_WRONG_PHONE_STATE})
+                public void testMethodAnyLiteralParentsShortPermission() {}
             }
             """).indented(),
             *stubs
@@ -337,14 +372,19 @@
             .run()
             .expect(
                 """
-                src/test/pkg/TestClass121.java:6: Error: The method \
-                TestClass121.testMethodAnyLiteral is annotated with @EnforcePermission(anyOf={"INTERNET", "READ_PHONE_STATE"}) \
-                which differs from the overridden method Stub.testMethodAnyLiteral: \
-                @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}). \
-                The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
-                    public void testMethodAnyLiteral() {}
-                                ~~~~~~~~~~~~~~~~~~~~
-                1 errors, 0 warnings
+            src/test/pkg/TestClass121.java:6: Error: The method TestClass121.testMethod is annotated with @EnforcePermission("READ_WRONG_PHONE_STATE") which differs from the overridden method Stub.testMethod: @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
+                public void testMethod() {}
+                            ~~~~~~~~~~
+            src/test/pkg/TestClass121.java:9: Error: The method TestClass121.testMethodParentShortPermission is annotated with @EnforcePermission(android.Manifest.permission.READ_WRONG_PHONE_STATE) which differs from the overridden method Stub.testMethodParentShortPermission: @android.annotation.EnforcePermission("READ_PHONE_STATE"). The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
+                public void testMethodParentShortPermission() {}
+                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+            src/test/pkg/TestClass121.java:12: Error: The method TestClass121.testMethodAnyLiteral is annotated with @EnforcePermission(anyOf={"WRONG_INTERNET", "READ_PHONE_STATE"}) which differs from the overridden method Stub.testMethodAnyLiteral: @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}). The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
+                public void testMethodAnyLiteral() {}
+                            ~~~~~~~~~~~~~~~~~~~~
+            src/test/pkg/TestClass121.java:15: Error: The method TestClass121.testMethodAnyLiteralParentsShortPermission is annotated with @EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_WRONG_PHONE_STATE}) which differs from the overridden method Stub.testMethodAnyLiteralParentsShortPermission: @android.annotation.EnforcePermission(anyOf={"INTERNET", "READ_PHONE_STATE"}). The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
+                public void testMethodAnyLiteralParentsShortPermission() {}
+                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+            4 errors, 0 warnings
                 """.addLineContinuation()
             )
     }
@@ -360,12 +400,18 @@
             @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
             public void testMethod() {}
             @Override
+            @android.annotation.EnforcePermission("READ_PHONE_STATE")
+            public void testMethodParentShortPermission() {}
+            @Override
             @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE})
             public void testMethodAny() {}
             @Override
             @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"})
             public void testMethodAnyLiteral() {}
             @Override
+            @android.annotation.EnforcePermission(anyOf={"INTERNET", "READ_PHONE_STATE"})
+            public void testMethodAnyLiteralParentsShortPermission() {}
+            @Override
             @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE})
             public void testMethodAll() {}
             @Override
@@ -374,10 +420,14 @@
           }
           @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
           public void testMethod();
+          @android.annotation.EnforcePermission("READ_PHONE_STATE")
+          public void testMethodParentShortPermission();
           @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE})
           public void testMethodAny() {}
           @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"})
           public void testMethodAnyLiteral() {}
+          @android.annotation.EnforcePermission(anyOf={"INTERNET", "READ_PHONE_STATE"})
+          public void testMethodAnyLiteralParentsShortPermission() {}
           @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE})
           public void testMethodAll() {}
           @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"})
@@ -404,6 +454,7 @@
         package android.Manifest;
         class permission {
           public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE";
+          public static final String READ_WRONG_PHONE_STATE = "android.permission.READ_WRONG_PHONE_STATE";
           public static final String NFC = "android.permission.NFC";
           public static final String INTERNET = "android.permission.INTERNET";
         }