Merge "Trim leading src-dir from Ravenwood sources" into main
diff --git a/Android.bp b/Android.bp
index b5f7e99..49256dd 100644
--- a/Android.bp
+++ b/Android.bp
@@ -631,6 +631,7 @@
     name: "android-non-updatable-stub-sources",
     srcs: [
         ":framework-mime-sources", // mimemap builds separately but has no separate droidstubs.
+        ":framework-minus-apex-aconfig-srcjars",
         ":framework-non-updatable-sources",
         ":opt-telephony-srcs",
         ":opt-net-voip-srcs",
diff --git a/api/Android.bp b/api/Android.bp
index 222275f..d11ea7b 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -74,7 +74,6 @@
         "framework-configinfrastructure",
         "framework-connectivity",
         "framework-connectivity-t",
-        "framework-crashrecovery",
         "framework-devicelock",
         "framework-graphics",
         "framework-healthfitness",
@@ -97,7 +96,6 @@
     system_server_classpath: [
         "service-art",
         "service-configinfrastructure",
-        "service-crashrecovery",
         "service-healthfitness",
         "service-media-s",
         "service-permission",
diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp
index d566552..5688b96 100644
--- a/api/StubLibraries.bp
+++ b/api/StubLibraries.bp
@@ -29,9 +29,6 @@
 
 droidstubs {
     name: "api-stubs-docs-non-updatable",
-    srcs: [
-        ":framework-minus-apex-aconfig-srcjars",
-    ],
     defaults: [
         "android-non-updatable-stubs-defaults",
         "module-classpath-stubs-defaults",
@@ -91,9 +88,6 @@
 
 droidstubs {
     name: "system-api-stubs-docs-non-updatable",
-    srcs: [
-        ":framework-minus-apex-aconfig-srcjars",
-    ],
     defaults: [
         "android-non-updatable-stubs-defaults",
         "module-classpath-stubs-defaults",
@@ -134,9 +128,6 @@
 
 droidstubs {
     name: "test-api-stubs-docs-non-updatable",
-    srcs: [
-        ":framework-minus-apex-aconfig-srcjars",
-    ],
     defaults: [
         "android-non-updatable-stubs-defaults",
         "module-classpath-stubs-defaults",
@@ -184,9 +175,6 @@
 
 droidstubs {
     name: "module-lib-api-stubs-docs-non-updatable",
-    srcs: [
-        ":framework-minus-apex-aconfig-srcjars",
-    ],
     defaults: [
         "android-non-updatable-stubs-defaults",
         "module-classpath-stubs-defaults",
diff --git a/boot/Android.bp b/boot/Android.bp
index b33fab6..8a3d35e 100644
--- a/boot/Android.bp
+++ b/boot/Android.bp
@@ -84,10 +84,6 @@
             module: "com.android.conscrypt-bootclasspath-fragment",
         },
         {
-            apex: "com.android.crashrecovery",
-            module: "com.android.crashrecovery-bootclasspath-fragment",
-        },
-        {
             apex: "com.android.devicelock",
             module: "com.android.devicelock-bootclasspath-fragment",
         },
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index e12181a..3b6ea14 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -135,6 +135,7 @@
 import android.os.Handler;
 import android.os.HandlerExecutor;
 import android.os.IBinder;
+import android.os.IBinderCallback;
 import android.os.ICancellationSignal;
 import android.os.LocaleList;
 import android.os.Looper;
@@ -7274,6 +7275,18 @@
         } catch (RemoteException ex) {
             throw ex.rethrowFromSystemServer();
         }
+
+        // Set binder transaction callback after finishing bindApplication
+        Binder.setTransactionCallback(new IBinderCallback() {
+            @Override
+            public void onTransactionError(int pid, int code, int flags, int err) {
+                try {
+                    mgr.frozenBinderTransactionDetected(pid, code, flags, err);
+                } catch (RemoteException ex) {
+                    throw ex.rethrowFromSystemServer();
+                }
+            }
+        });
     }
 
     @UnsupportedAppUsage
diff --git a/core/java/android/app/ApplicationExitInfo.java b/core/java/android/app/ApplicationExitInfo.java
index d15c79f..24cb9ea 100644
--- a/core/java/android/app/ApplicationExitInfo.java
+++ b/core/java/android/app/ApplicationExitInfo.java
@@ -477,6 +477,16 @@
      */
     public static final int SUBREASON_OOM_KILL = 30;
 
+    /**
+     * The process was killed because its async kernel binder buffer is running out
+     * while being frozen.
+     * this would be set only when the reason is {@link #REASON_FREEZER}.
+     *
+     * For internal use only.
+     * @hide
+     */
+    public static final int SUBREASON_FREEZER_BINDER_ASYNC_FULL = 31;
+
     // If there is any OEM code which involves additional app kill reasons, it should
     // be categorized in {@link #REASON_OTHER}, with subreason code starting from 1000.
 
@@ -654,6 +664,7 @@
         SUBREASON_UNDELIVERED_BROADCAST,
         SUBREASON_EXCESSIVE_BINDER_OBJECTS,
         SUBREASON_OOM_KILL,
+        SUBREASON_FREEZER_BINDER_ASYNC_FULL,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface SubReason {}
@@ -1383,6 +1394,8 @@
                 return "EXCESSIVE BINDER OBJECTS";
             case SUBREASON_OOM_KILL:
                 return "OOM KILL";
+            case SUBREASON_FREEZER_BINDER_ASYNC_FULL:
+                return "FREEZER BINDER ASYNC FULL";
             default:
                 return "UNKNOWN";
         }
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index 03baf26..520bf7d 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -939,4 +939,14 @@
     int[] getUidFrozenState(in int[] uids);
 
     int checkPermissionForDevice(in String permission, int pid, int uid, int deviceId);
+
+    /**
+     * Notify AMS about binder transactions to frozen apps.
+     *
+     * @param debugPid The binder transaction sender
+     * @param code The binder transaction code
+     * @param flags The binder transaction flags
+     * @param err The binder transaction error
+     */
+    oneway void frozenBinderTransactionDetected(int debugPid, int code, int flags, int err);
 }
diff --git a/core/java/android/companion/virtual/flags.aconfig b/core/java/android/companion/virtual/flags.aconfig
index 55ae8ee..f380963 100644
--- a/core/java/android/companion/virtual/flags.aconfig
+++ b/core/java/android/companion/virtual/flags.aconfig
@@ -37,10 +37,17 @@
 }
 
 flag {
-    name: "virtual_camera"
-    namespace: "virtual_devices"
-    description: "Enable Virtual Camera"
-    bug: "270352264"
+  name: "virtual_camera"
+  namespace: "virtual_devices"
+  description: "Enable Virtual Camera"
+  bug: "270352264"
+}
+
+flag {
+  name: "stream_camera"
+  namespace: "virtual_devices"
+  description: "Enable streaming camera to Virtual Devices"
+  bug: "291740640"
 }
 
 flag {
diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java
index 218d4bb..3db1cb0 100644
--- a/core/java/android/os/Binder.java
+++ b/core/java/android/os/Binder.java
@@ -665,6 +665,32 @@
      */
     public static final native void blockUntilThreadAvailable();
 
+
+    /**
+     * TODO (b/308179628): Move this to libbinder for non-Java usages.
+     */
+    private static IBinderCallback sBinderCallback = null;
+
+    /**
+     * Set callback function for unexpected binder transaction errors.
+     *
+     * @hide
+     */
+    public static final void setTransactionCallback(IBinderCallback callback) {
+        sBinderCallback = callback;
+    }
+
+    /**
+     * Execute the callback function if it's already set.
+     *
+     * @hide
+     */
+    public static final void transactionCallback(int pid, int code, int flags, int err) {
+        if (sBinderCallback != null) {
+            sBinderCallback.onTransactionError(pid, code, flags, err);
+        }
+    }
+
     /**
      * Default constructor just initializes the object.
      *
diff --git a/core/java/android/os/IBinderCallback.java b/core/java/android/os/IBinderCallback.java
new file mode 100644
index 0000000..e4be5b0
--- /dev/null
+++ b/core/java/android/os/IBinderCallback.java
@@ -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 android.os;
+
+/**
+ * Callback interface for binder transaction errors
+ *
+ * @hide
+ */
+public interface IBinderCallback {
+    /**
+     * Callback function for unexpected binder transaction errors.
+     *
+     * @param debugPid The binder transaction sender
+     * @param code The binder transaction code
+     * @param flags The binder transaction flags
+     * @param err The binder transaction error
+     */
+    void onTransactionError(int debugPid, int code, int flags, int err);
+}
diff --git a/core/java/com/android/internal/os/BinderfsStatsReader.java b/core/java/com/android/internal/os/BinderfsStatsReader.java
new file mode 100644
index 0000000..9cc4a35
--- /dev/null
+++ b/core/java/com/android/internal/os/BinderfsStatsReader.java
@@ -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.internal.os;
+
+import com.android.internal.util.ProcFileReader;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * Reads and parses {@code binder_logs/stats} file in the {@code binderfs} filesystem.
+ * Reuse procFileReader as the contents are generated by Linux kernel in the same way.
+ *
+ * A typical example of binderfs stats log
+ *
+ * binder stats:
+ * BC_TRANSACTION: 378004
+ * BC_REPLY: 268352
+ * BC_FREE_BUFFER: 665854
+ * ...
+ * proc 12645
+ * context binder
+ * threads: 12
+ * requested threads: 0+5/15
+ * ready threads 0
+ * free async space 520192
+ * ...
+ */
+public class BinderfsStatsReader {
+    private final String mPath;
+
+    public BinderfsStatsReader() {
+        mPath = "/dev/binderfs/binder_logs/stats";
+    }
+
+    public BinderfsStatsReader(String path) {
+        mPath = path;
+    }
+
+    /**
+     * Read binderfs stats and call the consumer(pid, free) function for each valid process
+     *
+     * @param predicate  Test if the pid is valid.
+     * @param biConsumer Callback function for each valid pid and its free async space
+     * @param consumer   The error function to deal with exceptions
+     */
+    public void handleFreeAsyncSpace(Predicate<Integer> predicate,
+            BiConsumer<Integer, Integer> biConsumer, Consumer<Exception> consumer) {
+        try (ProcFileReader mReader = new ProcFileReader(new FileInputStream(mPath))) {
+            while (mReader.hasMoreData()) {
+                // find the next process
+                if (!mReader.nextString().equals("proc")) {
+                    mReader.finishLine();
+                    continue;
+                }
+
+                // read pid
+                int pid = mReader.nextInt();
+                mReader.finishLine();
+
+                // check if we have interest in this process
+                if (!predicate.test(pid)) {
+                    continue;
+                }
+
+                // read free async space
+                mReader.finishLine(); // context binder
+                mReader.finishLine(); // threads:
+                mReader.finishLine(); // requested threads:
+                mReader.finishLine(); // ready threads
+                if (!mReader.nextString().equals("free")) {
+                    mReader.finishLine();
+                    continue;
+                }
+                if (!mReader.nextString().equals("async")) {
+                    mReader.finishLine();
+                    continue;
+                }
+                if (!mReader.nextString().equals("space")) {
+                    mReader.finishLine();
+                    continue;
+                }
+                int free = mReader.nextInt();
+                mReader.finishLine();
+                biConsumer.accept(pid, free);
+            }
+        } catch (IOException | NumberFormatException e) {
+            consumer.accept(e);
+        }
+    }
+}
diff --git a/core/java/com/android/internal/util/LatencyTracker.java b/core/java/com/android/internal/util/LatencyTracker.java
index 3e9458d..b462c21 100644
--- a/core/java/com/android/internal/util/LatencyTracker.java
+++ b/core/java/com/android/internal/util/LatencyTracker.java
@@ -27,6 +27,7 @@
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_FOLD_TO_AOD;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOAD_SHARE_SHEET;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOCKSCREEN_UNLOCK;
+import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATION_BIG_PICTURE_LOADED;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_REQUEST_IME_HIDDEN;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_REQUEST_IME_SHOWN;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_ROTATE_SCREEN;
@@ -215,6 +216,12 @@
      */
     public static final int ACTION_SMARTSPACE_DOORBELL = 22;
 
+    /**
+     * Time it takes to lazy-load the image of a {@link android.app.Notification.BigPictureStyle}
+     * notification.
+     */
+    public static final int ACTION_NOTIFICATION_BIG_PICTURE_LOADED = 23;
+
     private static final int[] ACTIONS_ALL = {
         ACTION_EXPAND_PANEL,
         ACTION_TOGGLE_RECENTS,
@@ -239,6 +246,7 @@
         ACTION_REQUEST_IME_SHOWN,
         ACTION_REQUEST_IME_HIDDEN,
         ACTION_SMARTSPACE_DOORBELL,
+        ACTION_NOTIFICATION_BIG_PICTURE_LOADED,
     };
 
     /** @hide */
@@ -266,6 +274,7 @@
         ACTION_REQUEST_IME_SHOWN,
         ACTION_REQUEST_IME_HIDDEN,
         ACTION_SMARTSPACE_DOORBELL,
+        ACTION_NOTIFICATION_BIG_PICTURE_LOADED,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface Action {
@@ -296,6 +305,7 @@
             UIACTION_LATENCY_REPORTED__ACTION__ACTION_REQUEST_IME_SHOWN,
             UIACTION_LATENCY_REPORTED__ACTION__ACTION_REQUEST_IME_HIDDEN,
             UIACTION_LATENCY_REPORTED__ACTION__ACTION_SMARTSPACE_DOORBELL,
+            UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATION_BIG_PICTURE_LOADED,
     };
 
     private final Object mLock = new Object();
@@ -480,6 +490,8 @@
                 return "ACTION_REQUEST_IME_HIDDEN";
             case UIACTION_LATENCY_REPORTED__ACTION__ACTION_SMARTSPACE_DOORBELL:
                 return "ACTION_SMARTSPACE_DOORBELL";
+            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATION_BIG_PICTURE_LOADED:
+                return "ACTION_NOTIFICATION_BIG_PICTURE_LOADED";
             default:
                 throw new IllegalArgumentException("Invalid action");
         }
diff --git a/core/jni/android_util_Binder.cpp b/core/jni/android_util_Binder.cpp
index bfd80a9e..d2d5186 100644
--- a/core/jni/android_util_Binder.cpp
+++ b/core/jni/android_util_Binder.cpp
@@ -73,6 +73,7 @@
     jclass mClass;
     jmethodID mExecTransact;
     jmethodID mGetInterfaceDescriptor;
+    jmethodID mTransactionCallback;
 
     // Object state.
     jfieldID mObject;
@@ -1173,6 +1174,8 @@
     gBinderOffsets.mExecTransact = GetMethodIDOrDie(env, clazz, "execTransact", "(IJJI)Z");
     gBinderOffsets.mGetInterfaceDescriptor = GetMethodIDOrDie(env, clazz, "getInterfaceDescriptor",
         "()Ljava/lang/String;");
+    gBinderOffsets.mTransactionCallback =
+            GetStaticMethodIDOrDie(env, clazz, "transactionCallback", "(IIII)V");
     gBinderOffsets.mObject = GetFieldIDOrDie(env, clazz, "mObject", "J");
 
     return RegisterMethodsOrDie(
@@ -1387,7 +1390,12 @@
 
     if (err == NO_ERROR) {
         return JNI_TRUE;
-    } else if (err == UNKNOWN_TRANSACTION) {
+    }
+
+    env->CallStaticVoidMethod(gBinderOffsets.mClass, gBinderOffsets.mTransactionCallback, getpid(),
+                              code, flags, err);
+
+    if (err == UNKNOWN_TRANSACTION) {
         return JNI_FALSE;
     }
 
diff --git a/core/tests/coretests/src/com/android/internal/os/BinderfsStatsReaderTest.java b/core/tests/coretests/src/com/android/internal/os/BinderfsStatsReaderTest.java
new file mode 100644
index 0000000..e9f6450
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/os/BinderfsStatsReaderTest.java
@@ -0,0 +1,194 @@
+/*
+ * 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.os;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import android.content.Context;
+import android.os.FileUtils;
+import android.util.IntArray;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.nio.file.Files;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BinderfsStatsReaderTest {
+    private static final String BINDER_LOGS_STATS_HEADER = """
+            binder stats:
+            BC_TRANSACTION: 695756
+            BC_REPLY: 547779
+            BC_FREE_BUFFER: 1283223
+            BR_FAILED_REPLY: 4
+            BR_FROZEN_REPLY: 3
+            BR_ONEWAY_SPAM_SUSPECT: 1
+            proc: active 313 total 377
+            thread: active 3077 total 5227
+            """;
+    private static final String BINDER_LOGS_STATS_PROC1 = """
+            proc 14505
+            context binder
+              threads: 4
+              requested threads: 0+2/15
+              ready threads 0
+              free async space 520192
+              nodes: 9
+              refs: 29 s 29 w 29
+              buffers: 0
+            """;
+    private static final String BINDER_LOGS_STATS_PROC2 = """
+            proc 14461
+            context binder
+              threads: 8
+              requested threads: 0+2/15
+              ready threads 0
+              free async space 62
+              nodes: 30
+              refs: 51 s 51 w 51
+              buffers: 0
+            """;
+    private static final String BINDER_LOGS_STATS_PROC3 = """
+            proc 542
+            context binder
+              threads: 2
+              requested threads: 0+0/15
+              ready threads 0
+              free async space 519896
+              nodes: 1
+              refs: 2 s 3 w 2
+              buffers: 1
+            """;
+    private static final String BINDER_LOGS_STATS_PROC4 = """
+            proc 540
+            context binder
+              threads: 1
+              requested threads: 0+0/0
+              ready threads 1
+              free async space 44
+              nodes: 4
+              refs: 1 s 1 w 1
+              buffers: 0
+            """;
+    private File mStatsDirectory;
+    private int mFreezerBinderAsyncThreshold;
+    private IntArray mValidPids; // The pool of valid pids
+    private IntArray mStatsPids; // The pids read from binderfs stats that are also valid
+    private IntArray mStatsFree; // The free async space of the above pids
+    private boolean mHasError;
+
+    @Before
+    public void setUp() {
+        Context context = InstrumentationRegistry.getContext();
+        mStatsDirectory = context.getDir("binder_logs", Context.MODE_PRIVATE);
+        mFreezerBinderAsyncThreshold = 1024;
+        mValidPids = IntArray.fromArray(new int[]{14505, 14461, 542, 540}, 4);
+        mStatsPids = new IntArray();
+        mStatsFree = new IntArray();
+        mHasError = false;
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        FileUtils.deleteContents(mStatsDirectory);
+    }
+
+    @Test
+    public void testNoneProc() throws Exception {
+        runHandleBlockingFileLocks(BINDER_LOGS_STATS_HEADER);
+        assertFalse(mHasError);
+        assertEquals(0, mStatsPids.size());
+        assertEquals(0, mStatsFree.size());
+    }
+
+    @Test
+    public void testOneProc() throws Exception {
+        runHandleBlockingFileLocks(BINDER_LOGS_STATS_HEADER + BINDER_LOGS_STATS_PROC1);
+        assertFalse(mHasError);
+        assertEquals(0, mStatsPids.size());
+        assertEquals(0, mStatsFree.size());
+    }
+
+    @Test
+    public void testTwoProc() throws Exception {
+        runHandleBlockingFileLocks(BINDER_LOGS_STATS_HEADER + BINDER_LOGS_STATS_PROC1
+                + BINDER_LOGS_STATS_PROC2);
+        assertFalse(mHasError);
+        assertArrayEquals(mStatsPids.toArray(), new int[]{14461});
+        assertArrayEquals(mStatsFree.toArray(), new int[]{62});
+    }
+
+    @Test
+    public void testThreeProc() throws Exception {
+        runHandleBlockingFileLocks(BINDER_LOGS_STATS_HEADER + BINDER_LOGS_STATS_PROC1
+                + BINDER_LOGS_STATS_PROC2 + BINDER_LOGS_STATS_PROC3);
+        assertFalse(mHasError);
+        assertArrayEquals(mStatsPids.toArray(), new int[]{14461});
+        assertArrayEquals(mStatsFree.toArray(), new int[]{62});
+    }
+
+    @Test
+    public void testFourProc() throws Exception {
+        runHandleBlockingFileLocks(BINDER_LOGS_STATS_HEADER + BINDER_LOGS_STATS_PROC1
+                + BINDER_LOGS_STATS_PROC2 + BINDER_LOGS_STATS_PROC3 + BINDER_LOGS_STATS_PROC4);
+        assertFalse(mHasError);
+        assertArrayEquals(mStatsPids.toArray(), new int[]{14461, 540});
+        assertArrayEquals(mStatsFree.toArray(), new int[]{62, 44});
+    }
+
+    @Test
+    public void testInvalidProc() throws Exception {
+        mValidPids = new IntArray();
+        runHandleBlockingFileLocks(BINDER_LOGS_STATS_HEADER + BINDER_LOGS_STATS_PROC1
+                + BINDER_LOGS_STATS_PROC2 + BINDER_LOGS_STATS_PROC3 + BINDER_LOGS_STATS_PROC4);
+        assertFalse(mHasError);
+        assertEquals(0, mStatsPids.size());
+        assertEquals(0, mStatsFree.size());
+    }
+
+    private void runHandleBlockingFileLocks(String fileContents) throws Exception {
+        File tempFile = File.createTempFile("stats", null, mStatsDirectory);
+        Files.write(tempFile.toPath(), fileContents.getBytes());
+        new BinderfsStatsReader(tempFile.toString()).handleFreeAsyncSpace(
+                // Check if the current process is a valid one
+                mValidPids::contains,
+
+                // Check if the current process is running out of async binder space
+                (pid, free) -> {
+                    if (free < mFreezerBinderAsyncThreshold) {
+                        mStatsPids.add(pid);
+                        mStatsFree.add(free);
+                    }
+                },
+
+                // Log the error if binderfs stats can't be accesses or correctly parsed
+                exception -> {
+                    mHasError = true;
+                });
+        Files.delete(tempFile.toPath());
+    }
+}
diff --git a/libs/WindowManager/Shell/proto/wm_shell_transition_trace.proto b/libs/WindowManager/Shell/proto/wm_shell_transition_trace.proto
index c82a70c..5c58158 100644
--- a/libs/WindowManager/Shell/proto/wm_shell_transition_trace.proto
+++ b/libs/WindowManager/Shell/proto/wm_shell_transition_trace.proto
@@ -48,7 +48,7 @@
     optional int32 handler = 3;
     optional int64 merge_time_ns = 4;
     optional int64 merge_request_time_ns = 5;
-    optional int32 merged_into = 6;
+    optional int32 merge_target = 6;
     optional int64 abort_time_ns = 7;
 }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Tracer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Tracer.java
index e27e4f9..5919aad 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Tracer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Tracer.java
@@ -129,13 +129,12 @@
      * Adds an entry in the trace to log that a request to merge a transition was made.
      *
      * @param mergeRequestedTransitionId The id of the transition we are requesting to be merged.
-     * @param playingTransitionId The id of the transition we was to merge the transition into.
      */
     public void logMergeRequested(int mergeRequestedTransitionId, int playingTransitionId) {
         com.android.wm.shell.nano.Transition proto = new com.android.wm.shell.nano.Transition();
         proto.id = mergeRequestedTransitionId;
         proto.mergeRequestTimeNs = SystemClock.elapsedRealtimeNanos();
-        proto.mergedInto = playingTransitionId;
+        proto.mergeTarget = playingTransitionId;
 
         mTraceBuffer.add(proto);
     }
@@ -150,7 +149,7 @@
         com.android.wm.shell.nano.Transition proto = new com.android.wm.shell.nano.Transition();
         proto.id = mergedTransitionId;
         proto.mergeTimeNs = SystemClock.elapsedRealtimeNanos();
-        proto.mergedInto = playingTransitionId;
+        proto.mergeTarget = playingTransitionId;
 
         mTraceBuffer.add(proto);
     }
diff --git a/packages/SettingsLib/SpaPrivileged/Android.bp b/packages/SettingsLib/SpaPrivileged/Android.bp
index 009407a..eaeda3c 100644
--- a/packages/SettingsLib/SpaPrivileged/Android.bp
+++ b/packages/SettingsLib/SpaPrivileged/Android.bp
@@ -38,7 +38,6 @@
     static_libs: [
         "androidx.compose.runtime_runtime",
         "SpaPrivilegedLib",
-        "android.content.pm.flags-aconfig-java",
     ],
     kotlincflags: ["-Xjvm-default=all"],
 }
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt
index 92fd0cd..95e678f 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt
@@ -35,7 +35,7 @@
     fun ApplicationInfo.hasGrantPermission(permission: String): Boolean
 
     suspend fun getAppOpPermissionPackages(userId: Int, permission: String): Set<String>
-    fun getPackageInfoAsUser(packageName: String, flags: Int, userId: Int): PackageInfo?
+    fun getPackageInfoAsUser(packageName: String, flags: Long, userId: Int): PackageInfo?
 }
 
 object PackageManagers : IPackageManagers by PackageManagersImpl(PackageManagerWrapperImpl)
@@ -72,14 +72,16 @@
             ?: false
 
     override fun ApplicationInfo.hasRequestPermission(permission: String): Boolean {
-        val packageInfo = getPackageInfoAsUser(packageName, PackageManager.GET_PERMISSIONS, userId)
+        val packageInfo =
+            getPackageInfoAsUser(packageName, PackageManager.GET_PERMISSIONS.toLong(), userId)
         return packageInfo?.requestedPermissions?.let {
             permission in it
         } ?: false
     }
 
     override fun ApplicationInfo.hasGrantPermission(permission: String): Boolean {
-        val packageInfo = getPackageInfoAsUser(packageName, PackageManager.GET_PERMISSIONS, userId)
+        val packageInfo =
+            getPackageInfoAsUser(packageName, PackageManager.GET_PERMISSIONS.toLong(), userId)
         val index = packageInfo?.requestedPermissions?.indexOf(permission) ?: return false
         return index >= 0 &&
             checkNotNull(packageInfo.requestedPermissionsFlags)[index]
@@ -91,8 +93,8 @@
             iPackageManager.isPackageAvailable(it, userId)
         }.toSet()
 
-    override fun getPackageInfoAsUser(packageName: String, flags: Int, userId: Int): PackageInfo? =
-        packageManagerWrapper.getPackageInfoAsUserCached(packageName, flags.toLong(), userId)
+    override fun getPackageInfoAsUser(packageName: String, flags: Long, userId: Int): PackageInfo? =
+        packageManagerWrapper.getPackageInfoAsUserCached(packageName, flags, userId)
 
     private fun Int.hasFlag(flag: Int) = (this and flag) > 0
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index c67df71..b0832e3 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -16,6 +16,7 @@
 
 package com.android.settingslib.bluetooth;
 
+import android.annotation.CallbackExecutor;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothClass;
 import android.bluetooth.BluetoothCsipSetCoordinator;
@@ -39,6 +40,7 @@
 import android.util.LruCache;
 import android.util.Pair;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.util.ArrayUtils;
@@ -52,8 +54,12 @@
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
 import java.util.stream.Stream;
 
 /**
@@ -101,6 +107,8 @@
 
     private final Collection<Callback> mCallbacks = new CopyOnWriteArrayList<>();
 
+    private final Map<Callback, Executor> mCallbackExecutorMap = new ConcurrentHashMap<>();
+
     /**
      * Last time a bt profile auto-connect was attempted.
      * If an ACTION_UUID intent comes in within
@@ -992,18 +1000,39 @@
         return new ArrayList<>(mRemovedProfiles);
     }
 
+    /**
+     * @deprecated Use {@link #registerCallback(Executor, Callback)}.
+     */
+    @Deprecated
     public void registerCallback(Callback callback) {
         mCallbacks.add(callback);
     }
 
+    /**
+     * Registers a {@link Callback} that will be invoked when the bluetooth device attribute is
+     * changed.
+     *
+     * @param executor an {@link Executor} to execute given callback
+     * @param callback user implementation of the {@link Callback}
+     */
+    public void registerCallback(
+            @NonNull @CallbackExecutor Executor executor, @NonNull Callback callback) {
+        Objects.requireNonNull(executor, "executor cannot be null");
+        Objects.requireNonNull(callback, "callback cannot be null");
+        mCallbackExecutorMap.put(callback, executor);
+    }
+
     public void unregisterCallback(Callback callback) {
         mCallbacks.remove(callback);
+        mCallbackExecutorMap.remove(callback);
     }
 
     void dispatchAttributesChanged() {
         for (Callback callback : mCallbacks) {
             callback.onDeviceAttributesChanged();
         }
+        mCallbackExecutorMap.forEach((callback, executor) ->
+                executor.execute(callback::onDeviceAttributesChanged));
     }
 
     @Override
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt
deleted file mode 100644
index 82d4239..0000000
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt
+++ /dev/null
@@ -1,75 +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.compose.animation.scene
-
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.dp
-
-interface EdgeDetector {
-    /**
-     * Return the [Edge] associated to [position] inside a layout of size [layoutSize], given
-     * [density] and [orientation].
-     */
-    fun edge(
-        layoutSize: IntSize,
-        position: IntOffset,
-        density: Density,
-        orientation: Orientation,
-    ): Edge?
-}
-
-val DefaultEdgeDetector = FixedSizeEdgeDetector(40.dp)
-
-/** An [EdgeDetector] that detects edges assuming a fixed edge size of [size]. */
-class FixedSizeEdgeDetector(val size: Dp) : EdgeDetector {
-    override fun edge(
-        layoutSize: IntSize,
-        position: IntOffset,
-        density: Density,
-        orientation: Orientation,
-    ): Edge? {
-        val axisSize: Int
-        val axisPosition: Int
-        val topOrLeft: Edge
-        val bottomOrRight: Edge
-        when (orientation) {
-            Orientation.Horizontal -> {
-                axisSize = layoutSize.width
-                axisPosition = position.x
-                topOrLeft = Edge.Left
-                bottomOrRight = Edge.Right
-            }
-            Orientation.Vertical -> {
-                axisSize = layoutSize.height
-                axisPosition = position.y
-                topOrLeft = Edge.Top
-                bottomOrRight = Edge.Bottom
-            }
-        }
-
-        val sizePx = with(density) { size.toPx() }
-        return when {
-            axisPosition <= sizePx -> topOrLeft
-            axisPosition >= axisSize - sizePx -> bottomOrRight
-            else -> null
-        }
-    }
-}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt
index ae7d8f5..d005413 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt
@@ -2,6 +2,7 @@
 
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import kotlinx.coroutines.CoroutineScope
 
 interface GestureHandler {
     val draggable: DraggableHandler
@@ -9,9 +10,9 @@
 }
 
 interface DraggableHandler {
-    fun onDragStarted(startedPosition: Offset, pointersDown: Int = 1)
+    suspend fun onDragStarted(coroutineScope: CoroutineScope, startedPosition: Offset)
     fun onDelta(pixels: Float)
-    fun onDragStopped(velocity: Float)
+    suspend fun onDragStopped(coroutineScope: CoroutineScope, velocity: Float)
 }
 
 interface NestedScrollHandler {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
deleted file mode 100644
index 97d3fff..0000000
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
+++ /dev/null
@@ -1,191 +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.compose.animation.scene
-
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.awaitEachGesture
-import androidx.compose.foundation.gestures.awaitFirstDown
-import androidx.compose.foundation.gestures.awaitHorizontalTouchSlopOrCancellation
-import androidx.compose.foundation.gestures.awaitVerticalTouchSlopOrCancellation
-import androidx.compose.foundation.gestures.horizontalDrag
-import androidx.compose.foundation.gestures.verticalDrag
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.PointerId
-import androidx.compose.ui.input.pointer.PointerInputChange
-import androidx.compose.ui.input.pointer.PointerInputScope
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.input.pointer.positionChange
-import androidx.compose.ui.input.pointer.util.VelocityTracker
-import androidx.compose.ui.input.pointer.util.addPointerInputChange
-import androidx.compose.ui.platform.LocalViewConfiguration
-import androidx.compose.ui.unit.Velocity
-import androidx.compose.ui.util.fastForEach
-
-/**
- * Make an element draggable in the given [orientation].
- *
- * The main difference with [multiPointerDraggable] and
- * [androidx.compose.foundation.gestures.draggable] is that [onDragStarted] also receives the number
- * of pointers that are down when the drag is started. If you don't need this information, you
- * should use `draggable` instead.
- *
- * Note that the current implementation is trivial: we wait for the touch slope on the *first* down
- * pointer, then we count the number of distinct pointers that are down right before calling
- * [onDragStarted]. This means that the drag won't start when a first pointer is down (but not
- * dragged) and a second pointer is down and dragged. This is an implementation detail that might
- * change in the future.
- */
-// TODO(b/291055080): Migrate to the Modifier.Node API.
-@Composable
-internal fun Modifier.multiPointerDraggable(
-    orientation: Orientation,
-    enabled: Boolean,
-    startDragImmediately: Boolean,
-    onDragStarted: (startedPosition: Offset, pointersDown: Int) -> Unit,
-    onDragDelta: (Float) -> Unit,
-    onDragStopped: (velocity: Float) -> Unit,
-): Modifier {
-    val onDragStarted by rememberUpdatedState(onDragStarted)
-    val onDragStopped by rememberUpdatedState(onDragStopped)
-    val onDragDelta by rememberUpdatedState(onDragDelta)
-    val startDragImmediately by rememberUpdatedState(startDragImmediately)
-
-    val velocityTracker = remember { VelocityTracker() }
-    val maxFlingVelocity =
-        LocalViewConfiguration.current.maximumFlingVelocity.let { max ->
-            val maxF = max.toFloat()
-            Velocity(maxF, maxF)
-        }
-
-    return this.pointerInput(enabled, orientation, maxFlingVelocity) {
-        if (!enabled) {
-            return@pointerInput
-        }
-
-        val onDragStart: (Offset, Int) -> Unit = { startedPosition, pointersDown ->
-            velocityTracker.resetTracking()
-            onDragStarted(startedPosition, pointersDown)
-        }
-
-        val onDragCancel: () -> Unit = { onDragStopped(/* velocity= */ 0f) }
-
-        val onDragEnd: () -> Unit = {
-            val velocity = velocityTracker.calculateVelocity(maxFlingVelocity)
-            onDragStopped(
-                when (orientation) {
-                    Orientation.Horizontal -> velocity.x
-                    Orientation.Vertical -> velocity.y
-                }
-            )
-        }
-
-        val onDrag: (change: PointerInputChange, dragAmount: Float) -> Unit = { change, amount ->
-            velocityTracker.addPointerInputChange(change)
-            onDragDelta(amount)
-        }
-
-        detectDragGestures(
-            orientation = orientation,
-            startDragImmediately = { startDragImmediately },
-            onDragStart = onDragStart,
-            onDragEnd = onDragEnd,
-            onDragCancel = onDragCancel,
-            onDrag = onDrag,
-        )
-    }
-}
-
-/**
- * Detect drag gestures in the given [orientation].
- *
- * This function is a mix of [androidx.compose.foundation.gestures.awaitDownAndSlop] and
- * [androidx.compose.foundation.gestures.detectVerticalDragGestures] to add support for:
- * 1) starting the gesture immediately without requiring a drag >= touch slope;
- * 2) passing the number of pointers down to [onDragStart].
- */
-private suspend fun PointerInputScope.detectDragGestures(
-    orientation: Orientation,
-    startDragImmediately: () -> Boolean,
-    onDragStart: (startedPosition: Offset, pointersDown: Int) -> Unit,
-    onDragEnd: () -> Unit,
-    onDragCancel: () -> Unit,
-    onDrag: (change: PointerInputChange, dragAmount: Float) -> Unit,
-) {
-    awaitEachGesture {
-        val initialDown = awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
-        var overSlop = 0f
-        val drag =
-            if (startDragImmediately()) {
-                initialDown.consume()
-                initialDown
-            } else {
-                val down = awaitFirstDown(requireUnconsumed = false)
-                val onSlopReached = { change: PointerInputChange, over: Float ->
-                    change.consume()
-                    overSlop = over
-                }
-
-                // TODO(b/291055080): Replace by await[Orientation]PointerSlopOrCancellation once
-                // it is public.
-                when (orientation) {
-                    Orientation.Horizontal ->
-                        awaitHorizontalTouchSlopOrCancellation(down.id, onSlopReached)
-                    Orientation.Vertical ->
-                        awaitVerticalTouchSlopOrCancellation(down.id, onSlopReached)
-                }
-            }
-
-        if (drag != null) {
-            // Count the number of pressed pointers.
-            val pressed = mutableSetOf<PointerId>()
-            currentEvent.changes.fastForEach { change ->
-                if (change.pressed) {
-                    pressed.add(change.id)
-                }
-            }
-
-            onDragStart(drag.position, pressed.size)
-            onDrag(drag, overSlop)
-
-            val successful =
-                when (orientation) {
-                    Orientation.Horizontal ->
-                        horizontalDrag(drag.id) {
-                            onDrag(it, it.positionChange().x)
-                            it.consume()
-                        }
-                    Orientation.Vertical ->
-                        verticalDrag(drag.id) {
-                            onDrag(it, it.positionChange().y)
-                            it.consume()
-                        }
-                }
-
-            if (successful) {
-                onDragEnd()
-            } else {
-                onDragCancel()
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
index 3fd6828..9c799b28 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
@@ -16,6 +16,7 @@
 
 package com.android.compose.animation.scene
 
+import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.layout.Box
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.State
@@ -100,3 +101,19 @@
         MovableElement(layoutImpl, scene, key, modifier, content)
     }
 }
+
+/** The destination scene when swiping up or left from [upOrLeft]. */
+internal fun Scene.upOrLeft(orientation: Orientation): SceneKey? {
+    return when (orientation) {
+        Orientation.Vertical -> userActions[Swipe.Up]
+        Orientation.Horizontal -> userActions[Swipe.Left]
+    }
+}
+
+/** The destination scene when swiping down or right from [downOrRight]. */
+internal fun Scene.downOrRight(orientation: Orientation): SceneKey? {
+    return when (orientation) {
+        Orientation.Vertical -> userActions[Swipe.Down]
+        Orientation.Horizontal -> userActions[Swipe.Right]
+    }
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 1f38e70..74e66d2 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -16,7 +16,6 @@
 
 package com.android.compose.animation.scene
 
-import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.State
 import androidx.compose.runtime.remember
@@ -38,7 +37,6 @@
  *   instance by triggering back navigation or by swiping to a new scene.
  * @param transitions the definition of the transitions used to animate a change of scene.
  * @param state the observable state of this layout.
- * @param edgeDetector the edge detector used to detect which edge a swipe is started from, if any.
  * @param scenes the configuration of the different scenes of this layout.
  */
 @Composable
@@ -48,7 +46,6 @@
     transitions: SceneTransitions,
     modifier: Modifier = Modifier,
     state: SceneTransitionLayoutState = remember { SceneTransitionLayoutState(currentScene) },
-    edgeDetector: EdgeDetector = DefaultEdgeDetector,
     scenes: SceneTransitionLayoutScope.() -> Unit,
 ) {
     val density = LocalDensity.current
@@ -59,17 +56,15 @@
             transitions,
             state,
             density,
-            edgeDetector,
         )
     }
 
     layoutImpl.onChangeScene = onChangeScene
     layoutImpl.transitions = transitions
     layoutImpl.density = density
-    layoutImpl.edgeDetector = edgeDetector
-
     layoutImpl.setScenes(scenes)
     layoutImpl.setCurrentScene(currentScene)
+
     layoutImpl.Content(modifier)
 }
 
@@ -196,9 +191,9 @@
     }
 }
 
-enum class SwipeDirection(val orientation: Orientation) {
-    Up(Orientation.Vertical),
-    Down(Orientation.Vertical),
-    Left(Orientation.Horizontal),
-    Right(Orientation.Horizontal),
+enum class SwipeDirection {
+    Up,
+    Down,
+    Left,
+    Right,
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index a803a47..a40b299 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -47,7 +47,6 @@
     transitions: SceneTransitions,
     internal val state: SceneTransitionLayoutState,
     density: Density,
-    edgeDetector: EdgeDetector,
 ) {
     internal val scenes = SnapshotStateMap<SceneKey, Scene>()
     internal val elements = SnapshotStateMap<ElementKey, Element>()
@@ -58,7 +57,6 @@
     internal var onChangeScene by mutableStateOf(onChangeScene)
     internal var transitions by mutableStateOf(transitions)
     internal var density: Density by mutableStateOf(density)
-    internal var edgeDetector by mutableStateOf(edgeDetector)
 
     /**
      * The size of this layout. Note that this could be [IntSize.Zero] if this layour does not have
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
index ee1f133..2dc53ab 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
@@ -22,6 +22,8 @@
 import androidx.compose.animation.core.Spring
 import androidx.compose.animation.core.spring
 import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.gestures.rememberDraggableState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.getValue
@@ -35,7 +37,6 @@
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.round
 import com.android.compose.nestedscroll.PriorityNestedScrollConnection
 import kotlin.math.absoluteValue
 import kotlinx.coroutines.CoroutineScope
@@ -54,7 +55,7 @@
 
     /** Whether swipe should be enabled in the given [orientation]. */
     fun Scene.shouldEnableSwipes(orientation: Orientation): Boolean =
-        userActions.keys.any { it is Swipe && it.direction.orientation == orientation }
+        upOrLeft(orientation) != null || downOrRight(orientation) != null
 
     val currentScene = gestureHandler.currentScene
     val canSwipe = currentScene.shouldEnableSwipes(orientation)
@@ -67,7 +68,8 @@
         )
 
     return nestedScroll(connection = gestureHandler.nestedScroll.connection)
-        .multiPointerDraggable(
+        .draggable(
+            state = rememberDraggableState(onDelta = gestureHandler.draggable::onDelta),
             orientation = orientation,
             enabled = gestureHandler.isDrivingTransition || canSwipe,
             // Immediately start the drag if this our [transition] is currently animating to a scene
@@ -78,7 +80,6 @@
                     gestureHandler.isAnimatingOffset &&
                     !canOppositeSwipe,
             onDragStarted = gestureHandler.draggable::onDragStarted,
-            onDragDelta = gestureHandler.draggable::onDelta,
             onDragStopped = gestureHandler.draggable::onDragStopped,
         )
 }
@@ -158,7 +159,7 @@
 
     internal var gestureWithPriority: Any? = null
 
-    internal fun onDragStarted(pointersDown: Int, startedPosition: Offset?) {
+    internal fun onDragStarted() {
         if (isDrivingTransition) {
             // This [transition] was already driving the animation: simply take over it.
             // Stop animating and start from where the current offset.
@@ -198,48 +199,6 @@
                 Orientation.Vertical -> layoutImpl.size.height
             }.toFloat()
 
-        val fromEdge =
-            startedPosition?.let { position ->
-                layoutImpl.edgeDetector.edge(
-                    layoutImpl.size,
-                    position.round(),
-                    layoutImpl.density,
-                    orientation,
-                )
-            }
-
-        swipeTransition.actionUpOrLeft =
-            Swipe(
-                direction =
-                    when (orientation) {
-                        Orientation.Horizontal -> SwipeDirection.Left
-                        Orientation.Vertical -> SwipeDirection.Up
-                    },
-                pointerCount = pointersDown,
-                fromEdge = fromEdge,
-            )
-
-        swipeTransition.actionDownOrRight =
-            Swipe(
-                direction =
-                    when (orientation) {
-                        Orientation.Horizontal -> SwipeDirection.Right
-                        Orientation.Vertical -> SwipeDirection.Down
-                    },
-                pointerCount = pointersDown,
-                fromEdge = fromEdge,
-            )
-
-        if (fromEdge == null) {
-            swipeTransition.actionUpOrLeftNoEdge = null
-            swipeTransition.actionDownOrRightNoEdge = null
-        } else {
-            swipeTransition.actionUpOrLeftNoEdge =
-                (swipeTransition.actionUpOrLeft as Swipe).copy(fromEdge = null)
-            swipeTransition.actionDownOrRightNoEdge =
-                (swipeTransition.actionDownOrRight as Swipe).copy(fromEdge = null)
-        }
-
         if (swipeTransition.absoluteDistance > 0f) {
             transitionState = swipeTransition
         }
@@ -287,11 +246,11 @@
         // to the next screen or go back to the previous one.
         val offset = swipeTransition.dragOffset
         val absoluteDistance = swipeTransition.absoluteDistance
-        if (offset <= -absoluteDistance && swipeTransition.upOrLeft(fromScene) == toScene.key) {
+        if (offset <= -absoluteDistance && fromScene.upOrLeft(orientation) == toScene.key) {
             swipeTransition.dragOffset += absoluteDistance
             swipeTransition._fromScene = toScene
         } else if (
-            offset >= absoluteDistance && swipeTransition.downOrRight(fromScene) == toScene.key
+            offset >= absoluteDistance && fromScene.downOrRight(orientation) == toScene.key
         ) {
             swipeTransition.dragOffset -= absoluteDistance
             swipeTransition._fromScene = toScene
@@ -313,8 +272,8 @@
                 Orientation.Vertical -> layoutImpl.size.height
             }.toFloat()
 
-        val upOrLeft = swipeTransition.upOrLeft(this)
-        val downOrRight = swipeTransition.downOrRight(this)
+        val upOrLeft = upOrLeft(orientation)
+        val downOrRight = downOrRight(orientation)
 
         // Compute the target scene depending on the current offset.
         return when {
@@ -557,22 +516,6 @@
         var _distance by mutableFloatStateOf(0f)
         val distance: Float
             get() = _distance
-
-        /** The [UserAction]s associated to this swipe. */
-        var actionUpOrLeft: UserAction = Back
-        var actionDownOrRight: UserAction = Back
-        var actionUpOrLeftNoEdge: UserAction? = null
-        var actionDownOrRightNoEdge: UserAction? = null
-
-        fun upOrLeft(scene: Scene): SceneKey? {
-            return scene.userActions[actionUpOrLeft]
-                ?: actionUpOrLeftNoEdge?.let { scene.userActions[it] }
-        }
-
-        fun downOrRight(scene: Scene): SceneKey? {
-            return scene.userActions[actionDownOrRight]
-                ?: actionDownOrRightNoEdge?.let { scene.userActions[it] }
-        }
     }
 
     companion object {
@@ -583,9 +526,9 @@
 private class SceneDraggableHandler(
     private val gestureHandler: SceneGestureHandler,
 ) : DraggableHandler {
-    override fun onDragStarted(startedPosition: Offset, pointersDown: Int) {
+    override suspend fun onDragStarted(coroutineScope: CoroutineScope, startedPosition: Offset) {
         gestureHandler.gestureWithPriority = this
-        gestureHandler.onDragStarted(pointersDown, startedPosition)
+        gestureHandler.onDragStarted()
     }
 
     override fun onDelta(pixels: Float) {
@@ -594,7 +537,7 @@
         }
     }
 
-    override fun onDragStopped(velocity: Float) {
+    override suspend fun onDragStopped(coroutineScope: CoroutineScope, velocity: Float) {
         if (gestureHandler.gestureWithPriority == this) {
             gestureHandler.gestureWithPriority = null
             gestureHandler.onDragStopped(velocity = velocity, canChangeScene = true)
@@ -637,31 +580,11 @@
         // moving on to the next scene.
         var gestureStartedOnNestedChild = false
 
-        val actionUpOrLeft =
-            Swipe(
-                direction =
-                    when (gestureHandler.orientation) {
-                        Orientation.Horizontal -> SwipeDirection.Left
-                        Orientation.Vertical -> SwipeDirection.Up
-                    },
-                pointerCount = 1,
-            )
-
-        val actionDownOrRight =
-            Swipe(
-                direction =
-                    when (gestureHandler.orientation) {
-                        Orientation.Horizontal -> SwipeDirection.Right
-                        Orientation.Vertical -> SwipeDirection.Down
-                    },
-                pointerCount = 1,
-            )
-
         fun findNextScene(amount: Float): SceneKey? {
             val fromScene = gestureHandler.currentScene
             return when {
-                amount < 0f -> fromScene.userActions[actionUpOrLeft]
-                amount > 0f -> fromScene.userActions[actionDownOrRight]
+                amount < 0f -> fromScene.upOrLeft(gestureHandler.orientation)
+                amount > 0f -> fromScene.downOrRight(gestureHandler.orientation)
                 else -> null
             }
         }
@@ -702,7 +625,7 @@
             onStart = {
                 gestureHandler.gestureWithPriority = this
                 priorityScene = nextScene
-                gestureHandler.onDragStarted(pointersDown = 1, startedPosition = null)
+                gestureHandler.onDragStarted()
             },
             onScroll = { offsetAvailable ->
                 if (gestureHandler.gestureWithPriority != this) {
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/FixedSizeEdgeDetectorTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/FixedSizeEdgeDetectorTest.kt
deleted file mode 100644
index a68282a..0000000
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/FixedSizeEdgeDetectorTest.kt
+++ /dev/null
@@ -1,70 +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.compose.animation.scene
-
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class FixedSizeEdgeDetectorTest {
-    private val detector = FixedSizeEdgeDetector(30.dp)
-    private val layoutSize = IntSize(100, 100)
-    private val density = Density(1f)
-
-    @Test
-    fun horizontalEdges() {
-        fun horizontalEdge(position: Int): Edge? =
-            detector.edge(
-                layoutSize,
-                position = IntOffset(position, 0),
-                density,
-                Orientation.Horizontal,
-            )
-
-        assertThat(horizontalEdge(0)).isEqualTo(Edge.Left)
-        assertThat(horizontalEdge(30)).isEqualTo(Edge.Left)
-        assertThat(horizontalEdge(31)).isEqualTo(null)
-        assertThat(horizontalEdge(69)).isEqualTo(null)
-        assertThat(horizontalEdge(70)).isEqualTo(Edge.Right)
-        assertThat(horizontalEdge(100)).isEqualTo(Edge.Right)
-    }
-
-    @Test
-    fun verticalEdges() {
-        fun verticalEdge(position: Int): Edge? =
-            detector.edge(
-                layoutSize,
-                position = IntOffset(0, position),
-                density,
-                Orientation.Vertical,
-            )
-
-        assertThat(verticalEdge(0)).isEqualTo(Edge.Top)
-        assertThat(verticalEdge(30)).isEqualTo(Edge.Top)
-        assertThat(verticalEdge(31)).isEqualTo(null)
-        assertThat(verticalEdge(69)).isEqualTo(null)
-        assertThat(verticalEdge(70)).isEqualTo(Edge.Bottom)
-        assertThat(verticalEdge(100)).isEqualTo(Edge.Bottom)
-    }
-}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
index 1eb3392..6791a85 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
@@ -55,8 +55,7 @@
                             builder = scenesBuilder,
                             transitions = EmptyTestTransitions,
                             state = layoutState,
-                            density = Density(1f),
-                            edgeDetector = DefaultEdgeDetector,
+                            density = Density(1f)
                         )
                         .also { it.size = IntSize(SCREEN_SIZE.toInt(), SCREEN_SIZE.toInt()) },
                 orientation = Orientation.Vertical,
@@ -105,13 +104,13 @@
 
     @Test
     fun onDragStarted_shouldStartATransition() = runGestureTest {
-        draggable.onDragStarted(startedPosition = Offset.Zero)
+        draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
         assertScene(currentScene = SceneA, isIdle = false)
     }
 
     @Test
     fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest {
-        draggable.onDragStarted(startedPosition = Offset.Zero)
+        draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
         assertScene(currentScene = SceneA, isIdle = false)
         val transition = transitionState as Transition
 
@@ -124,13 +123,14 @@
 
     @Test
     fun onDragStoppedAfterDrag_velocityLowerThanThreshold_remainSameScene() = runGestureTest {
-        draggable.onDragStarted(startedPosition = Offset.Zero)
+        draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
         assertScene(currentScene = SceneA, isIdle = false)
 
         draggable.onDelta(pixels = deltaInPixels10)
         assertScene(currentScene = SceneA, isIdle = false)
 
         draggable.onDragStopped(
+            coroutineScope = coroutineScope,
             velocity = velocityThreshold - 0.01f,
         )
         assertScene(currentScene = SceneA, isIdle = false)
@@ -142,13 +142,14 @@
 
     @Test
     fun onDragStoppedAfterDrag_velocityAtLeastThreshold_goToNextScene() = runGestureTest {
-        draggable.onDragStarted(startedPosition = Offset.Zero)
+        draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
         assertScene(currentScene = SceneA, isIdle = false)
 
         draggable.onDelta(pixels = deltaInPixels10)
         assertScene(currentScene = SceneA, isIdle = false)
 
         draggable.onDragStopped(
+            coroutineScope = coroutineScope,
             velocity = velocityThreshold,
         )
         assertScene(currentScene = SceneC, isIdle = false)
@@ -160,22 +161,23 @@
 
     @Test
     fun onDragStoppedAfterStarted_returnImmediatelyToIdle() = runGestureTest {
-        draggable.onDragStarted(startedPosition = Offset.Zero)
+        draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
         assertScene(currentScene = SceneA, isIdle = false)
 
-        draggable.onDragStopped(velocity = 0f)
+        draggable.onDragStopped(coroutineScope = coroutineScope, velocity = 0f)
         assertScene(currentScene = SceneA, isIdle = true)
     }
 
     @Test
     fun startGestureDuringAnimatingOffset_shouldImmediatelyStopTheAnimation() = runGestureTest {
-        draggable.onDragStarted(startedPosition = Offset.Zero)
+        draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
         assertScene(currentScene = SceneA, isIdle = false)
 
         draggable.onDelta(pixels = deltaInPixels10)
         assertScene(currentScene = SceneA, isIdle = false)
 
         draggable.onDragStopped(
+            coroutineScope = coroutineScope,
             velocity = velocityThreshold,
         )
 
@@ -189,7 +191,7 @@
         assertScene(currentScene = SceneC, isIdle = false)
 
         // Start a new gesture while the offset is animating
-        draggable.onDragStarted(startedPosition = Offset.Zero)
+        draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
         assertThat(sceneGestureHandler.isAnimatingOffset).isFalse()
     }
 
@@ -318,7 +320,7 @@
     }
     @Test
     fun beforeDraggableStart_stop_shouldBeIgnored() = runGestureTest {
-        draggable.onDragStopped(velocityThreshold)
+        draggable.onDragStopped(coroutineScope, velocityThreshold)
         assertScene(currentScene = SceneA, isIdle = true)
     }
 
@@ -330,7 +332,7 @@
 
     @Test
     fun startNestedScrollWhileDragging() = runGestureTest {
-        draggable.onDragStarted(Offset.Zero)
+        draggable.onDragStarted(coroutineScope, Offset.Zero)
         assertScene(currentScene = SceneA, isIdle = false)
         val transition = transitionState as Transition
 
@@ -342,7 +344,7 @@
         assertThat(transition.progress).isEqualTo(0.2f)
 
         // this should be ignored, we are scrolling now!
-        draggable.onDragStopped(velocityThreshold)
+        draggable.onDragStopped(coroutineScope, velocityThreshold)
         assertScene(currentScene = SceneA, isIdle = false)
 
         nestedScrollEvents(available = offsetY10)
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
index 4a6066f..df3b72a 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -48,14 +48,6 @@
         /** The middle of the layout, in pixels. */
         private val Density.middle: Offset
             get() = Offset((LayoutWidth / 2).toPx(), (LayoutHeight / 2).toPx())
-
-        /** The middle-top of the layout, in pixels. */
-        private val Density.middleTop: Offset
-            get() = Offset((LayoutWidth / 2).toPx(), 0f)
-
-        /** The middle-left of the layout, in pixels. */
-        private val Density.middleLeft: Offset
-            get() = Offset(0f, (LayoutHeight / 2).toPx())
     }
 
     private var currentScene by mutableStateOf(TestScenes.SceneA)
@@ -91,13 +83,7 @@
             }
             scene(
                 TestScenes.SceneC,
-                userActions =
-                    mapOf(
-                        Swipe.Down to TestScenes.SceneA,
-                        Swipe(SwipeDirection.Down, pointerCount = 2) to TestScenes.SceneB,
-                        Swipe(SwipeDirection.Right, fromEdge = Edge.Left) to TestScenes.SceneB,
-                        Swipe(SwipeDirection.Down, fromEdge = Edge.Top) to TestScenes.SceneB,
-                    ),
+                userActions = mapOf(Swipe.Down to TestScenes.SceneA),
             ) {
                 Box(Modifier.fillMaxSize())
             }
@@ -256,100 +242,4 @@
         assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
         assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
     }
-
-    @Test
-    fun multiPointerSwipe() {
-        // Start at scene C.
-        currentScene = TestScenes.SceneC
-
-        // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is
-        // detected as a drag event.
-        var touchSlop = 0f
-        rule.setContent {
-            touchSlop = LocalViewConfiguration.current.touchSlop
-            TestContent()
-        }
-
-        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
-        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
-
-        // Swipe down with two fingers.
-        rule.onRoot().performTouchInput {
-            repeat(2) { i -> down(pointerId = i, middle) }
-            repeat(2) { i ->
-                moveBy(pointerId = i, Offset(0f, touchSlop + 10.dp.toPx()), delayMillis = 1_000)
-            }
-        }
-
-        // We are transitioning to B because we used 2 fingers.
-        val transition = layoutState.transitionState
-        assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
-        assertThat((transition as TransitionState.Transition).fromScene)
-            .isEqualTo(TestScenes.SceneC)
-        assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
-
-        // Release the fingers and wait for the animation to end. We are back to C because we only
-        // swiped 10dp.
-        rule.onRoot().performTouchInput { repeat(2) { i -> up(pointerId = i) } }
-        rule.waitForIdle()
-        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
-        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
-    }
-
-    @Test
-    fun defaultEdgeSwipe() {
-        // Start at scene C.
-        currentScene = TestScenes.SceneC
-
-        // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is
-        // detected as a drag event.
-        var touchSlop = 0f
-        rule.setContent {
-            touchSlop = LocalViewConfiguration.current.touchSlop
-            TestContent()
-        }
-
-        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
-        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
-
-        // Swipe down from the top edge.
-        rule.onRoot().performTouchInput {
-            down(middleTop)
-            moveBy(Offset(0f, touchSlop + 10.dp.toPx()), delayMillis = 1_000)
-        }
-
-        // We are transitioning to B (and not A) because we started from the top edge.
-        var transition = layoutState.transitionState
-        assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
-        assertThat((transition as TransitionState.Transition).fromScene)
-            .isEqualTo(TestScenes.SceneC)
-        assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
-
-        // Release the fingers and wait for the animation to end. We are back to C because we only
-        // swiped 10dp.
-        rule.onRoot().performTouchInput { up() }
-        rule.waitForIdle()
-        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
-        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
-
-        // Swipe right from the left edge.
-        rule.onRoot().performTouchInput {
-            down(middleLeft)
-            moveBy(Offset(touchSlop + 10.dp.toPx(), 0f), delayMillis = 1_000)
-        }
-
-        // We are transitioning to B (and not A) because we started from the left edge.
-        transition = layoutState.transitionState
-        assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
-        assertThat((transition as TransitionState.Transition).fromScene)
-            .isEqualTo(TestScenes.SceneC)
-        assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
-
-        // Release the fingers and wait for the animation to end. We are back to C because we only
-        // swiped 10dp.
-        rule.onRoot().performTouchInput { up() }
-        rule.waitForIdle()
-        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
-        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
-    }
 }
diff --git a/packages/SystemUI/res/layout/connected_display_chip.xml b/packages/SystemUI/res/layout/connected_display_chip.xml
index d9df91e..f9a183d 100644
--- a/packages/SystemUI/res/layout/connected_display_chip.xml
+++ b/packages/SystemUI/res/layout/connected_display_chip.xml
@@ -41,6 +41,7 @@
             android:layout_height="wrap_content"
             android:layout_gravity="center"
             android:layout_marginHorizontal="10dp"
+            android:layout_marginVertical="3dp"
             android:scaleType="centerInside"
             android:src="@drawable/stat_sys_connected_display"
             android:tint="@android:color/black" />
diff --git a/packages/SystemUI/res/layout/connected_display_dialog.xml b/packages/SystemUI/res/layout/connected_display_dialog.xml
index 8cfcb68..3f65aa7 100644
--- a/packages/SystemUI/res/layout/connected_display_dialog.xml
+++ b/packages/SystemUI/res/layout/connected_display_dialog.xml
@@ -30,11 +30,11 @@
         android:layout_width="@dimen/connected_display_dialog_logo_size"
         android:layout_height="@dimen/connected_display_dialog_logo_size"
         android:background="@drawable/circular_background"
-        android:backgroundTint="?androidprv:attr/materialColorPrimary"
+        android:backgroundTint="?androidprv:attr/materialColorSecondary"
         android:importantForAccessibility="no"
         android:padding="6dp"
         android:src="@drawable/stat_sys_connected_display"
-        android:tint="?androidprv:attr/materialColorOnPrimary" />
+        android:tint="?androidprv:attr/materialColorOnSecondary" />
 
     <TextView
         android:id="@+id/connected_display_dialog_title"
diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/SharedNotificationContainerPosition.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/SharedNotificationContainerPosition.kt
index b2bc06f..48d3742 100644
--- a/packages/SystemUI/src/com/android/systemui/common/shared/model/SharedNotificationContainerPosition.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/SharedNotificationContainerPosition.kt
@@ -20,4 +20,7 @@
 data class SharedNotificationContainerPosition(
     val top: Float = 0f,
     val bottom: Float = 0f,
+
+    /** Whether any modifications to top/bottom are smoothly animated */
+    val animate: Boolean = false,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 04b2852d..a41bb2f 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -54,6 +54,7 @@
 import com.android.systemui.dreams.dagger.DreamModule;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.FlagDependenciesModule;
 import com.android.systemui.flags.FlagsModule;
 import com.android.systemui.keyboard.KeyboardModule;
 import com.android.systemui.keyevent.data.repository.KeyEventRepositoryModule;
@@ -182,6 +183,7 @@
         DreamModule.class,
         FalsingModule.class,
         FlagsModule.class,
+        FlagDependenciesModule.class,
         FooterActionsModule.class,
         KeyEventRepositoryModule.class,
         KeyboardModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
index e96e318..e872d13 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
@@ -1,3 +1,19 @@
+/*
+ * 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.deviceentry.domain.interactor
 
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
@@ -5,6 +21,8 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository
+import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.TrustRepository
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.model.SceneKey
 import javax.inject.Inject
@@ -14,6 +32,7 @@
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.stateIn
 
 /**
@@ -27,9 +46,11 @@
 @Inject
 constructor(
     @Application private val applicationScope: CoroutineScope,
-    private val repository: DeviceEntryRepository,
+    repository: DeviceEntryRepository,
     private val authenticationInteractor: AuthenticationInteractor,
     sceneInteractor: SceneInteractor,
+    deviceEntryFaceAuthRepository: DeviceEntryFaceAuthRepository,
+    trustRepository: TrustRepository,
 ) {
     /**
      * Whether the device is unlocked.
@@ -73,6 +94,13 @@
                 initialValue = false,
             )
 
+    // Authenticated by a TrustAgent like trusted device, location, etc or by face auth.
+    private val passivelyAuthenticated =
+        merge(
+            trustRepository.isCurrentUserTrusted,
+            deviceEntryFaceAuthRepository.isAuthenticated,
+        )
+
     /**
      * Whether it's currently possible to swipe up to enter the device without requiring
      * authentication. This returns `false` whenever the lockscreen has been dismissed.
@@ -81,10 +109,14 @@
      * UI.
      */
     val canSwipeToEnter =
-        combine(authenticationInteractor.authenticationMethod, isDeviceEntered) {
-                authenticationMethod,
-                isDeviceEntered ->
-                authenticationMethod is AuthenticationMethodModel.Swipe && !isDeviceEntered
+        combine(
+                authenticationInteractor.authenticationMethod.map {
+                    it == AuthenticationMethodModel.Swipe
+                },
+                passivelyAuthenticated,
+                isDeviceEntered
+            ) { isSwipeAuthMethod, passivelyAuthenticated, isDeviceEntered ->
+                (isSwipeAuthMethod || passivelyAuthenticated) && !isDeviceEntered
             }
             .stateIn(
                 scope = applicationScope,
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
new file mode 100644
index 0000000..730a7a6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -0,0 +1,37 @@
+/*
+ * 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 com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.Flags as Classic
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
+import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
+import javax.inject.Inject
+
+/** A class in which engineers can define flag dependencies */
+@SysUISingleton
+class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, handler: Handler) :
+    FlagDependenciesBase(featureFlags, handler) {
+    override fun defineDependencies() {
+        FooterViewRefactor.token dependsOn NotificationIconContainerRefactor.token
+        NotificationIconContainerRefactor.token dependsOn Classic.NOTIFICATION_SHELF_REFACTOR
+
+        // These two flags are effectively linked. We should migrate them to a single aconfig flag.
+        Classic.MIGRATE_NSSL dependsOn Classic.MIGRATE_KEYGUARD_STATUS_VIEW
+        Classic.MIGRATE_KEYGUARD_STATUS_VIEW dependsOn Classic.MIGRATE_NSSL
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependenciesBase.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependenciesBase.kt
new file mode 100644
index 0000000..ae3b501
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependenciesBase.kt
@@ -0,0 +1,158 @@
+/*
+ * 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.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import android.util.Log
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.util.Compile
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.withIncreasedIndent
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import java.io.PrintWriter
+import javax.inject.Inject
+
+/**
+ * This base class provides the helpers necessary to define dependencies between flags from the
+ * different flagging systems; classic and aconfig. This class is to be extended
+ */
+abstract class FlagDependenciesBase(
+    private val featureFlags: FeatureFlagsClassic,
+    private val handler: Handler
+) : CoreStartable {
+    protected abstract fun defineDependencies()
+
+    private val workingDependencies = mutableListOf<Dependency>()
+    private var allDependencies = emptyList<Dependency>()
+    private var unmetDependencies = emptyList<Dependency>()
+
+    override fun start() {
+        defineDependencies()
+        allDependencies = workingDependencies.toList()
+        unmetDependencies = workingDependencies.filter { !it.isMet }
+        workingDependencies.clear()
+        if (unmetDependencies.isNotEmpty()) {
+            handler.warnAboutBadFlagConfiguration(all = allDependencies, unmet = unmetDependencies)
+        }
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.asIndenting().run {
+            println("allDependencies: ${allDependencies.size}")
+            withIncreasedIndent { allDependencies.forEach(::println) }
+            println("unmetDependencies: ${unmetDependencies.size}")
+            withIncreasedIndent { unmetDependencies.forEach(::println) }
+        }
+    }
+
+    /** A dependency where enabling the `alpha` feature depends on enabling the `beta` feature */
+    class Dependency(
+        private val alphaName: String,
+        private val alphaEnabled: Boolean,
+        private val betaName: String,
+        private val betaEnabled: Boolean
+    ) {
+        val isMet = !alphaEnabled || betaEnabled
+        override fun toString(): String {
+            val isMetBullet = if (isMet) "+" else "-"
+            return "$isMetBullet $alphaName ($alphaEnabled) DEPENDS ON $betaName ($betaEnabled)"
+        }
+    }
+
+    protected infix fun UnreleasedFlag.dependsOn(other: UnreleasedFlag) =
+        addDependency(this.token, other.token)
+    protected infix fun ReleasedFlag.dependsOn(other: UnreleasedFlag) =
+        addDependency(this.token, other.token)
+    protected infix fun ReleasedFlag.dependsOn(other: ReleasedFlag) =
+        addDependency(this.token, other.token)
+    protected infix fun FlagToken.dependsOn(other: UnreleasedFlag) =
+        addDependency(this, other.token)
+    protected infix fun FlagToken.dependsOn(other: ReleasedFlag) = addDependency(this, other.token)
+    protected infix fun FlagToken.dependsOn(other: FlagToken) = addDependency(this, other)
+
+    private val UnreleasedFlag.token
+        get() = FlagToken("classic.$name", featureFlags.isEnabled(this))
+    private val ReleasedFlag.token
+        get() = FlagToken("classic.$name", featureFlags.isEnabled(this))
+
+    /** Add a dependency to the working list */
+    private fun addDependency(first: FlagToken, second: FlagToken) {
+        if (!Compile.IS_DEBUG) return // `user` builds should omit all this code
+        workingDependencies.add(
+            Dependency(first.name, first.isEnabled, second.name, second.isEnabled)
+        )
+    }
+
+    /** An interface which handles a warning about a bad flag configuration. */
+    interface Handler {
+        fun warnAboutBadFlagConfiguration(all: List<Dependency>, unmet: List<Dependency>)
+    }
+}
+
+/**
+ * A flag dependencies handler which posts a notification and logs to logcat that the configuration
+ * is invalid.
+ */
+@SysUISingleton
+class FlagDependenciesNotifier
+@Inject
+constructor(
+    private val context: Context,
+    private val notifManager: NotificationManager,
+) : FlagDependenciesBase.Handler {
+    override fun warnAboutBadFlagConfiguration(
+        all: List<FlagDependenciesBase.Dependency>,
+        unmet: List<FlagDependenciesBase.Dependency>
+    ) {
+        val title = "Invalid flag dependencies: ${unmet.size} of ${all.size}"
+        val details = unmet.joinToString("\n")
+        Log.e("FlagDependencies", "$title:\n$details")
+        val channel = NotificationChannel("FLAGS", "Flags", NotificationManager.IMPORTANCE_DEFAULT)
+        val notification =
+            Notification.Builder(context, channel.id)
+                .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
+                .setContentTitle(title)
+                .setContentText(details)
+                .setStyle(Notification.BigTextStyle().bigText(details))
+                .build()
+        notifManager.createNotificationChannel(channel)
+        notifManager.notify("flags", 0, notification)
+    }
+}
+
+@Module
+abstract class FlagDependenciesModule {
+
+    /** Inject into FlagDependencies. */
+    @Binds
+    @IntoMap
+    @ClassKey(FlagDependencies::class)
+    abstract fun bindFlagDependencies(sysui: FlagDependencies): CoreStartable
+
+    /** Bind the flag dependencies handler */
+    @Binds
+    abstract fun bindFlagDependenciesHandler(
+        handler: FlagDependenciesNotifier
+    ): FlagDependenciesBase.Handler
+}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/OWNERS b/packages/SystemUI/src/com/android/systemui/flags/OWNERS
index c9d2db1..57ebccb 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/flags/OWNERS
@@ -10,3 +10,6 @@
 alexflo@google.com
 dsandler@android.com
 adamcohen@google.com
+
+# Anyone in System UI can declare dependencies between flags
+per-file FlagDependencies.kt = file:../../../../../OWNERS
diff --git a/packages/SystemUI/src/com/android/systemui/flags/RefactorFlagUtils.kt b/packages/SystemUI/src/com/android/systemui/flags/RefactorFlagUtils.kt
index 2aa397f..ae67e60 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/RefactorFlagUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/RefactorFlagUtils.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.flags
 
+import android.os.Build
 import android.util.Log
 
 /**
@@ -25,6 +26,7 @@
  * ```
  * object SomeRefactor {
  *     const val FLAG_NAME = Flags.SOME_REFACTOR
+ *     val token: FlagToken get() = FlagToken(FLAG_NAME, isEnabled)
  *     @JvmStatic inline val isEnabled get() = Flags.someRefactor()
  *     @JvmStatic inline fun isUnexpectedlyInLegacyMode() =
  *         RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
@@ -32,6 +34,11 @@
  *         RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
  * }
  * ```
+ *
+ * Legacy mode crashes can be disabled with the command:
+ * ```
+ * adb shell setprop log.tag.RefactorFlagAssert silent
+ * ```
  */
 @Suppress("NOTHING_TO_INLINE")
 object RefactorFlagUtils {
@@ -51,8 +58,7 @@
     inline fun isUnexpectedlyInLegacyMode(isEnabled: Boolean, flagName: Any): Boolean {
         val inLegacyMode = !isEnabled
         if (inLegacyMode) {
-            val message = "New code path expects $flagName to be enabled."
-            Log.wtf("RefactorFlag", message, IllegalStateException(message))
+            assertOnEngBuild("New code path expects $flagName to be enabled.")
         }
         return inLegacyMode
     }
@@ -71,4 +77,37 @@
      */
     inline fun assertInLegacyMode(isEnabled: Boolean, flagName: Any) =
         check(!isEnabled) { "Legacy code path not supported when $flagName is enabled." }
+
+    /**
+     * This will [Log.wtf] with the given message, assuming [ASSERT_TAG] is loggable at that level.
+     * This means an engineer can prevent this from crashing by running the command:
+     * ```
+     * adb shell setprop log.tag.RefactorFlagAssert silent
+     * ```
+     */
+    fun assertOnEngBuild(message: String) {
+        if (Log.isLoggable(ASSERT_TAG, Log.ASSERT)) {
+            val exception = if (Build.isDebuggable()) IllegalStateException(message) else null
+            Log.wtf(ASSERT_TAG, message, exception)
+        } else if (Log.isLoggable(STANDARD_TAG, Log.WARN)) {
+            Log.w(STANDARD_TAG, message)
+        }
+    }
+
+    /**
+     * Tag used to determine if an incorrect flag guard should crash System UI running an eng build.
+     * This is enabled by default. To disable, run:
+     * ```
+     * adb shell setprop log.tag.RefactorFlagAssert silent
+     * ```
+     */
+    private const val ASSERT_TAG = "RefactorFlagAssert"
+
+    /** Tag used for non-crashing logs or when the [ASSERT_TAG] has been silenced. */
+    private const val STANDARD_TAG = "RefactorFlag"
+}
+
+/** An object which allows dependency tracking */
+data class FlagToken(val name: String, val isEnabled: Boolean) {
+    override fun toString(): String = "$name (${if (isEnabled) "enabled" else "disabled"})"
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index 4d5c503..67a12b0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -22,6 +22,8 @@
 import android.view.View.OnLayoutChangeListener
 import android.view.ViewGroup
 import android.view.ViewGroup.OnHierarchyChangeListener
+import android.view.WindowInsets
+import android.view.WindowInsets.Type
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.internal.jank.InteractionJankMonitor
@@ -242,11 +244,18 @@
             }
         )
 
+        view.setOnApplyWindowInsetsListener { v: View, insets: WindowInsets ->
+            val insetTypes = WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout()
+            viewModel.topInset = insets.getInsetsIgnoringVisibility(insetTypes).top
+            insets
+        }
+
         return object : DisposableHandle {
             override fun dispose() {
                 disposableHandle.dispose()
                 view.removeOnLayoutChangeListener(onLayoutChangeListener)
                 view.setOnHierarchyChangeListener(null)
+                view.setOnApplyWindowInsetsListener(null)
                 childViews.clear()
             }
         }
@@ -288,7 +297,6 @@
             oldBottom: Int
         ) {
             val nsslPlaceholder = v.findViewById(R.id.nssl_placeholder) as View?
-
             if (nsslPlaceholder != null) {
                 // After layout, ensure the notifications are positioned correctly
                 viewModel.onSharedNotificationContainerPositionChanged(
@@ -296,6 +304,11 @@
                     nsslPlaceholder.bottom.toFloat(),
                 )
             }
+
+            val ksv = v.findViewById(R.id.keyguard_status_view) as View?
+            if (ksv != null) {
+                viewModel.statusViewTop = ksv.top
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 1f98082..e12da53 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -65,7 +65,12 @@
      */
     private val previewMode = MutableStateFlow(PreviewMode())
 
-    public var clockControllerProvider: Provider<ClockController>? = null
+    var clockControllerProvider: Provider<ClockController>? = null
+
+    /** System insets that keyguard needs to stay out of */
+    var topInset: Int = 0
+    /** Status view top, without translation added in */
+    var statusViewTop: Int = 0
 
     val burnInLayerVisibility: Flow<Int> =
         keyguardTransitionInteractor.startedKeyguardState
@@ -102,9 +107,12 @@
                     scale = MathUtils.lerp(burnIn.scale, 1f, 1f - interpolation),
                 )
             } else {
+                // Ensure the desired translation doesn't encroach on the top inset
+                val burnInY = MathUtils.lerp(0, burnIn.translationY, interpolation).toInt()
+                val translationY = -(statusViewTop - Math.max(topInset, statusViewTop + burnInY))
                 BurnInModel(
                     translationX = MathUtils.lerp(0, burnIn.translationX, interpolation).toInt(),
-                    translationY = MathUtils.lerp(0, burnIn.translationY, interpolation).toInt(),
+                    translationY = translationY,
                     scale = MathUtils.lerp(burnIn.scale, 1f, 1f - interpolation),
                     scaleClockOnly = true,
                 )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/shared/FooterViewRefactor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/shared/FooterViewRefactor.kt
index 94e70e5..7e6044e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/shared/FooterViewRefactor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/shared/FooterViewRefactor.kt
@@ -17,13 +17,19 @@
 package com.android.systemui.statusbar.notification.footer.shared
 
 import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
 import com.android.systemui.flags.RefactorFlagUtils
 
 /** Helper for reading or using the FooterView refactor flag state. */
 @Suppress("NOTHING_TO_INLINE")
 object FooterViewRefactor {
+    /** The aconfig flag name */
     const val FLAG_NAME = Flags.FLAG_NOTIFICATIONS_FOOTER_VIEW_REFACTOR
 
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
     /** Is the refactor enabled */
     @JvmStatic
     inline val isEnabled
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BigPictureIconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BigPictureIconManager.kt
index 79bdd1f..e6deb8b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BigPictureIconManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BigPictureIconManager.kt
@@ -117,11 +117,6 @@
 
     @WorkerThread
     override fun updateIcon(drawableConsumer: NotificationDrawableConsumer, icon: Icon?): Runnable {
-        if (this.drawableConsumer != null && this.drawableConsumer != drawableConsumer) {
-            Log.wtf(TAG, "A consumer is already set for this iconManager.")
-            return Runnable {}
-        }
-
         this.drawableConsumer = drawableConsumer
         this.lastLoadingJob?.cancel()
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationIconContainerRefactor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationIconContainerRefactor.kt
index dee609c..a08af75 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationIconContainerRefactor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationIconContainerRefactor.kt
@@ -16,13 +16,19 @@
 package com.android.systemui.statusbar.notification.shared
 
 import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
 import com.android.systemui.flags.RefactorFlagUtils
 
 /** Helper for reading or using the NotificationIconContainer refactor flag state. */
 @Suppress("NOTHING_TO_INLINE")
 object NotificationIconContainerRefactor {
+    /** The aconfig flag name */
     const val FLAG_NAME = Flags.FLAG_NOTIFICATIONS_ICON_CONTAINER_REFACTOR
 
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
     /** Is the refactor enabled? */
     @JvmStatic
     inline val isEnabled
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
index 2af7181..6785da4 100644
--- 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
@@ -59,10 +59,8 @@
 
                 launch {
                     viewModel.position.collect {
-                        controller.updateTopPadding(
-                            it.top,
-                            controller.isAddOrRemoveAnimationPending()
-                        )
+                        val animate = it.animate || controller.isAddOrRemoveAnimationPending()
+                        controller.updateTopPadding(it.top, animate)
                     }
                 }
 
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
index 1229cb9..b86b5dc 100644
--- 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
@@ -25,6 +25,7 @@
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
+import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
@@ -118,8 +119,15 @@
                     }
                 }
             } else {
-                interactor.topPosition.map { top ->
-                    keyguardInteractor.sharedNotificationContainerPosition.value.copy(top = top)
+                interactor.topPosition.sample(shadeInteractor.qsExpansion, ::Pair).map {
+                    (top, qsExpansion) ->
+                    // When QS expansion > 0, it should directly set the top padding so do not
+                    // animate it
+                    val animate = qsExpansion == 0f
+                    keyguardInteractor.sharedNotificationContainerPosition.value.copy(
+                        top = top,
+                        animate = animate
+                    )
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
index dd32434..649a4ac 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
@@ -512,7 +512,7 @@
     }
 
     public void showIconIsolated(StatusBarIconView icon, boolean animated) {
-        mNotificationIcons.showIconIsolated(icon, animated);
+        mNotificationIcons.showIconIsolatedLegacy(icon, animated);
     }
 
     public void setIsolatedIconLocation(@NotNull Rect iconDrawingRect, boolean requireStateUpdate) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
index efb8e2c..75a697f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
@@ -701,13 +701,13 @@
     }
 
     @Deprecated
-    public void showIconIsolated(StatusBarIconView icon, boolean animated) {
+    public void showIconIsolatedLegacy(StatusBarIconView icon, boolean animated) {
         NotificationIconContainerRefactor.assertInLegacyMode();
         if (animated) {
-            showIconIsolatedAnimated(icon, null);
-        } else {
-            showIconIsolated(icon);
+            mIsolatedIconForAnimation = icon != null ? icon : mIsolatedIcon;
         }
+        mIsolatedIcon = icon;
+        updateState();
     }
 
     public void showIconIsolatedAnimated(StatusBarIconView icon,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
index c13fde7..aebadc5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
@@ -1,3 +1,19 @@
+/*
+ * 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.deviceentry.domain.interactor
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -6,6 +22,8 @@
 import com.android.systemui.authentication.data.model.AuthenticationMethodModel
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.FakeTrustRepository
 import com.android.systemui.scene.SceneTestUtils
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
@@ -24,6 +42,8 @@
     private val utils = SceneTestUtils(this)
     private val testScope = utils.testScope
     private val repository: FakeDeviceEntryRepository = utils.deviceEntryRepository
+    private val faceAuthRepository = FakeDeviceEntryFaceAuthRepository()
+    private val trustRepository = FakeTrustRepository()
     private val sceneInteractor = utils.sceneInteractor()
     private val authenticationInteractor = utils.authenticationInteractor()
     private val underTest =
@@ -31,6 +51,8 @@
             repository = repository,
             authenticationInteractor = authenticationInteractor,
             sceneInteractor = sceneInteractor,
+            faceAuthRepository = faceAuthRepository,
+            trustRepository = trustRepository,
         )
 
     @Test
@@ -171,6 +193,40 @@
         }
 
     @Test
+    fun canSwipeToEnter_whenTrustedByTrustManager_isTrue() =
+        testScope.runTest {
+            val canSwipeToEnter by collectLastValue(underTest.canSwipeToEnter)
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Password
+            )
+            switchToScene(SceneKey.Lockscreen)
+            assertThat(canSwipeToEnter).isFalse()
+
+            trustRepository.setCurrentUserTrusted(true)
+            runCurrent()
+            faceAuthRepository.isAuthenticated.value = false
+
+            assertThat(canSwipeToEnter).isTrue()
+        }
+
+    @Test
+    fun canSwipeToEnter_whenAuthenticatedByFace_isTrue() =
+        testScope.runTest {
+            val canSwipeToEnter by collectLastValue(underTest.canSwipeToEnter)
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Password
+            )
+            switchToScene(SceneKey.Lockscreen)
+            assertThat(canSwipeToEnter).isFalse()
+
+            faceAuthRepository.isAuthenticated.value = true
+            runCurrent()
+            trustRepository.setCurrentUserTrusted(false)
+
+            assertThat(canSwipeToEnter).isTrue()
+        }
+
+    @Test
     fun isAuthenticationRequired_lockedAndSecured_true() =
         testScope.runTest {
             utils.deviceEntryRepository.setUnlocked(false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagDependenciesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagDependenciesTest.kt
new file mode 100644
index 0000000..936aa8b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagDependenciesTest.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import java.io.PrintWriter
+import kotlin.test.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class FlagDependenciesTest : SysuiTestCase() {
+    @Test
+    fun testRelease() {
+        testFlagDependencies(teamfood = false).start()
+    }
+
+    @Test
+    fun testTeamfood() {
+        testFlagDependencies(teamfood = true).start()
+    }
+
+    private fun testFlagDependencies(teamfood: Boolean) =
+        FlagDependencies(TestFeatureFlags(teamfood = teamfood), TestHandler())
+
+    private class TestHandler : FlagDependenciesBase.Handler {
+        override fun warnAboutBadFlagConfiguration(
+            all: List<FlagDependenciesBase.Dependency>,
+            unmet: List<FlagDependenciesBase.Dependency>
+        ) {
+            val title = "${unmet.size} invalid of ${all.size} flag dependencies"
+            val details = unmet.joinToString("\n")
+            fail("$title:\n$details")
+        }
+    }
+
+    private class TestFeatureFlags(val teamfood: Boolean) : FeatureFlagsClassic {
+        private val unsupported: Nothing
+            get() = fail("Unsupported")
+
+        override fun isEnabled(flag: ReleasedFlag): Boolean = true
+        override fun isEnabled(flag: UnreleasedFlag): Boolean = teamfood && flag.teamfood
+        override fun isEnabled(flag: ResourceBooleanFlag): Boolean = unsupported
+        override fun isEnabled(flag: SysPropBooleanFlag): Boolean = unsupported
+        override fun getString(flag: StringFlag): String = unsupported
+        override fun getString(flag: ResourceStringFlag): String = unsupported
+        override fun getInt(flag: IntFlag): Int = unsupported
+        override fun getInt(flag: ResourceIntFlag): Int = unsupported
+        override fun addListener(flag: Flag<*>, listener: FlagListenable.Listener) = unsupported
+        override fun removeListener(listener: FlagListenable.Listener) = unsupported
+        override fun dump(writer: PrintWriter, args: Array<out String>?) = unsupported
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
index 4f545cb..b80771f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
@@ -179,6 +179,8 @@
             val translationY by collectLastValue(underTest.translationY)
             val scale by collectLastValue(underTest.scale)
 
+            underTest.statusViewTop = 100
+
             // Set to dozing (on AOD)
             dozeAmountTransitionStep.emit(TransitionStep(value = 1f))
             // Trigger a change to the burn-in model
@@ -200,6 +202,37 @@
         }
 
     @Test
+    fun translationAndScaleFromBurnFullyDozingStaysOutOfTopInset() =
+        testScope.runTest {
+            val translationX by collectLastValue(underTest.translationX)
+            val translationY by collectLastValue(underTest.translationY)
+            val scale by collectLastValue(underTest.scale)
+
+            underTest.statusViewTop = 100
+            underTest.topInset = 80
+
+            // Set to dozing (on AOD)
+            dozeAmountTransitionStep.emit(TransitionStep(value = 1f))
+            // Trigger a change to the burn-in model
+            burnInFlow.value =
+                BurnInModel(
+                    translationX = 20,
+                    translationY = -30,
+                    scale = 0.5f,
+                )
+            assertThat(translationX).isEqualTo(20)
+            // -20 instead of -30, due to inset of 80
+            assertThat(translationY).isEqualTo(-20)
+            assertThat(scale).isEqualTo(Pair(0.5f, true /* scaleClockOnly */))
+
+            // Set to the beginning of GONE->AOD transition
+            goneToAodTransitionStep.emit(TransitionStep(value = 0f))
+            assertThat(translationX).isEqualTo(0)
+            assertThat(translationY).isEqualTo(0)
+            assertThat(scale).isEqualTo(Pair(1f, true /* scaleClockOnly */))
+        }
+
+    @Test
     fun translationAndScaleFromBurnInUseScaleOnly() =
         testScope.runTest {
             whenever(clockController.config.useAlternateSmartspaceAODTransition).thenReturn(true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt
index 1bb7b61..2bad9f5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt
@@ -143,18 +143,44 @@
         }
 
     @Test
-    fun onIconUpdated_consumerAlreadySet_nothingHappens() =
+    fun onIconUpdated_consumerAlreadySet_newConsumerIsUpdatedWithPlaceholder() =
         testScope.runTest {
             // GIVEN a consumer is set
-            val otherConsumer: NotificationDrawableConsumer = mock()
             iconManager.updateIcon(mockConsumer, supportedIcon).run()
             clearInvocations(mockConsumer)
 
             // WHEN a new consumer is set
-            iconManager.updateIcon(otherConsumer, unsupportedIcon).run()
+            val newConsumer: NotificationDrawableConsumer = mock()
+            iconManager.updateIcon(newConsumer, supportedIcon).run()
 
-            // THEN nothing happens
-            verifyZeroInteractions(mockConsumer, otherConsumer)
+            // THEN the new consumer is updated
+            verify(newConsumer).setImageDrawable(drawableCaptor.capture())
+            assertIsPlaceHolder(drawableCaptor.value)
+            assertSize(drawableCaptor.value)
+            // AND nothing happens on the old consumer
+            verifyZeroInteractions(mockConsumer)
+        }
+
+    @Test
+    fun onIconUpdated_consumerAlreadySet_newConsumerIsUpdatedWithFullImage() =
+        testScope.runTest {
+            // GIVEN a consumer is set
+            iconManager.updateIcon(mockConsumer, supportedIcon).run()
+            // AND an icon is loaded
+            iconManager.onViewShown(true)
+            runCurrent()
+            clearInvocations(mockConsumer)
+
+            // WHEN a new consumer is set
+            val newConsumer: NotificationDrawableConsumer = mock()
+            iconManager.updateIcon(newConsumer, supportedIcon).run()
+
+            // THEN the new consumer is updated
+            verify(newConsumer).setImageDrawable(drawableCaptor.capture())
+            assertIsFullImage(drawableCaptor.value)
+            assertSize(drawableCaptor.value)
+            // AND nothing happens on the old consumer
+            verifyZeroInteractions(mockConsumer)
         }
 
     @Test
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
index 60421c98..978fafe 100644
--- 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
@@ -314,7 +314,26 @@
             sharedNotificationContainerInteractor.setTopPosition(10f)
 
             assertThat(position)
-                .isEqualTo(SharedNotificationContainerPosition(top = 10f, bottom = 0f))
+                .isEqualTo(
+                    SharedNotificationContainerPosition(top = 10f, bottom = 0f, animate = true)
+                )
+        }
+
+    @Test
+    fun positionOnQS() =
+        testScope.runTest {
+            val position by collectLastValue(underTest.position)
+
+            // Start on lockscreen with shade expanded
+            showLockscreenWithQSExpanded()
+
+            // When not in split shade
+            sharedNotificationContainerInteractor.setTopPosition(10f)
+
+            assertThat(position)
+                .isEqualTo(
+                    SharedNotificationContainerPosition(top = 10f, bottom = 0f, animate = false)
+                )
         }
 
     @Test
@@ -390,6 +409,18 @@
         )
     }
 
+    private suspend fun showLockscreenWithQSExpanded() {
+        shadeRepository.setLockscreenShadeExpansion(0f)
+        shadeRepository.setQsExpansion(1f)
+        keyguardRepository.setStatusBarState(StatusBarState.SHADE_LOCKED)
+        keyguardTransitionRepository.sendTransitionStep(
+            TransitionStep(
+                to = KeyguardState.LOCKSCREEN,
+                transitionState = TransitionState.FINISHED
+            )
+        )
+    }
+
     @SysUISingleton
     @Component(
         modules =
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/FakeDeviceEntryDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/FakeDeviceEntryDataLayerModule.kt
index ef02bdd..44286b7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/FakeDeviceEntryDataLayerModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/FakeDeviceEntryDataLayerModule.kt
@@ -16,6 +16,16 @@
 package com.android.systemui.deviceentry.data
 
 import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepositoryModule
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepositoryModule
+import com.android.systemui.keyguard.data.repository.FakeTrustRepositoryModule
 import dagger.Module
 
-@Module(includes = [FakeDeviceEntryRepositoryModule::class]) object FakeDeviceEntryDataLayerModule
+@Module(
+    includes =
+        [
+            FakeDeviceEntryRepositoryModule::class,
+            FakeTrustRepositoryModule::class,
+            FakeDeviceEntryFaceAuthRepositoryModule::class,
+        ]
+)
+object FakeDeviceEntryDataLayerModule
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/FakeKeyguardDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/FakeKeyguardDataLayerModule.kt
index 6710072..abf72af 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/FakeKeyguardDataLayerModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/FakeKeyguardDataLayerModule.kt
@@ -16,7 +16,6 @@
 package com.android.systemui.keyguard.data
 
 import com.android.systemui.keyguard.data.repository.FakeCommandQueueModule
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepositoryModule
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepositoryModule
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepositoryModule
 import dagger.Module
@@ -25,7 +24,6 @@
     includes =
         [
             FakeCommandQueueModule::class,
-            FakeDeviceEntryFaceAuthRepositoryModule::class,
             FakeKeyguardRepositoryModule::class,
             FakeKeyguardTransitionRepositoryModule::class,
         ]
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeTrustRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeTrustRepository.kt
index 482126d..cd83c2f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeTrustRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeTrustRepository.kt
@@ -18,13 +18,18 @@
 package com.android.systemui.keyguard.data.repository
 
 import com.android.keyguard.TrustGrantFlags
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.shared.model.TrustModel
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 
-class FakeTrustRepository : TrustRepository {
+@SysUISingleton
+class FakeTrustRepository @Inject constructor() : TrustRepository {
     private val _isTrustUsuallyManaged = MutableStateFlow(false)
     override val isCurrentUserTrustUsuallyManaged: StateFlow<Boolean>
         get() = _isTrustUsuallyManaged
@@ -63,3 +68,8 @@
         _isTrustUsuallyManaged.value = value
     }
 }
+
+@Module
+interface FakeTrustRepositoryModule {
+    @Binds fun bindFake(fake: FakeTrustRepository): TrustRepository
+}
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 17384351..bdddc04 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
@@ -42,9 +42,13 @@
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
 import com.android.systemui.flags.FakeFeatureFlagsClassic
 import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository
 import com.android.systemui.keyguard.data.repository.FakeCommandQueue
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.FakeTrustRepository
 import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import com.android.systemui.keyguard.data.repository.TrustRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.power.data.repository.FakePowerRepository
 import com.android.systemui.power.domain.interactor.PowerInteractorFactory
@@ -155,12 +159,16 @@
         repository: DeviceEntryRepository = deviceEntryRepository,
         authenticationInteractor: AuthenticationInteractor,
         sceneInteractor: SceneInteractor,
+        faceAuthRepository: DeviceEntryFaceAuthRepository = FakeDeviceEntryFaceAuthRepository(),
+        trustRepository: TrustRepository = FakeTrustRepository(),
     ): DeviceEntryInteractor {
         return DeviceEntryInteractor(
             applicationScope = applicationScope(),
             repository = repository,
             authenticationInteractor = authenticationInteractor,
             sceneInteractor = sceneInteractor,
+            deviceEntryFaceAuthRepository = faceAuthRepository,
+            trustRepository = trustRepository,
         )
     }
 
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 70c449f..a036383 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -326,7 +326,9 @@
         mSensorController = new SensorController(this, mDeviceId,
                 mParams.getVirtualSensorCallback(), mParams.getVirtualSensorConfigs());
         mCameraAccessController = cameraAccessController;
-        mCameraAccessController.startObservingIfNeeded();
+        if (mCameraAccessController != null) {
+            mCameraAccessController.startObservingIfNeeded();
+        }
         if (!Flags.streamPermissions()) {
             mPermissionDialogComponent = getPermissionDialogComponent();
         } else {
@@ -566,7 +568,9 @@
             }
 
             mAppToken.unlinkToDeath(this, 0);
-            mCameraAccessController.stopObservingIfNeeded();
+            if (mCameraAccessController != null) {
+                mCameraAccessController.stopObservingIfNeeded();
+            }
 
             mInputController.close();
             mSensorController.close();
@@ -586,7 +590,9 @@
     @Override
     @RequiresPermission(android.Manifest.permission.CAMERA_INJECT_EXTERNAL_CAMERA)
     public void onRunningAppsChanged(ArraySet<Integer> runningUids) {
-        mCameraAccessController.blockCameraAccessIfNeeded(runningUids);
+        if (mCameraAccessController != null) {
+            mCameraAccessController.blockCameraAccessIfNeeded(runningUids);
+        }
         mRunningAppsChangedCallback.accept(runningUids);
     }
 
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index 3031a84..959f69e 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -187,6 +187,9 @@
     }
 
     CameraAccessController getCameraAccessController(UserHandle userHandle) {
+        if (Flags.streamCamera()) {
+            return null;
+        }
         int userId = userHandle.getIdentifier();
         synchronized (mVirtualDeviceManagerLock) {
             for (int i = 0; i < mVirtualDevices.size(); i++) {
diff --git a/services/core/Android.bp b/services/core/Android.bp
index c220528..7dbf61b 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -192,7 +192,6 @@
         "com.android.sysprop.watchdog",
         "ImmutabilityAnnotation",
         "securebox",
-        "android.content.pm.flags-aconfig-java",
         "apache-commons-math",
         "backstage_power_flags_lib",
         "notification_flags_lib",
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index e88d0c6..ae79f19 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -20305,7 +20305,7 @@
         final long token = Binder.clearCallingIdentity();
 
         try {
-            return mOomAdjuster.mCachedAppOptimizer.isFreezerSupported();
+            return CachedAppOptimizer.isFreezerSupported();
         } finally {
             Binder.restoreCallingIdentity(token);
         }
@@ -20433,4 +20433,21 @@
             return index >= 0 && !mMediaProjectionTokenMap.valueAt(index).isEmpty();
         }
     }
+
+    /**
+     * Deal with binder transactions to frozen apps.
+     *
+     * @param debugPid The binder transaction sender
+     * @param code The binder transaction code
+     * @param flags The binder transaction flags
+     * @param err The binder transaction error
+     */
+    @Override
+    public void frozenBinderTransactionDetected(int debugPid, int code, int flags, int err) {
+        final ProcessRecord app;
+        synchronized (mPidsSelfLocked) {
+            app = mPidsSelfLocked.get(debugPid);
+        }
+        mOomAdjuster.mCachedAppOptimizer.binderError(debugPid, app, code, flags, err);
+    }
 }
diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index 6005b64..68af626 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -52,6 +52,8 @@
 import android.app.ActivityManagerInternal.OomAdjReason;
 import android.app.ActivityThread;
 import android.app.ApplicationExitInfo;
+import android.app.ApplicationExitInfo.Reason;
+import android.app.ApplicationExitInfo.SubReason;
 import android.app.IApplicationThread;
 import android.database.ContentObserver;
 import android.net.Uri;
@@ -67,6 +69,7 @@
 import android.provider.DeviceConfig.Properties;
 import android.provider.Settings;
 import android.text.TextUtils;
+import android.util.ArraySet;
 import android.util.EventLog;
 import android.util.IntArray;
 import android.util.Pair;
@@ -75,6 +78,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.BinderfsStatsReader;
 import com.android.internal.os.ProcLocksReader;
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.server.ServiceThread;
@@ -131,6 +135,12 @@
             "freeze_binder_offset";
     @VisibleForTesting static final String KEY_FREEZER_BINDER_THRESHOLD =
             "freeze_binder_threshold";
+    @VisibleForTesting static final String KEY_FREEZER_BINDER_CALLBACK_ENABLED =
+            "freeze_binder_callback_enabled";
+    @VisibleForTesting static final String KEY_FREEZER_BINDER_CALLBACK_THROTTLE =
+            "freeze_binder_callback_throttle";
+    @VisibleForTesting static final String KEY_FREEZER_BINDER_ASYNC_THRESHOLD =
+            "freeze_binder_async_threshold";
 
     static final int UNFREEZE_REASON_NONE =
             FrameworkStatsLog.APP_FREEZE_CHANGED__UNFREEZE_REASON_V2__UFR_NONE;
@@ -270,6 +280,9 @@
     @VisibleForTesting static final long DEFAULT_FREEZER_BINDER_DIVISOR = 4;
     @VisibleForTesting static final int DEFAULT_FREEZER_BINDER_OFFSET = 500;
     @VisibleForTesting static final long DEFAULT_FREEZER_BINDER_THRESHOLD = 1_000;
+    @VisibleForTesting static final boolean DEFAULT_FREEZER_BINDER_CALLBACK_ENABLED = true;
+    @VisibleForTesting static final long DEFAULT_FREEZER_BINDER_CALLBACK_THROTTLE = 10_000L;
+    @VisibleForTesting static final int DEFAULT_FREEZER_BINDER_ASYNC_THRESHOLD = 1_024;
 
     @VisibleForTesting static final Uri CACHED_APP_FREEZER_ENABLED_URI = Settings.Global.getUriFor(
                 Settings.Global.CACHED_APPS_FREEZER_ENABLED);
@@ -312,6 +325,7 @@
     static final int COMPACT_NATIVE_MSG = 5;
     static final int UID_FROZEN_STATE_CHANGED_MSG = 6;
     static final int DEADLOCK_WATCHDOG_MSG = 7;
+    static final int BINDER_ERROR_MSG = 8;
 
     // When free swap falls below this percentage threshold any full (file + anon)
     // compactions will be downgraded to file only compactions to reduce pressure
@@ -408,7 +422,10 @@
                             } else if (KEY_FREEZER_BINDER_ENABLED.equals(name)
                                     || KEY_FREEZER_BINDER_DIVISOR.equals(name)
                                     || KEY_FREEZER_BINDER_THRESHOLD.equals(name)
-                                    || KEY_FREEZER_BINDER_OFFSET.equals(name)) {
+                                    || KEY_FREEZER_BINDER_OFFSET.equals(name)
+                                    || KEY_FREEZER_BINDER_CALLBACK_ENABLED.equals(name)
+                                    || KEY_FREEZER_BINDER_CALLBACK_THROTTLE.equals(name)
+                                    || KEY_FREEZER_BINDER_ASYNC_THRESHOLD.equals(name)) {
                                 updateFreezerBinderState();
                             }
                         }
@@ -480,7 +497,15 @@
     @VisibleForTesting volatile int mFreezerBinderOffset = DEFAULT_FREEZER_BINDER_OFFSET;
     @GuardedBy("mPhenotypeFlagLock")
     @VisibleForTesting volatile long mFreezerBinderThreshold = DEFAULT_FREEZER_BINDER_THRESHOLD;
-
+    @GuardedBy("mPhenotypeFlagLock")
+    @VisibleForTesting volatile boolean mFreezerBinderCallbackEnabled =
+            DEFAULT_FREEZER_BINDER_CALLBACK_ENABLED;
+    @GuardedBy("mPhenotypeFlagLock")
+    @VisibleForTesting volatile long mFreezerBinderCallbackThrottle =
+            DEFAULT_FREEZER_BINDER_CALLBACK_THROTTLE;
+    @GuardedBy("mPhenotypeFlagLock")
+    @VisibleForTesting volatile int mFreezerBinderAsyncThreshold =
+            DEFAULT_FREEZER_BINDER_ASYNC_THRESHOLD;
 
     // Handler on which compaction runs.
     @VisibleForTesting
@@ -488,6 +513,7 @@
     private Handler mFreezeHandler;
     @GuardedBy("mProcLock")
     private boolean mFreezerOverride = false;
+    private long mFreezerBinderCallbackLast = -1;
 
     @VisibleForTesting volatile long mFreezerDebounceTimeout = DEFAULT_FREEZER_DEBOUNCE_TIMEOUT;
     @VisibleForTesting volatile boolean mFreezerExemptInstPkg = DEFAULT_FREEZER_EXEMPT_INST_PKG;
@@ -790,6 +816,12 @@
             pw.println("  " + KEY_FREEZER_BINDER_THRESHOLD + "=" + mFreezerBinderThreshold);
             pw.println("  " + KEY_FREEZER_BINDER_DIVISOR + "=" + mFreezerBinderDivisor);
             pw.println("  " + KEY_FREEZER_BINDER_OFFSET + "=" + mFreezerBinderOffset);
+            pw.println("  " + KEY_FREEZER_BINDER_CALLBACK_ENABLED + "="
+                    + mFreezerBinderCallbackEnabled);
+            pw.println("  " + KEY_FREEZER_BINDER_CALLBACK_THROTTLE + "="
+                    + mFreezerBinderCallbackThrottle);
+            pw.println("  " + KEY_FREEZER_BINDER_ASYNC_THRESHOLD + "="
+                    + mFreezerBinderAsyncThreshold);
             synchronized (mProcLock) {
                 int size = mFrozenProcesses.size();
                 pw.println("  Apps frozen: " + size);
@@ -1309,10 +1341,22 @@
         mFreezerBinderThreshold = DeviceConfig.getLong(
                 DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT,
                 KEY_FREEZER_BINDER_THRESHOLD, DEFAULT_FREEZER_BINDER_THRESHOLD);
+        mFreezerBinderCallbackEnabled = DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT,
+                KEY_FREEZER_BINDER_CALLBACK_ENABLED, DEFAULT_FREEZER_BINDER_CALLBACK_ENABLED);
+        mFreezerBinderCallbackThrottle = DeviceConfig.getLong(
+                DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT,
+                KEY_FREEZER_BINDER_CALLBACK_THROTTLE, DEFAULT_FREEZER_BINDER_CALLBACK_THROTTLE);
+        mFreezerBinderAsyncThreshold = DeviceConfig.getInt(
+                DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT,
+                KEY_FREEZER_BINDER_ASYNC_THRESHOLD, DEFAULT_FREEZER_BINDER_ASYNC_THRESHOLD);
         Slog.d(TAG_AM, "Freezer binder state set to enabled=" + mFreezerBinderEnabled
                 + ", divisor=" + mFreezerBinderDivisor
                 + ", offset=" + mFreezerBinderOffset
-                + ", threshold=" + mFreezerBinderThreshold);
+                + ", threshold=" + mFreezerBinderThreshold
+                + ", callback enabled=" + mFreezerBinderCallbackEnabled
+                + ", callback throttle=" + mFreezerBinderCallbackThrottle
+                + ", async threshold=" + mFreezerBinderAsyncThreshold);
     }
 
     private boolean parseProcStateThrottle(String procStateThrottleString) {
@@ -2182,6 +2226,21 @@
                         Slog.w(TAG_AM, "Unable to check file locks");
                     }
                 } break;
+                case BINDER_ERROR_MSG: {
+                    IntArray pids = new IntArray();
+                    // Copy the frozen pids to a local array to release mProcLock ASAP
+                    synchronized (mProcLock) {
+                        int size = mFrozenProcesses.size();
+                        for (int i = 0; i < size; i++) {
+                            pids.add(mFrozenProcesses.keyAt(i));
+                        }
+                    }
+
+                    // Check binder errors to frozen processes with a local freezer lock
+                    synchronized (mFreezerLock) {
+                        binderErrorLocked(pids);
+                    }
+                } break;
                 default:
                     return;
             }
@@ -2487,4 +2546,115 @@
                 return UNFREEZE_REASON_NONE;
         }
     }
+
+    /**
+     * Kill a frozen process with a specified reason
+     */
+    public void killProcess(int pid, String reason, @Reason int reasonCode,
+            @SubReason int subReason) {
+        mAm.mHandler.post(() -> {
+            synchronized (mAm) {
+                synchronized (mProcLock) {
+                    ProcessRecord proc = mFrozenProcesses.get(pid);
+                    // The process might have been killed or unfrozen by others
+                    if (proc != null && proc.getThread() != null && !proc.isKilledByAm()) {
+                        proc.killLocked(reason, reasonCode, subReason, true);
+                    }
+                }
+            }
+        });
+    }
+
+    /**
+     * Sending binder transactions to frozen apps most likely indicates there's a bug. Log it and
+     * kill the frozen apps if they 1) receive sync binder transactions while frozen, or 2) miss
+     * async binder transactions due to kernel binder buffer running out.
+     *
+     * @param debugPid The binder transaction sender
+     * @param app The ProcessRecord of the sender
+     * @param code The binder transaction code
+     * @param flags The binder transaction flags
+     * @param err The binder transaction error
+     */
+    public void binderError(int debugPid, ProcessRecord app, int code, int flags, int err) {
+        Slog.w(TAG_AM, "pid " + debugPid + " " + (app == null ? "null" : app.processName)
+                + " sent binder code " + code + " with flags " + flags
+                + " to frozen apps and got error " + err);
+
+        // Do nothing if the binder error callback is not enabled.
+        // That means the frozen apps in a wrong state will be killed when they are unfrozen later.
+        if (!mFreezerBinderCallbackEnabled) {
+            return;
+        }
+
+        final long now = SystemClock.uptimeMillis();
+        if (now < mFreezerBinderCallbackLast + mFreezerBinderCallbackThrottle) {
+            Slog.d(TAG_AM, "Too many transaction errors, throttling freezer binder callback.");
+            return;
+        }
+        mFreezerBinderCallbackLast = now;
+
+        // Check all frozen processes in Freezer handler
+        mFreezeHandler.sendEmptyMessage(BINDER_ERROR_MSG);
+    }
+
+    private void binderErrorLocked(IntArray pids) {
+        // PIDs that run out of async binder buffer when being frozen
+        ArraySet<Integer> pidsAsync = (mFreezerBinderAsyncThreshold < 0) ? null : new ArraySet<>();
+
+        for (int i = 0; i < pids.size(); i++) {
+            int current = pids.get(i);
+            try {
+                int freezeInfo = getBinderFreezeInfo(current);
+
+                if ((freezeInfo & SYNC_RECEIVED_WHILE_FROZEN) != 0) {
+                    killProcess(current, "Sync transaction while frozen",
+                            ApplicationExitInfo.REASON_FREEZER,
+                            ApplicationExitInfo.SUBREASON_FREEZER_BINDER_TRANSACTION);
+
+                    // No need to check async transactions in this case
+                    continue;
+                }
+
+                if ((freezeInfo & ASYNC_RECEIVED_WHILE_FROZEN) != 0) {
+                    if (pidsAsync != null) {
+                        pidsAsync.add(current);
+                    }
+                    if (DEBUG_FREEZER) {
+                        Slog.w(TAG_AM, "pid " + current
+                                + " received async transactions while frozen");
+                    }
+                }
+            } catch (Exception e) {
+                // The process has died. No need to kill it again.
+                Slog.w(TAG_AM, "Unable to query binder frozen stats for pid " + current);
+            }
+        }
+
+        // TODO: when kernel binder driver supports, poll the binder status directly.
+        // Binderfs stats, like other debugfs files, is not a reliable interface. But it's the
+        // only true source for now. The following code checks all frozen PIDs. If any of them
+        // is running out of async binder buffer, kill it. Otherwise it will be killed at a
+        // later time when AMS unfreezes it, which causes race issues.
+        if (pidsAsync == null || pidsAsync.size() == 0) {
+            return;
+        }
+        new BinderfsStatsReader().handleFreeAsyncSpace(
+                // Check if the frozen process has pending async calls
+                pidsAsync::contains,
+
+                // Kill the current process if it's running out of async binder space
+                (current, free) -> {
+                    if (free < mFreezerBinderAsyncThreshold) {
+                        Slog.w(TAG_AM, "pid " + current
+                                + " has " + free + " free async space, killing");
+                        killProcess(current, "Async binder space running out while frozen",
+                                ApplicationExitInfo.REASON_FREEZER,
+                                ApplicationExitInfo.SUBREASON_FREEZER_BINDER_ASYNC_FULL);
+                    }
+                },
+
+                // Log the error if binderfs stats can't be accesses or correctly parsed
+                exception -> Slog.e(TAG_AM, "Unable to parse binderfs stats"));
+    }
 }
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 9bbfc8f..16f55e8 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -125,6 +125,7 @@
         "arc_next",
         "bluetooth",
         "build",
+        "biometrics",
         "biometrics_framework",
         "biometrics_integration",
         "camera_platform",
diff --git a/services/core/java/com/android/server/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java
index 8e767e7..8bf903a 100644
--- a/services/core/java/com/android/server/pm/DeletePackageHelper.java
+++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java
@@ -18,6 +18,7 @@
 
 import static android.Manifest.permission.CONTROL_KEYGUARD;
 import static android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS;
+import static android.content.pm.Flags.sdkLibIndependence;
 import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
 import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
 import static android.content.pm.PackageManager.DELETE_KEEP_DATA;
@@ -187,7 +188,9 @@
                         List<VersionedPackage> libClientPackages =
                                 computer.getPackagesUsingSharedLibrary(libraryInfo,
                                         MATCH_KNOWN_PACKAGES, Process.SYSTEM_UID, currUserId);
-                        if (!ArrayUtils.isEmpty(libClientPackages)) {
+                        boolean allowSdkLibIndependence =
+                                (pkg.getSdkLibraryName() != null) && sdkLibIndependence();
+                        if (!ArrayUtils.isEmpty(libClientPackages) && !allowSdkLibIndependence) {
                             Slog.w(TAG, "Not removing package " + pkg.getManifestPackageName()
                                     + " hosting lib " + libraryInfo.getName() + " version "
                                     + libraryInfo.getLongVersion() + " used by " + libClientPackages
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 0d024d6..56e385d 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -65,6 +65,7 @@
 import android.os.FactoryTest;
 import android.os.FileUtils;
 import android.os.IBinder;
+import android.os.IBinderCallback;
 import android.os.IIncidentManager;
 import android.os.Looper;
 import android.os.Message;
@@ -985,6 +986,14 @@
             }
         }
 
+        // Set binder transaction callback after starting system services
+        Binder.setTransactionCallback(new IBinderCallback() {
+            @Override
+            public void onTransactionError(int pid, int code, int flags, int err) {
+                mActivityManagerService.frozenBinderTransactionDetected(pid, code, flags, err);
+            }
+        });
+
         // Loop forever.
         Looper.loop();
         throw new RuntimeException("Main thread loop unexpectedly exited");