Merge "Disable ModesTileDataInteractorTest on ravenwood" into main
diff --git a/Android.bp b/Android.bp
index 9933940..f0aa62c 100644
--- a/Android.bp
+++ b/Android.bp
@@ -427,7 +427,6 @@
         "modules-utils-expresslog",
         "perfetto_trace_javastream_protos_jarjar",
         "libaconfig_java_proto_nano",
-        "aconfig_device_paths_java",
     ],
 }
 
diff --git a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java
index 086fcc8..f306b0b 100644
--- a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java
+++ b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java
@@ -18,7 +18,6 @@
 
 import static com.android.internal.pm.pkg.parsing.ParsingUtils.ANDROID_RES_NAMESPACE;
 
-import android.aconfig.DevicePaths;
 import android.aconfig.nano.Aconfig;
 import android.aconfig.nano.Aconfig.parsed_flag;
 import android.aconfig.nano.Aconfig.parsed_flags;
@@ -41,6 +40,7 @@
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -54,20 +54,20 @@
 public class AconfigFlags {
     private static final String LOG_TAG = "AconfigFlags";
 
-    public enum Permission {
-        READ_WRITE,
-        READ_ONLY
-    }
+    private static final List<String> sTextProtoFilesOnDevice = List.of(
+            "/system/etc/aconfig_flags.pb",
+            "/system_ext/etc/aconfig_flags.pb",
+            "/product/etc/aconfig_flags.pb",
+            "/vendor/etc/aconfig_flags.pb");
 
     private final ArrayMap<String, Boolean> mFlagValues = new ArrayMap<>();
-    private final ArrayMap<String, Permission> mFlagPermissions = new ArrayMap<>();
 
     public AconfigFlags() {
         if (!Flags.manifestFlagging()) {
             Slog.v(LOG_TAG, "Feature disabled, skipped all loading");
             return;
         }
-        for (String fileName : DevicePaths.parsedFlagsProtoPaths()) {
+        for (String fileName : sTextProtoFilesOnDevice) {
             try (var inputStream = new FileInputStream(fileName)) {
                 loadAconfigDefaultValues(inputStream.readAllBytes());
             } catch (IOException e) {
@@ -184,12 +184,6 @@
             Slog.v(LOG_TAG, "Read Aconfig default flag value "
                     + flagPackageAndName + " = " + flagValue);
             mFlagValues.put(flagPackageAndName, flagValue);
-
-            Permission permission = flag.permission == Aconfig.READ_ONLY
-                    ? Permission.READ_ONLY
-                    : Permission.READ_WRITE;
-
-            mFlagPermissions.put(flagPackageAndName, permission);
         }
     }
 
@@ -206,17 +200,6 @@
     }
 
     /**
-     * Get the flag permission, or null if the flag doesn't exist.
-     * @param flagPackageAndName Full flag name formatted as 'package.flag'
-     * @return the current permission of the given Aconfig flag, or null if there is no such flag
-     */
-    @Nullable
-    public Permission getFlagPermission(@NonNull String flagPackageAndName) {
-        Permission permission = mFlagPermissions.get(flagPackageAndName);
-        return permission;
-    }
-
-    /**
      * Check if the element in {@code parser} should be skipped because of the feature flag.
      * @param parser XML parser object currently parsing an element
      * @return true if the element is disabled because of its feature flag
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 6315e69..5807246 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -1107,6 +1107,10 @@
         if (useDesktopOverrideDensity()) {
             wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi())
         }
+        if (desktopModeTaskRepository.isOnlyVisibleNonClosingTask(taskInfo.taskId)) {
+            // Remove wallpaper activity when leaving desktop mode
+            removeWallpaperActivity(wct)
+        }
     }
 
     /**
@@ -1122,6 +1126,10 @@
         // The task's density may have been overridden in freeform; revert it here as we don't
         // want it overridden in multi-window.
         wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi())
+        if (desktopModeTaskRepository.isOnlyVisibleNonClosingTask(taskInfo.taskId)) {
+            // Remove wallpaper activity when leaving desktop mode
+            removeWallpaperActivity(wct)
+        }
     }
 
     /** Returns the ID of the Task that will be minimized, or null if no task will be minimized. */
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index f670434..8558a77 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -935,6 +935,24 @@
   }
 
   @Test
+  fun moveToFullscreen_tdaFullscreen_windowingModeUndefined_removesWallpaperActivity() {
+    val task = setUpFreeformTask()
+    val wallpaperToken = MockToken().token()
+
+    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
+      .configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+
+    controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN)
+
+    val wct = getLatestExitDesktopWct()
+    val taskChange = assertNotNull(wct.changes[task.token.asBinder()])
+    assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED)
+    // Removes wallpaper activity when leaving desktop
+    wct.assertRemoveAt(index = 0, wallpaperToken)
+  }
+
+  @Test
   fun moveToFullscreen_tdaFreeform_windowingModeSetToFullscreen() {
     val task = setUpFreeformTask()
     val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
@@ -946,6 +964,44 @@
   }
 
   @Test
+  fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_removesWallpaperActivity() {
+    val task = setUpFreeformTask()
+    val wallpaperToken = MockToken().token()
+
+    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
+      .configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+
+    controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN)
+
+    val wct = getLatestExitDesktopWct()
+    val taskChange = assertNotNull(wct.changes[task.token.asBinder()])
+    assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_FULLSCREEN)
+    // Removes wallpaper activity when leaving desktop
+    wct.assertRemoveAt(index = 0, wallpaperToken)
+  }
+
+  @Test
+  fun moveToFullscreen_multipleVisibleNonMinimizedTasks_doesNotRemoveWallpaperActivity() {
+    val task1 = setUpFreeformTask()
+    // Setup task2
+    setUpFreeformTask()
+    val wallpaperToken = MockToken().token()
+
+    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
+      .configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+
+    controller.moveToFullscreen(task1.taskId, transitionSource = UNKNOWN)
+
+    val wct = getLatestExitDesktopWct()
+    val task1Change = assertNotNull(wct.changes[task1.token.asBinder()])
+    assertThat(task1Change.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED)
+    // Does not remove wallpaper activity, as desktop still has a visible desktop task
+    assertThat(wct.hierarchyOps).isEmpty()
+  }
+
+  @Test
   fun moveToFullscreen_nonExistentTask_doesNothing() {
     controller.moveToFullscreen(999, transitionSource = UNKNOWN)
     verifyExitDesktopWCTNotExecuted()
@@ -1769,6 +1825,49 @@
   }
 
   @Test
+  fun moveFocusedTaskToFullscreen_onlyVisibleNonMinimizedTask_removesWallpaperActivity() {
+    val task1 = setUpFreeformTask()
+    val task2 = setUpFreeformTask()
+    val task3 = setUpFreeformTask()
+    val wallpaperToken = MockToken().token()
+
+    task1.isFocused = false
+    task2.isFocused = true
+    task3.isFocused = false
+    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, task1.taskId)
+    desktopModeTaskRepository.updateVisibleFreeformTasks(DEFAULT_DISPLAY, task3.taskId,
+      visible = false)
+
+    controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
+
+    val wct = getLatestExitDesktopWct()
+    val taskChange = assertNotNull(wct.changes[task2.token.asBinder()])
+    assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
+    wct.assertRemoveAt(index = 0, wallpaperToken)
+  }
+
+  @Test
+  fun moveFocusedTaskToFullscreen_multipleVisibleTasks_doesNotRemoveWallpaperActivity() {
+    val task1 = setUpFreeformTask()
+    val task2 = setUpFreeformTask()
+    val task3 = setUpFreeformTask()
+    val wallpaperToken = MockToken().token()
+
+    task1.isFocused = false
+    task2.isFocused = true
+    task3.isFocused = false
+    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
+
+    val wct = getLatestExitDesktopWct()
+    val taskChange = assertNotNull(wct.changes[task2.token.asBinder()])
+    assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
+    // Does not remove wallpaper activity, as desktop still has visible desktop tasks
+    assertThat(wct.hierarchyOps).isEmpty()
+  }
+
+  @Test
   @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
   fun dragToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() {
     val spyController = spy(controller)
@@ -1977,6 +2076,7 @@
             eq(null))
   }
 
+  @Test
   fun enterSplit_freeformTaskIsMovedToSplit() {
     val task1 = setUpFreeformTask()
     val task2 = setUpFreeformTask()
@@ -1986,14 +2086,67 @@
     task2.isFocused = true
     task3.isFocused = false
 
-    controller.enterSplit(DEFAULT_DISPLAY, false)
+    controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false)
 
     verify(splitScreenController)
         .requestEnterSplitSelect(
-            task2,
+            eq(task2),
             any(),
-            SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT,
-            task2.configuration.windowConfiguration.bounds)
+            eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT),
+            eq(task2.configuration.windowConfiguration.bounds))
+  }
+
+  @Test
+  fun enterSplit_onlyVisibleNonMinimizedTask_removesWallpaperActivity() {
+    val task1 = setUpFreeformTask()
+    val task2 = setUpFreeformTask()
+    val task3 = setUpFreeformTask()
+    val wallpaperToken = MockToken().token()
+
+    task1.isFocused = false
+    task2.isFocused = true
+    task3.isFocused = false
+    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, task1.taskId)
+    desktopModeTaskRepository.updateVisibleFreeformTasks(DEFAULT_DISPLAY, task3.taskId,
+      visible = false)
+
+    controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false)
+
+    val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+    verify(splitScreenController)
+      .requestEnterSplitSelect(
+        eq(task2),
+        wctArgument.capture(),
+        eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT),
+        eq(task2.configuration.windowConfiguration.bounds))
+    // Removes wallpaper activity when leaving desktop
+    wctArgument.value.assertRemoveAt(index = 0, wallpaperToken)
+  }
+
+  @Test
+  fun enterSplit_multipleVisibleNonMinimizedTasks_removesWallpaperActivity() {
+    val task1 = setUpFreeformTask()
+    val task2 = setUpFreeformTask()
+    val task3 = setUpFreeformTask()
+    val wallpaperToken = MockToken().token()
+
+    task1.isFocused = false
+    task2.isFocused = true
+    task3.isFocused = false
+    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+
+    controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false)
+
+    val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+    verify(splitScreenController)
+      .requestEnterSplitSelect(
+        eq(task2),
+        wctArgument.capture(),
+        eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT),
+        eq(task2.configuration.windowConfiguration.bounds))
+    // Does not remove wallpaper activity, as desktop still has visible desktop tasks
+    assertThat(wctArgument.value.hierarchyOps).isEmpty()
   }
 
   @Test
diff --git a/libs/hwui/apex/LayoutlibLoader.cpp b/libs/hwui/apex/LayoutlibLoader.cpp
index 073bc8d..b4e6b72 100644
--- a/libs/hwui/apex/LayoutlibLoader.cpp
+++ b/libs/hwui/apex/LayoutlibLoader.cpp
@@ -28,6 +28,7 @@
 
 extern int register_android_graphics_Bitmap(JNIEnv*);
 extern int register_android_graphics_BitmapFactory(JNIEnv*);
+extern int register_android_graphics_BitmapRegionDecoder(JNIEnv*);
 extern int register_android_graphics_ByteBufferStreamAdaptor(JNIEnv* env);
 extern int register_android_graphics_Camera(JNIEnv* env);
 extern int register_android_graphics_CreateJavaOutputStreamAdaptor(JNIEnv* env);
@@ -53,8 +54,11 @@
 extern int register_android_graphics_DrawFilter(JNIEnv* env);
 extern int register_android_graphics_FontFamily(JNIEnv* env);
 extern int register_android_graphics_Gainmap(JNIEnv* env);
+extern int register_android_graphics_HardwareBufferRenderer(JNIEnv* env);
 extern int register_android_graphics_HardwareRendererObserver(JNIEnv* env);
 extern int register_android_graphics_Matrix(JNIEnv* env);
+extern int register_android_graphics_Mesh(JNIEnv* env);
+extern int register_android_graphics_MeshSpecification(JNIEnv* env);
 extern int register_android_graphics_Paint(JNIEnv* env);
 extern int register_android_graphics_Path(JNIEnv* env);
 extern int register_android_graphics_PathIterator(JNIEnv* env);
@@ -87,6 +91,8 @@
 static const std::unordered_map<std::string, RegJNIRec> gRegJNIMap = {
         {"android.graphics.Bitmap", REG_JNI(register_android_graphics_Bitmap)},
         {"android.graphics.BitmapFactory", REG_JNI(register_android_graphics_BitmapFactory)},
+        {"android.graphics.BitmapRegionDecoder",
+         REG_JNI(register_android_graphics_BitmapRegionDecoder)},
         {"android.graphics.ByteBufferStreamAdaptor",
          REG_JNI(register_android_graphics_ByteBufferStreamAdaptor)},
         {"android.graphics.Camera", REG_JNI(register_android_graphics_Camera)},
@@ -101,6 +107,8 @@
         {"android.graphics.FontFamily", REG_JNI(register_android_graphics_FontFamily)},
         {"android.graphics.Gainmap", REG_JNI(register_android_graphics_Gainmap)},
         {"android.graphics.Graphics", REG_JNI(register_android_graphics_Graphics)},
+        {"android.graphics.HardwareBufferRenderer",
+         REG_JNI(register_android_graphics_HardwareBufferRenderer)},
         {"android.graphics.HardwareRenderer", REG_JNI(register_android_view_ThreadedRenderer)},
         {"android.graphics.HardwareRendererObserver",
          REG_JNI(register_android_graphics_HardwareRendererObserver)},
@@ -108,6 +116,9 @@
         {"android.graphics.Interpolator", REG_JNI(register_android_graphics_Interpolator)},
         {"android.graphics.MaskFilter", REG_JNI(register_android_graphics_MaskFilter)},
         {"android.graphics.Matrix", REG_JNI(register_android_graphics_Matrix)},
+        {"android.graphics.Mesh", REG_JNI(register_android_graphics_Mesh)},
+        {"android.graphics.MeshSpecification",
+         REG_JNI(register_android_graphics_MeshSpecification)},
         {"android.graphics.NinePatch", REG_JNI(register_android_graphics_NinePatch)},
         {"android.graphics.Paint", REG_JNI(register_android_graphics_Paint)},
         {"android.graphics.Path", REG_JNI(register_android_graphics_Path)},
diff --git a/libs/hwui/jni/MeshSpecification.cpp b/libs/hwui/jni/MeshSpecification.cpp
index ae9792d..b943496 100644
--- a/libs/hwui/jni/MeshSpecification.cpp
+++ b/libs/hwui/jni/MeshSpecification.cpp
@@ -126,7 +126,7 @@
     SkSafeUnref(meshSpec);
 }
 
-static jlong getMeshSpecificationFinalizer() {
+static jlong getMeshSpecificationFinalizer(CRITICAL_JNI_PARAMS) {
     return static_cast<jlong>(reinterpret_cast<uintptr_t>(&MeshSpecification_safeUnref));
 }
 
diff --git a/libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp
index e3cdee6..3b1b861 100644
--- a/libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp
+++ b/libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp
@@ -135,7 +135,7 @@
     proxy->setLightAlpha((uint8_t)(255 * ambientShadowAlpha), (uint8_t)(255 * spotShadowAlpha));
 }
 
-static jlong android_graphics_HardwareBufferRenderer_getFinalizer() {
+static jlong android_graphics_HardwareBufferRenderer_getFinalizer(CRITICAL_JNI_PARAMS) {
     return static_cast<jlong>(reinterpret_cast<uintptr_t>(&HardwareBufferRenderer_destroy));
 }
 
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java b/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java
index 8b0772b..2227943 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java
@@ -42,7 +42,6 @@
 import android.provider.UpdatableDeviceConfigServiceReadiness;
 import android.util.Slog;
 
-import com.android.internal.pm.pkg.component.AconfigFlags;
 import com.android.internal.util.FastPrintWriter;
 
 import java.io.File;
@@ -417,13 +416,7 @@
                     DeviceConfig.setProperty(namespace, key, value, makeDefault);
                     break;
                 case OVERRIDE:
-                    AconfigFlags.Permission permission =
-                            (new AconfigFlags()).getFlagPermission(key);
-                    if (permission == AconfigFlags.Permission.READ_ONLY) {
-                        pout.println("cannot override read-only flag " + key);
-                    } else {
-                        DeviceConfig.setLocalOverride(namespace, key, value);
-                    }
+                    DeviceConfig.setLocalOverride(namespace, key, value);
                     break;
                 case CLEAR_OVERRIDE:
                     DeviceConfig.clearLocalOverride(namespace, key);
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 6e6e4b2..b580eb1 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1171,3 +1171,12 @@
   bug: "345227709"
 }
 
+flag {
+  namespace: "systemui"
+  name: "register_content_observers_async"
+  description: "Use new Async API to register content observers"
+  bug: "316922634"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt
index 9e53792..49817b2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt
@@ -20,6 +20,7 @@
 import android.graphics.drawable.Icon
 import android.hardware.input.InputManager
 import android.util.Log
+import android.view.InputDevice
 import android.view.KeyCharacterMap
 import android.view.KeyEvent
 import android.view.KeyboardShortcutGroup
@@ -47,8 +48,11 @@
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.withContext
 
 @SysUISingleton
@@ -56,6 +60,7 @@
 @Inject
 constructor(
     private val context: Context,
+    @Background private val backgroundScope: CoroutineScope,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
     @SystemShortcuts private val systemShortcutsSource: KeyboardShortcutGroupsSource,
     @MultitaskingShortcuts private val multitaskingShortcutsSource: KeyboardShortcutGroupsSource,
@@ -75,81 +80,82 @@
             }
         }
 
-    val systemShortcutsCategory =
-        activeInputDevice.map {
-            if (it != null) {
-                toShortcutCategory(
-                    it.keyCharacterMap,
-                    System,
-                    systemShortcutsSource.shortcutGroups(it.id),
-                    keepIcons = true,
-                )
-            } else {
-                null
-            }
-        }
-
-    val multitaskingShortcutsCategory =
-        activeInputDevice.map {
-            if (it != null) {
-                toShortcutCategory(
-                    it.keyCharacterMap,
-                    MultiTasking,
-                    multitaskingShortcutsSource.shortcutGroups(it.id),
-                    keepIcons = true,
-                )
-            } else {
-                null
-            }
-        }
-
-    val appCategoriesShortcutsCategory =
-        activeInputDevice.map {
-            if (it != null) {
-                toShortcutCategory(
-                    it.keyCharacterMap,
-                    AppCategories,
-                    appCategoriesShortcutsSource.shortcutGroups(it.id),
-                    keepIcons = true,
-                )
-            } else {
-                null
-            }
-        }
-
-    val imeShortcutsCategory =
-        activeInputDevice.map {
-            if (it != null) {
-                toShortcutCategory(
-                    it.keyCharacterMap,
-                    InputMethodEditor,
-                    inputShortcutsSource.shortcutGroups(it.id),
-                    keepIcons = false,
-                )
-            } else {
-                null
-            }
-        }
-
-    val currentAppShortcutsCategory: Flow<ShortcutCategory?> =
-        activeInputDevice.map {
-            if (it != null) {
-                val shortcutGroups = currentAppShortcutsSource.shortcutGroups(it.id)
-                val categoryType = getCurrentAppShortcutCategoryType(shortcutGroups)
-                if (categoryType == null) {
-                    null
-                } else {
-                    toShortcutCategory(
-                        it.keyCharacterMap,
-                        categoryType,
-                        shortcutGroups,
-                        keepIcons = false
-                    )
+    val categories: Flow<List<ShortcutCategory>> =
+        activeInputDevice
+            .map {
+                if (it == null) {
+                    return@map emptyList()
                 }
-            } else {
-                null
+                return@map listOfNotNull(
+                    fetchSystemShortcuts(it),
+                    fetchMultiTaskingShortcuts(it),
+                    fetchAppCategoriesShortcuts(it),
+                    fetchImeShortcuts(it),
+                    fetchCurrentAppShortcuts(it),
+                )
             }
+            .stateIn(
+                scope = backgroundScope,
+                started = SharingStarted.Lazily,
+                initialValue = emptyList(),
+            )
+
+    private suspend fun fetchSystemShortcuts(inputDevice: InputDevice) =
+        toShortcutCategory(
+            inputDevice.keyCharacterMap,
+            System,
+            systemShortcutsSource.shortcutGroups(inputDevice.id),
+            keepIcons = true,
+        )
+
+    private suspend fun fetchMultiTaskingShortcuts(inputDevice: InputDevice) =
+        toShortcutCategory(
+            inputDevice.keyCharacterMap,
+            MultiTasking,
+            multitaskingShortcutsSource.shortcutGroups(inputDevice.id),
+            keepIcons = true,
+        )
+
+    private suspend fun fetchAppCategoriesShortcuts(inputDevice: InputDevice) =
+        toShortcutCategory(
+            inputDevice.keyCharacterMap,
+            AppCategories,
+            appCategoriesShortcutsSource.shortcutGroups(inputDevice.id),
+            keepIcons = true,
+        )
+
+    private suspend fun fetchImeShortcuts(inputDevice: InputDevice) =
+        toShortcutCategory(
+            inputDevice.keyCharacterMap,
+            InputMethodEditor,
+            inputShortcutsSource.shortcutGroups(inputDevice.id),
+            keepIcons = false,
+        )
+
+    private suspend fun fetchCurrentAppShortcuts(inputDevice: InputDevice): ShortcutCategory? {
+        val shortcutGroups = currentAppShortcutsSource.shortcutGroups(inputDevice.id)
+        val categoryType = getCurrentAppShortcutCategoryType(shortcutGroups)
+        return if (categoryType == null) {
+            null
+        } else {
+            toShortcutCategory(
+                inputDevice.keyCharacterMap,
+                categoryType,
+                shortcutGroups,
+                keepIcons = false
+            )
         }
+    }
+
+    private fun getCurrentAppShortcutCategoryType(
+        shortcutGroups: List<KeyboardShortcutGroup>
+    ): ShortcutCategoryType? {
+        return if (shortcutGroups.isEmpty()) {
+            null
+        } else {
+            CurrentApp(packageName = shortcutGroups[0].packageName.toString())
+        }
+    }
 
     private fun toShortcutCategory(
         keyCharacterMap: KeyCharacterMap,
@@ -174,16 +180,6 @@
         }
     }
 
-    private fun getCurrentAppShortcutCategoryType(
-        shortcutGroups: List<KeyboardShortcutGroup>
-    ): ShortcutCategoryType? {
-        return if (shortcutGroups.isEmpty()) {
-            null
-        } else {
-            CurrentApp(packageName = shortcutGroups[0].packageName.toString())
-        }
-    }
-
     private fun toShortcuts(
         keyCharacterMap: KeyCharacterMap,
         infoList: List<KeyboardShortcutInfo>,
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt
index f215c74..6f19561 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt
@@ -23,7 +23,7 @@
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
 
 @SysUISingleton
 class ShortcutHelperCategoriesInteractor
@@ -33,14 +33,8 @@
 ) {
 
     val shortcutCategories: Flow<List<ShortcutCategory>> =
-        combine(
-            categoriesRepository.systemShortcutsCategory,
-            categoriesRepository.multitaskingShortcutsCategory,
-            categoriesRepository.imeShortcutsCategory,
-            categoriesRepository.appCategoriesShortcutsCategory,
-            categoriesRepository.currentAppShortcutsCategory
-        ) { shortcutCategories ->
-            shortcutCategories.filterNotNull().map { groupSubCategoriesInCategory(it) }
+        categoriesRepository.categories.map { categories ->
+            categories.map { category -> groupSubCategoriesInCategory(category) }
         }
 
     private fun groupSubCategoriesInCategory(shortcutCategory: ShortcutCategory): ShortcutCategory {
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt
index e602cad..ad258f4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt
@@ -19,7 +19,6 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperCategoriesInteractor
 import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperStateInteractor
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState
 import com.android.systemui.keyboard.shortcut.ui.model.ShortcutsUiState
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
@@ -40,8 +39,8 @@
 ) {
 
     val shouldShow =
-        stateInteractor.state
-            .map { it is ShortcutHelperState.Active }
+        categoriesInteractor.shortcutCategories
+            .map { it.isNotEmpty() }
             .distinctUntilChanged()
             .flowOn(backgroundDispatcher)
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
index ab432d6..c0049d4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
@@ -31,7 +31,6 @@
 import com.android.systemui.plugins.clocks.ClockId
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
-import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
 import com.android.systemui.util.kotlin.combine
@@ -104,21 +103,21 @@
     val clockShouldBeCentered: Flow<Boolean> =
         if (SceneContainerFlag.isEnabled) {
             combine(
-                shadeInteractor.shadeMode,
+                shadeInteractor.isShadeLayoutWide,
                 activeNotificationsInteractor.areAnyNotificationsPresent,
                 keyguardInteractor.isActiveDreamLockscreenHosted,
                 isOnAod,
                 headsUpNotificationInteractor.isHeadsUpOrAnimatingAway,
                 keyguardInteractor.isDozing,
             ) {
-                shadeMode,
+                isShadeLayoutWide,
                 areAnyNotificationsPresent,
                 isActiveDreamLockscreenHosted,
                 isOnAod,
                 isHeadsUp,
                 isDozing ->
                 when {
-                    shadeMode != ShadeMode.Split -> true
+                    !isShadeLayoutWide -> true
                     !areAnyNotificationsPresent -> true
                     isActiveDreamLockscreenHosted -> true
                     // Pulsing notification appears on the right. Move clock left to avoid overlap.
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt
index 071d8f8c..760ff7d 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt
@@ -36,14 +36,16 @@
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.mapLatest
 import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
 @SysUISingleton
+@OptIn(ExperimentalCoroutinesApi::class)
 class MediaProjectionManagerRepository
 @Inject
 constructor(
@@ -76,12 +78,12 @@
                     object : MediaProjectionManager.Callback() {
                         override fun onStart(info: MediaProjectionInfo?) {
                             Log.d(TAG, "MediaProjectionManager.Callback#onStart")
-                            trySendWithFailureLogging(MediaProjectionState.NotProjecting, TAG)
+                            trySendWithFailureLogging(CallbackEvent.OnStart, TAG)
                         }
 
                         override fun onStop(info: MediaProjectionInfo?) {
                             Log.d(TAG, "MediaProjectionManager.Callback#onStop")
-                            trySendWithFailureLogging(MediaProjectionState.NotProjecting, TAG)
+                            trySendWithFailureLogging(CallbackEvent.OnStop, TAG)
                         }
 
                         override fun onRecordingSessionSet(
@@ -89,14 +91,36 @@
                             session: ContentRecordingSession?
                         ) {
                             Log.d(TAG, "MediaProjectionManager.Callback#onSessionStarted: $session")
-                            launch {
-                                trySendWithFailureLogging(stateForSession(info, session), TAG)
-                            }
+                            trySendWithFailureLogging(
+                                CallbackEvent.OnRecordingSessionSet(info, session),
+                                TAG,
+                            )
                         }
                     }
                 mediaProjectionManager.addCallback(callback, handler)
                 awaitClose { mediaProjectionManager.removeCallback(callback) }
             }
+            // When we get an #onRecordingSessionSet event, we need to do some work in the
+            // background before emitting the right state value. But when we get an #onStop
+            // event, we immediately know what state value to emit.
+            //
+            // Without `mapLatest`, this could be a problem if an #onRecordingSessionSet event
+            // comes in and then an #onStop event comes in shortly afterwards (b/352483752):
+            // 1. #onRecordingSessionSet -> start some work in the background
+            // 2. #onStop -> immediately emit "Not Projecting"
+            // 3. onRecordingSessionSet work finishes -> emit "Projecting"
+            //
+            // At step 3, we *shouldn't* emit "Projecting" because #onStop was the last callback
+            // event we received, so we should be "Not Projecting". This `mapLatest` ensures
+            // that if an #onStop event comes in, we cancel any ongoing work for
+            // #onRecordingSessionSet and we don't emit "Projecting".
+            .mapLatest {
+                when (it) {
+                    is CallbackEvent.OnStart,
+                    is CallbackEvent.OnStop -> MediaProjectionState.NotProjecting
+                    is CallbackEvent.OnRecordingSessionSet -> stateForSession(it.info, it.session)
+                }
+            }
             .stateIn(
                 scope = applicationScope,
                 started = SharingStarted.Lazily,
@@ -129,6 +153,21 @@
         return MediaProjectionState.Projecting.SingleTask(hostPackage, hostDeviceName, matchingTask)
     }
 
+    /**
+     * Translates [MediaProjectionManager.Callback] events into objects so that we always maintain
+     * the correct callback ordering.
+     */
+    sealed interface CallbackEvent {
+        data object OnStart : CallbackEvent
+
+        data object OnStop : CallbackEvent
+
+        data class OnRecordingSessionSet(
+            val info: MediaProjectionInfo,
+            val session: ContentRecordingSession?,
+        ) : CallbackEvent
+    }
+
     companion object {
         private const val TAG = "MediaProjectionMngrRepo"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
index eebbb13..f22f399 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository
 import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository
 import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
 import javax.inject.Inject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.FlowPreview
@@ -50,48 +51,65 @@
     val topHeadsUpRow: Flow<HeadsUpRowKey?> = headsUpRepository.topHeadsUpRow
 
     /** Set of currently pinned top-level heads up rows to be displayed. */
-    val pinnedHeadsUpRows: Flow<Set<HeadsUpRowKey>> =
-        headsUpRepository.activeHeadsUpRows.flatMapLatest { repositories ->
-            if (repositories.isNotEmpty()) {
-                val toCombine: List<Flow<Pair<HeadsUpRowRepository, Boolean>>> =
-                    repositories.map { repo -> repo.isPinned.map { isPinned -> repo to isPinned } }
-                combine(toCombine) { pairs ->
-                    pairs.filter { (_, isPinned) -> isPinned }.map { (repo, _) -> repo }.toSet()
+    val pinnedHeadsUpRows: Flow<Set<HeadsUpRowKey>> by lazy {
+        if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) {
+            flowOf(emptySet())
+        } else {
+            headsUpRepository.activeHeadsUpRows.flatMapLatest { repositories ->
+                if (repositories.isNotEmpty()) {
+                    val toCombine: List<Flow<Pair<HeadsUpRowRepository, Boolean>>> =
+                        repositories.map { repo ->
+                            repo.isPinned.map { isPinned -> repo to isPinned }
+                        }
+                    combine(toCombine) { pairs ->
+                        pairs.filter { (_, isPinned) -> isPinned }.map { (repo, _) -> repo }.toSet()
+                    }
+                } else {
+                    // if the set is empty, there are no flows to combine
+                    flowOf(emptySet())
                 }
-            } else {
-                // if the set is empty, there are no flows to combine
-                flowOf(emptySet())
             }
         }
+    }
 
     /** Are there any pinned heads up rows to display? */
-    val hasPinnedRows: Flow<Boolean> =
-        headsUpRepository.activeHeadsUpRows.flatMapLatest { rows ->
-            if (rows.isNotEmpty()) {
-                combine(rows.map { it.isPinned }) { pins -> pins.any { it } }
-            } else {
-                // if the set is empty, there are no flows to combine
-                flowOf(false)
-            }
-        }
-
-    val isHeadsUpOrAnimatingAway: Flow<Boolean> =
-        combine(hasPinnedRows, headsUpRepository.isHeadsUpAnimatingAway) {
-                hasPinnedRows,
-                animatingAway ->
-                hasPinnedRows || animatingAway
-            }
-            .debounce { isHeadsUpOrAnimatingAway ->
-                if (isHeadsUpOrAnimatingAway) {
-                    0
+    val hasPinnedRows: Flow<Boolean> by lazy {
+        if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) {
+            flowOf(false)
+        } else {
+            headsUpRepository.activeHeadsUpRows.flatMapLatest { rows ->
+                if (rows.isNotEmpty()) {
+                    combine(rows.map { it.isPinned }) { pins -> pins.any { it } }
                 } else {
-                    // When the last pinned entry is removed from the [HeadsUpRepository],
-                    // there might be a delay before the View starts animating.
-                    50L
+                    // if the set is empty, there are no flows to combine
+                    flowOf(false)
                 }
             }
-            .onStart { emit(false) } // emit false, so we don't wait for the initial update
-            .distinctUntilChanged()
+        }
+    }
+
+    val isHeadsUpOrAnimatingAway: Flow<Boolean> by lazy {
+        if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) {
+            flowOf(false)
+        } else {
+            combine(hasPinnedRows, headsUpRepository.isHeadsUpAnimatingAway) {
+                    hasPinnedRows,
+                    animatingAway ->
+                    hasPinnedRows || animatingAway
+                }
+                .debounce { isHeadsUpOrAnimatingAway ->
+                    if (isHeadsUpOrAnimatingAway) {
+                        0
+                    } else {
+                        // When the last pinned entry is removed from the [HeadsUpRepository],
+                        // there might be a delay before the View starts animating.
+                        50L
+                    }
+                }
+                .onStart { emit(false) } // emit false, so we don't wait for the initial update
+                .distinctUntilChanged()
+        }
+    }
 
     private val canShowHeadsUp: Flow<Boolean> =
         combine(
@@ -109,10 +127,15 @@
             }
         }
 
-    val showHeadsUpStatusBar: Flow<Boolean> =
-        combine(hasPinnedRows, canShowHeadsUp) { hasPinnedRows, canShowHeadsUp ->
-            hasPinnedRows && canShowHeadsUp
+    val showHeadsUpStatusBar: Flow<Boolean> by lazy {
+        if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) {
+            flowOf(false)
+        } else {
+            combine(hasPinnedRows, canShowHeadsUp) { hasPinnedRows, canShowHeadsUp ->
+                hasPinnedRows && canShowHeadsUp
+            }
         }
+    }
 
     fun headsUpRow(key: HeadsUpRowKey): HeadsUpRowInteractor =
         HeadsUpRowInteractor(key as HeadsUpRowRepository)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
index a11cbc3..98869be 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
@@ -37,6 +37,7 @@
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.KeyguardUpdateMonitorCallback;
 import com.android.systemui.Dumpable;
+import com.android.systemui.Flags;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -54,6 +55,7 @@
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.unfold.FoldAodAnimationController;
 import com.android.systemui.unfold.SysUIUnfoldComponent;
+import com.android.systemui.util.settings.SecureSettings;
 
 import java.io.PrintWriter;
 import java.util.Optional;
@@ -86,6 +88,7 @@
     private final FoldAodAnimationController mFoldAodAnimationController;
     private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
     private final UserTracker mUserTracker;
+    private final SecureSettings mSecureSettings;
 
     private boolean mDozeAlwaysOn;
     private boolean mControlScreenOffAnimation;
@@ -130,7 +133,8 @@
             ConfigurationController configurationController,
             StatusBarStateController statusBarStateController,
             UserTracker userTracker,
-            DozeInteractor dozeInteractor) {
+            DozeInteractor dozeInteractor,
+            SecureSettings secureSettings) {
         mResources = resources;
         mAmbientDisplayConfiguration = ambientDisplayConfiguration;
         mAlwaysOnPolicy = alwaysOnDisplayPolicy;
@@ -144,6 +148,7 @@
         mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
         mUserTracker = userTracker;
         mDozeInteractor = dozeInteractor;
+        mSecureSettings = secureSettings;
 
         keyguardUpdateMonitor.registerCallback(mKeyguardVisibilityCallback);
         tunerService.addTunable(
@@ -160,7 +165,8 @@
             mFoldAodAnimationController.addCallback(this);
         }
 
-        SettingsObserver quickPickupSettingsObserver = new SettingsObserver(context, handler);
+        SettingsObserver quickPickupSettingsObserver =
+                new SettingsObserver(context, handler, mSecureSettings);
         quickPickupSettingsObserver.observe();
 
         batteryController.addCallback(new BatteryStateChangeCallback() {
@@ -479,18 +485,36 @@
                 Settings.Secure.getUriFor(Settings.Secure.DOZE_ALWAYS_ON);
         private final Context mContext;
 
-        SettingsObserver(Context context, Handler handler) {
+        private final Handler mHandler;
+        private final SecureSettings mSecureSettings;
+
+        SettingsObserver(Context context, Handler handler, SecureSettings secureSettings) {
             super(handler);
             mContext = context;
+            mHandler = handler;
+            mSecureSettings = secureSettings;
         }
 
         void observe() {
-            ContentResolver resolver = mContext.getContentResolver();
-            resolver.registerContentObserver(mQuickPickupGesture, false, this,
-                    UserHandle.USER_ALL);
-            resolver.registerContentObserver(mPickupGesture, false, this, UserHandle.USER_ALL);
-            resolver.registerContentObserver(mAlwaysOnEnabled, false, this, UserHandle.USER_ALL);
-            update(null);
+            if (Flags.registerContentObserversAsync()) {
+                mSecureSettings.registerContentObserverForUserAsync(mQuickPickupGesture,
+                        this, UserHandle.USER_ALL);
+                mSecureSettings.registerContentObserverForUserAsync(mPickupGesture,
+                        this, UserHandle.USER_ALL);
+                mSecureSettings.registerContentObserverForUserAsync(mAlwaysOnEnabled,
+                        this, UserHandle.USER_ALL,
+                        // The register calls are called in order, so this ensures that update()
+                        // is called after them all and value retrieval isn't racy.
+                        () -> mHandler.post(() -> update(null)));
+            } else {
+                ContentResolver resolver = mContext.getContentResolver();
+                resolver.registerContentObserver(mQuickPickupGesture, false, this,
+                        UserHandle.USER_ALL);
+                resolver.registerContentObserver(mPickupGesture, false, this, UserHandle.USER_ALL);
+                resolver.registerContentObserver(mAlwaysOnEnabled, false, this,
+                        UserHandle.USER_ALL);
+                update(null);
+            }
         }
 
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt
index 3bf5b65..025354b 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt
@@ -21,6 +21,7 @@
 import android.net.Uri
 import android.os.UserHandle
 import android.provider.Settings.SettingNotFoundException
+import androidx.annotation.WorkerThread
 import com.android.app.tracing.TraceUtils.trace
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.util.settings.SettingsProxy.Companion.parseFloat
@@ -199,6 +200,24 @@
         }
 
     /**
+     * Convenience wrapper around [ContentResolver.registerContentObserver].'
+     *
+     * API corresponding to [registerContentObserverForUser] for Java usage. After registration is
+     * complete, the callback block is called on the <b>background thread</b> to allow for update of
+     * value.
+     */
+    fun registerContentObserverForUserAsync(
+        uri: Uri,
+        settingsObserver: ContentObserver,
+        userHandle: Int,
+        @WorkerThread registered: Runnable
+    ) =
+        CoroutineScope(backgroundDispatcher).launch {
+            registerContentObserverForUserSync(uri, settingsObserver, userHandle)
+            registered.run()
+        }
+
+    /**
      * Convenience wrapper around [ContentResolver.registerContentObserver]
      *
      * Implicitly calls [getUriFor] on the passed in name.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt
new file mode 100644
index 0000000..14837f2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2024 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.keyboard.shortcut.data.repository
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyboard.shortcut.data.source.FakeKeyboardShortcutGroupsSource
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts
+import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource
+import com.android.systemui.keyboard.shortcut.shortcutHelperCategoriesRepository
+import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource
+import com.android.systemui.keyboard.shortcut.shortcutHelperInputShortcutsSource
+import com.android.systemui.keyboard.shortcut.shortcutHelperMultiTaskingShortcutsSource
+import com.android.systemui.keyboard.shortcut.shortcutHelperSystemShortcutsSource
+import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ShortcutHelperCategoriesRepositoryTest : SysuiTestCase() {
+
+    private val fakeSystemSource = FakeKeyboardShortcutGroupsSource()
+    private val fakeMultiTaskingSource = FakeKeyboardShortcutGroupsSource()
+
+    private val kosmos =
+        testKosmos().also {
+            it.testDispatcher = UnconfinedTestDispatcher()
+            it.shortcutHelperSystemShortcutsSource = fakeSystemSource
+            it.shortcutHelperMultiTaskingShortcutsSource = fakeMultiTaskingSource
+            it.shortcutHelperAppCategoriesShortcutsSource = FakeKeyboardShortcutGroupsSource()
+            it.shortcutHelperInputShortcutsSource = FakeKeyboardShortcutGroupsSource()
+            it.shortcutHelperCurrentAppShortcutsSource = FakeKeyboardShortcutGroupsSource()
+        }
+
+    private val repo = kosmos.shortcutHelperCategoriesRepository
+    private val helper = kosmos.shortcutHelperTestHelper
+    private val testScope = kosmos.testScope
+
+    @Before
+    fun setUp() {
+        fakeSystemSource.setGroups(TestShortcuts.systemGroups)
+        fakeMultiTaskingSource.setGroups(TestShortcuts.multitaskingGroups)
+    }
+
+    @Test
+    fun categories_multipleSubscribers_replaysExistingValueToNewSubscribers() =
+        testScope.runTest {
+            fakeSystemSource.setGroups(TestShortcuts.systemGroups)
+            fakeMultiTaskingSource.setGroups(TestShortcuts.multitaskingGroups)
+            helper.showFromActivity()
+            val firstCategories by collectLastValue(repo.categories)
+
+            // Intentionally change shortcuts now. This simulates "current app" shortcuts changing
+            // when our helper is shown.
+            // We still want to return the shortcuts that were returned before our helper was
+            // showing.
+            fakeSystemSource.setGroups(emptyList())
+
+            val secondCategories by collectLastValue(repo.categories)
+            // Make sure the second subscriber receives the same value as the first subscriber, even
+            // though fetching shortcuts again would have returned a new result.
+            assertThat(secondCategories).isEqualTo(firstCategories)
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt
index d20ce3f..57c8b44 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt
@@ -28,6 +28,7 @@
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System
 import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource
 import com.android.systemui.keyboard.shortcut.shortcutHelperCategoriesInteractor
+import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource
 import com.android.systemui.keyboard.shortcut.shortcutHelperMultiTaskingShortcutsSource
 import com.android.systemui.keyboard.shortcut.shortcutHelperSystemShortcutsSource
 import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
@@ -48,14 +49,14 @@
 
     private val systemShortcutsSource = FakeKeyboardShortcutGroupsSource()
     private val multitaskingShortcutsSource = FakeKeyboardShortcutGroupsSource()
-    private val defaultAppsShortcutsSource = FakeKeyboardShortcutGroupsSource()
     @OptIn(ExperimentalCoroutinesApi::class)
     private val kosmos =
         testKosmos().also {
             it.testDispatcher = UnconfinedTestDispatcher()
             it.shortcutHelperSystemShortcutsSource = systemShortcutsSource
             it.shortcutHelperMultiTaskingShortcutsSource = multitaskingShortcutsSource
-            it.shortcutHelperAppCategoriesShortcutsSource = defaultAppsShortcutsSource
+            it.shortcutHelperAppCategoriesShortcutsSource = FakeKeyboardShortcutGroupsSource()
+            it.shortcutHelperCurrentAppShortcutsSource = FakeKeyboardShortcutGroupsSource()
         }
 
     private val testScope = kosmos.testScope
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt
index 0757ea1..f8e2f47 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt
@@ -20,8 +20,15 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyboard.shortcut.data.source.FakeKeyboardShortcutGroupsSource
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts
 import com.android.systemui.keyboard.shortcut.fakeShortcutHelperStartActivity
 import com.android.systemui.keyboard.shortcut.shortcutHelperActivityStarter
+import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource
+import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource
+import com.android.systemui.keyboard.shortcut.shortcutHelperInputShortcutsSource
+import com.android.systemui.keyboard.shortcut.shortcutHelperMultiTaskingShortcutsSource
+import com.android.systemui.keyboard.shortcut.shortcutHelperSystemShortcutsSource
 import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
 import com.android.systemui.keyboard.shortcut.ui.view.ShortcutHelperActivity
 import com.android.systemui.kosmos.Kosmos
@@ -32,6 +39,7 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -40,10 +48,18 @@
 @RunWith(AndroidJUnit4::class)
 class ShortcutHelperActivityStarterTest : SysuiTestCase() {
 
+    private val fakeSystemSource = FakeKeyboardShortcutGroupsSource()
+    private val fakeMultiTaskingSource = FakeKeyboardShortcutGroupsSource()
+
     private val kosmos =
         Kosmos().also {
             it.testCase = this
             it.testDispatcher = UnconfinedTestDispatcher()
+            it.shortcutHelperSystemShortcutsSource = fakeSystemSource
+            it.shortcutHelperMultiTaskingShortcutsSource = fakeMultiTaskingSource
+            it.shortcutHelperAppCategoriesShortcutsSource = FakeKeyboardShortcutGroupsSource()
+            it.shortcutHelperInputShortcutsSource = FakeKeyboardShortcutGroupsSource()
+            it.shortcutHelperCurrentAppShortcutsSource = FakeKeyboardShortcutGroupsSource()
         }
 
     private val testScope = kosmos.testScope
@@ -51,6 +67,12 @@
     private val fakeStartActivity = kosmos.fakeShortcutHelperStartActivity
     private val starter = kosmos.shortcutHelperActivityStarter
 
+    @Before
+    fun setUp() {
+        fakeSystemSource.setGroups(TestShortcuts.systemGroups)
+        fakeMultiTaskingSource.setGroups(TestShortcuts.multitaskingGroups)
+    }
+
     @Test
     fun start_doesNotStartByDefault() =
         testScope.runTest {
@@ -70,13 +92,30 @@
         }
 
     @Test
+    fun start_onToggle_noShortcuts_doesNotStartActivity() =
+        testScope.runTest {
+            fakeSystemSource.setGroups(emptyList())
+            fakeMultiTaskingSource.setGroups(emptyList())
+
+            starter.start()
+
+            testHelper.toggle(deviceId = 456)
+
+            assertThat(fakeStartActivity.startIntents).isEmpty()
+        }
+
+    @Test
     fun start_onToggle_multipleTimesStartsActivityOnlyWhenNotStarted() =
         testScope.runTest {
             starter.start()
 
+            // Starts
             testHelper.toggle(deviceId = 456)
+            // Stops
             testHelper.toggle(deviceId = 456)
+            // Starts again
             testHelper.toggle(deviceId = 456)
+            // Stops
             testHelper.toggle(deviceId = 456)
 
             verifyShortcutHelperActivityStarted(numTimes = 2)
@@ -93,6 +132,18 @@
         }
 
     @Test
+    fun start_onRequestShowShortcuts_noShortcuts_doesNotStartActivity() =
+        testScope.runTest {
+            fakeSystemSource.setGroups(emptyList())
+            fakeMultiTaskingSource.setGroups(emptyList())
+            starter.start()
+
+            testHelper.showFromActivity()
+
+            assertThat(fakeStartActivity.startIntents).isEmpty()
+        }
+
+    @Test
     fun start_onRequestShowShortcuts_multipleTimes_startsActivityOnlyOnce() =
         testScope.runTest {
             starter.start()
@@ -109,13 +160,21 @@
         testScope.runTest {
             starter.start()
 
+            // No-op. Already hidden.
             testHelper.hideFromActivity()
+            // No-op. Already hidden.
             testHelper.hideForSystem()
+            // Show 1st time.
             testHelper.toggle(deviceId = 987)
+            // No-op. Already shown.
             testHelper.showFromActivity()
+            // Hidden.
             testHelper.hideFromActivity()
+            // No-op. Already hidden.
             testHelper.hideForSystem()
+            // Show 2nd time.
             testHelper.toggle(deviceId = 456)
+            // No-op. Already shown.
             testHelper.showFromActivity()
 
             verifyShortcutHelperActivityStarted(numTimes = 2)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt
index 80d487c..07feaa1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt
@@ -21,6 +21,13 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
+import com.android.systemui.keyboard.shortcut.data.source.FakeKeyboardShortcutGroupsSource
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts
+import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource
+import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource
+import com.android.systemui.keyboard.shortcut.shortcutHelperInputShortcutsSource
+import com.android.systemui.keyboard.shortcut.shortcutHelperMultiTaskingShortcutsSource
+import com.android.systemui.keyboard.shortcut.shortcutHelperSystemShortcutsSource
 import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
 import com.android.systemui.keyboard.shortcut.shortcutHelperViewModel
 import com.android.systemui.keyboard.shortcut.ui.model.ShortcutsUiState
@@ -34,6 +41,7 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -42,10 +50,18 @@
 @RunWith(AndroidJUnit4::class)
 class ShortcutHelperViewModelTest : SysuiTestCase() {
 
+    private val fakeSystemSource = FakeKeyboardShortcutGroupsSource()
+    private val fakeMultiTaskingSource = FakeKeyboardShortcutGroupsSource()
+
     private val kosmos =
         Kosmos().also {
             it.testCase = this
             it.testDispatcher = UnconfinedTestDispatcher()
+            it.shortcutHelperSystemShortcutsSource = fakeSystemSource
+            it.shortcutHelperMultiTaskingShortcutsSource = fakeMultiTaskingSource
+            it.shortcutHelperAppCategoriesShortcutsSource = FakeKeyboardShortcutGroupsSource()
+            it.shortcutHelperInputShortcutsSource = FakeKeyboardShortcutGroupsSource()
+            it.shortcutHelperCurrentAppShortcutsSource = FakeKeyboardShortcutGroupsSource()
         }
 
     private val testScope = kosmos.testScope
@@ -53,6 +69,12 @@
     private val sysUiState = kosmos.sysUiState
     private val viewModel = kosmos.shortcutHelperViewModel
 
+    @Before
+    fun setUp() {
+        fakeSystemSource.setGroups(TestShortcuts.systemGroups)
+        fakeMultiTaskingSource.setGroups(TestShortcuts.multitaskingGroups)
+    }
+
     @Test
     fun shouldShow_falseByDefault() =
         testScope.runTest {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
index c604b6a..5db8981 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
@@ -19,6 +19,7 @@
 import android.hardware.display.displayManager
 import android.media.projection.MediaProjectionInfo
 import android.os.Binder
+import android.os.Handler
 import android.os.UserHandle
 import android.view.ContentRecordingSession
 import android.view.Display
@@ -26,11 +27,14 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.mediaprojection.data.model.MediaProjectionState
 import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
 import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createToken
 import com.android.systemui.mediaprojection.taskswitcher.FakeMediaProjectionManager.Companion.createDisplaySession
+import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeTasksRepository
 import com.android.systemui.mediaprojection.taskswitcher.fakeActivityTaskManager
 import com.android.systemui.mediaprojection.taskswitcher.fakeMediaProjectionManager
 import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherKosmos
@@ -253,6 +257,50 @@
                 .isNull()
         }
 
+    /** Regression test for b/352483752. */
+    @Test
+    fun mediaProjectionState_sessionStartedThenImmediatelyStopped_emitsOnlyNotProjecting() =
+        testScope.runTest {
+            val fakeTasksRepo = FakeTasksRepository()
+            val repoWithTimingControl =
+                MediaProjectionManagerRepository(
+                    // fakeTasksRepo lets us have control over when the background dispatcher
+                    // finishes fetching the tasks info.
+                    tasksRepository = fakeTasksRepo,
+                    mediaProjectionManager = fakeMediaProjectionManager.mediaProjectionManager,
+                    displayManager = displayManager,
+                    handler = Handler.getMain(),
+                    applicationScope = kosmos.applicationCoroutineScope,
+                    backgroundDispatcher = kosmos.testDispatcher,
+                    mediaProjectionServiceHelper = fakeMediaProjectionManager.helper,
+                )
+
+            val state by collectLastValue(repoWithTimingControl.mediaProjectionState)
+
+            val token = createToken()
+            val task = createTask(taskId = 1, token = token)
+
+            // Dispatch a session using a task session so that MediaProjectionManagerRepository
+            // has to ask TasksRepository for the tasks info.
+            fakeMediaProjectionManager.dispatchOnSessionSet(
+                session = ContentRecordingSession.createTaskSession(token.asBinder())
+            )
+            // FakeTasksRepository is set up to not return the tasks info until the test manually
+            // calls [FakeTasksRepository#setRunningTaskResult]. At this point,
+            // MediaProjectionManagerRepository is waiting for the tasks info and hasn't emitted
+            // anything yet.
+
+            // Before the tasks info comes back, dispatch a stop event.
+            fakeMediaProjectionManager.dispatchOnStop()
+
+            // Then let the tasks info come back.
+            fakeTasksRepo.setRunningTaskResult(task)
+
+            // Verify that MediaProjectionManagerRepository threw away the tasks info because
+            // a newer callback event (#onStop) occurred.
+            assertThat(state).isEqualTo(MediaProjectionState.NotProjecting)
+        }
+
     @Test
     fun stopProjecting_invokesManager() =
         testScope.runTest {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeTasksRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeTasksRepository.kt
new file mode 100644
index 0000000..ce2b983
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeTasksRepository.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 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.mediaprojection.taskswitcher.data.repository
+
+import android.app.ActivityManager
+import android.os.IBinder
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
+
+/**
+ * Fake tasks repository that gives us fine-grained control over when the result of
+ * [findRunningTaskFromWindowContainerToken] gets emitted.
+ */
+class FakeTasksRepository : TasksRepository {
+    override suspend fun launchRecentTask(taskInfo: ActivityManager.RunningTaskInfo) {}
+
+    private val findRunningTaskResult: CompletableDeferred<ActivityManager.RunningTaskInfo?> =
+        CompletableDeferred()
+
+    override suspend fun findRunningTaskFromWindowContainerToken(
+        windowContainerToken: IBinder
+    ): ActivityManager.RunningTaskInfo? {
+        return findRunningTaskResult.await()
+    }
+
+    fun setRunningTaskResult(task: ActivityManager.RunningTaskInfo?) {
+        findRunningTaskResult.complete(task)
+    }
+
+    override val foregroundTask: Flow<ActivityManager.RunningTaskInfo> = emptyFlow()
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java
index 7cb41f1..10d07a0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java
@@ -35,8 +35,8 @@
 import android.os.PowerManager;
 import android.provider.Settings;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.SysuiTestCase;
@@ -52,6 +52,7 @@
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.unfold.FoldAodAnimationController;
 import com.android.systemui.unfold.SysUIUnfoldComponent;
+import com.android.systemui.util.settings.FakeSettings;
 
 import org.junit.Assert;
 import org.junit.Before;
@@ -130,7 +131,8 @@
             mConfigurationController,
             mStatusBarStateController,
             mUserTracker,
-            mDozeInteractor
+            mDozeInteractor,
+            new FakeSettings()
         );
 
         verify(mBatteryController).addCallback(mBatteryStateChangeCallback.capture());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
index ef4e734..cc2ef53 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.ui.viewmodel
 
+import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.FlagsParameterization
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -32,6 +33,7 @@
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.statusbar.domain.interactor.keyguardStatusBarInteractor
 import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
 import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
 import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications
 import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
@@ -126,6 +128,7 @@
         }
 
     @Test
+    @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
     fun isVisible_headsUpStatusBarShown_false() =
         testScope.runTest {
             val latest by collectLastValue(underTest.isVisible)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt
index e3e20c8..5f7420d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt
@@ -31,9 +31,11 @@
 import com.android.systemui.settings.UserTracker
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceUntilIdle
 import kotlinx.coroutines.test.runTest
 import org.junit.Assert.assertThrows
 import org.junit.Before
@@ -65,20 +67,21 @@
     }
 
     @Test
-    fun registerContentObserverForUser_inputString_success() {
-        mSettings.registerContentObserverForUserSync(
-            TEST_SETTING,
-            mContentObserver,
-            mUserTracker.userId
-        )
-        verify(mSettings.getContentResolver())
-            .registerContentObserver(
-                eq(TEST_SETTING_URI),
-                eq(false),
-                eq(mContentObserver),
-                eq(MAIN_USER_ID)
+    fun registerContentObserverForUser_inputString_success() =
+        testScope.runTest {
+            mSettings.registerContentObserverForUserSync(
+                TEST_SETTING,
+                mContentObserver,
+                mUserTracker.userId
             )
-    }
+            verify(mSettings.getContentResolver())
+                .registerContentObserver(
+                    eq(TEST_SETTING_URI),
+                    eq(false),
+                    eq(mContentObserver),
+                    eq(MAIN_USER_ID)
+                )
+        }
 
     @Test
     fun registerContentObserverForUserSuspend_inputString_success() =
@@ -98,13 +101,14 @@
         }
 
     @Test
-    fun registerContentObserverForUserAsync_inputString_success() {
-        mSettings.registerContentObserverForUserAsync(
-            TEST_SETTING,
-            mContentObserver,
-            mUserTracker.userId
-        )
-        testScope.launch {
+    fun registerContentObserverForUserAsync_inputString_success() =
+        testScope.runTest {
+            mSettings.registerContentObserverForUserAsync(
+                TEST_SETTING,
+                mContentObserver,
+                mUserTracker.userId
+            )
+            testScope.advanceUntilIdle()
             verify(mSettings.getContentResolver())
                 .registerContentObserver(
                     eq(TEST_SETTING_URI),
@@ -113,24 +117,24 @@
                     eq(MAIN_USER_ID)
                 )
         }
-    }
 
     @Test
-    fun registerContentObserverForUser_inputString_notifyForDescendants_true() {
-        mSettings.registerContentObserverForUserSync(
-            TEST_SETTING,
-            notifyForDescendants = true,
-            mContentObserver,
-            mUserTracker.userId
-        )
-        verify(mSettings.getContentResolver())
-            .registerContentObserver(
-                eq(TEST_SETTING_URI),
-                eq(true),
-                eq(mContentObserver),
-                eq(MAIN_USER_ID)
+    fun registerContentObserverForUser_inputString_notifyForDescendants_true() =
+        testScope.runTest {
+            mSettings.registerContentObserverForUserSync(
+                TEST_SETTING,
+                notifyForDescendants = true,
+                mContentObserver,
+                mUserTracker.userId
             )
-    }
+            verify(mSettings.getContentResolver())
+                .registerContentObserver(
+                    eq(TEST_SETTING_URI),
+                    eq(true),
+                    eq(mContentObserver),
+                    eq(MAIN_USER_ID)
+                )
+        }
 
     @Test
     fun registerContentObserverForUserSuspend_inputString_notifyForDescendants_true() =
@@ -153,14 +157,15 @@
         }
 
     @Test
-    fun registerContentObserverForUserAsync_inputString_notifyForDescendants_true() {
-        mSettings.registerContentObserverForUserAsync(
-            TEST_SETTING,
-            notifyForDescendants = true,
-            mContentObserver,
-            mUserTracker.userId
-        )
-        testScope.launch {
+    fun registerContentObserverForUserAsync_inputString_notifyForDescendants_true() =
+        testScope.runTest {
+            mSettings.registerContentObserverForUserAsync(
+                TEST_SETTING,
+                notifyForDescendants = true,
+                mContentObserver,
+                mUserTracker.userId
+            )
+            testScope.advanceUntilIdle()
             verify(mSettings.getContentResolver())
                 .registerContentObserver(
                     eq(TEST_SETTING_URI),
@@ -169,23 +174,23 @@
                     eq(MAIN_USER_ID)
                 )
         }
-    }
 
     @Test
-    fun registerContentObserverForUser_inputUri_success() {
-        mSettings.registerContentObserverForUserSync(
-            TEST_SETTING_URI,
-            mContentObserver,
-            mUserTracker.userId
-        )
-        verify(mSettings.getContentResolver())
-            .registerContentObserver(
-                eq(TEST_SETTING_URI),
-                eq(false),
-                eq(mContentObserver),
-                eq(MAIN_USER_ID)
+    fun registerContentObserverForUser_inputUri_success() =
+        testScope.runTest {
+            mSettings.registerContentObserverForUserSync(
+                TEST_SETTING_URI,
+                mContentObserver,
+                mUserTracker.userId
             )
-    }
+            verify(mSettings.getContentResolver())
+                .registerContentObserver(
+                    eq(TEST_SETTING_URI),
+                    eq(false),
+                    eq(mContentObserver),
+                    eq(MAIN_USER_ID)
+                )
+        }
 
     @Test
     fun registerContentObserverForUserSuspend_inputUri_success() =
@@ -205,13 +210,15 @@
         }
 
     @Test
-    fun registerContentObserverForUserAsync_inputUri_success() {
-        mSettings.registerContentObserverForUserAsync(
-            TEST_SETTING_URI,
-            mContentObserver,
-            mUserTracker.userId
-        )
-        testScope.launch {
+    fun registerContentObserverForUserAsync_inputUri_success() =
+        testScope.runTest {
+            mSettings.registerContentObserverForUserAsync(
+                TEST_SETTING_URI,
+                mContentObserver,
+                mUserTracker.userId
+            )
+            testScope.advanceUntilIdle()
+
             verify(mSettings.getContentResolver())
                 .registerContentObserver(
                     eq(TEST_SETTING_URI),
@@ -220,24 +227,41 @@
                     eq(MAIN_USER_ID)
                 )
         }
-    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun registerContentObserverForUserAsync_callbackAfterRegister() =
+        testScope.runTest {
+            var callbackCalled = false
+            val runnable = { callbackCalled = true }
+
+            mSettings.registerContentObserverForUserAsync(
+                TEST_SETTING_URI,
+                mContentObserver,
+                mUserTracker.userId,
+                runnable
+            )
+            testScope.advanceUntilIdle()
+            assertThat(callbackCalled).isTrue()
+        }
 
     @Test
-    fun registerContentObserverForUser_inputUri_notifyForDescendants_true() {
-        mSettings.registerContentObserverForUserSync(
-            TEST_SETTING_URI,
-            notifyForDescendants = true,
-            mContentObserver,
-            mUserTracker.userId
-        )
-        verify(mSettings.getContentResolver())
-            .registerContentObserver(
-                eq(TEST_SETTING_URI),
-                eq(true),
-                eq(mContentObserver),
-                eq(MAIN_USER_ID)
+    fun registerContentObserverForUser_inputUri_notifyForDescendants_true() =
+        testScope.runTest {
+            mSettings.registerContentObserverForUserSync(
+                TEST_SETTING_URI,
+                notifyForDescendants = true,
+                mContentObserver,
+                mUserTracker.userId
             )
-    }
+            verify(mSettings.getContentResolver())
+                .registerContentObserver(
+                    eq(TEST_SETTING_URI),
+                    eq(true),
+                    eq(mContentObserver),
+                    eq(MAIN_USER_ID)
+                )
+        }
 
     @Test
     fun registerContentObserverForUserSuspend_inputUri_notifyForDescendants_true() =
@@ -260,14 +284,15 @@
         }
 
     @Test
-    fun registerContentObserverForUserAsync_inputUri_notifyForDescendants_true() {
-        mSettings.registerContentObserverForUserAsync(
-            TEST_SETTING_URI,
-            notifyForDescendants = true,
-            mContentObserver,
-            mUserTracker.userId
-        )
-        testScope.launch {
+    fun registerContentObserverForUserAsync_inputUri_notifyForDescendants_true() =
+        testScope.runTest {
+            mSettings.registerContentObserverForUserAsync(
+                TEST_SETTING_URI,
+                notifyForDescendants = true,
+                mContentObserver,
+                mUserTracker.userId
+            )
+            testScope.advanceUntilIdle()
             verify(mSettings.getContentResolver())
                 .registerContentObserver(
                     eq(TEST_SETTING_URI),
@@ -276,14 +301,19 @@
                     eq(MAIN_USER_ID)
                 )
         }
-    }
 
     @Test
-    fun registerContentObserver_inputUri_success() {
-        mSettings.registerContentObserverSync(TEST_SETTING_URI, mContentObserver)
-        verify(mSettings.getContentResolver())
-            .registerContentObserver(eq(TEST_SETTING_URI), eq(false), eq(mContentObserver), eq(0))
-    }
+    fun registerContentObserver_inputUri_success() =
+        testScope.runTest {
+            mSettings.registerContentObserverSync(TEST_SETTING_URI, mContentObserver)
+            verify(mSettings.getContentResolver())
+                .registerContentObserver(
+                    eq(TEST_SETTING_URI),
+                    eq(false),
+                    eq(mContentObserver),
+                    eq(0)
+                )
+        }
 
     @Test
     fun registerContentObserverSuspend_inputUri_success() =
@@ -313,15 +343,21 @@
     }
 
     @Test
-    fun registerContentObserver_inputUri_notifyForDescendants_true() {
-        mSettings.registerContentObserverSync(
-            TEST_SETTING_URI,
-            notifyForDescendants = true,
-            mContentObserver
-        )
-        verify(mSettings.getContentResolver())
-            .registerContentObserver(eq(TEST_SETTING_URI), eq(true), eq(mContentObserver), eq(0))
-    }
+    fun registerContentObserver_inputUri_notifyForDescendants_true() =
+        testScope.runTest {
+            mSettings.registerContentObserverSync(
+                TEST_SETTING_URI,
+                notifyForDescendants = true,
+                mContentObserver
+            )
+            verify(mSettings.getContentResolver())
+                .registerContentObserver(
+                    eq(TEST_SETTING_URI),
+                    eq(true),
+                    eq(mContentObserver),
+                    eq(0)
+                )
+        }
 
     @Test
     fun registerContentObserverSuspend_inputUri_notifyForDescendants_true() =
@@ -337,18 +373,19 @@
         }
 
     @Test
-    fun registerContentObserverAsync_inputUri_notifyForDescendants_true() {
-        mSettings.registerContentObserverAsync(TEST_SETTING_URI, mContentObserver)
-        testScope.launch {
-            verify(mSettings.getContentResolver())
-                .registerContentObserver(
-                    eq(TEST_SETTING_URI),
-                    eq(false),
-                    eq(mContentObserver),
-                    eq(0)
-                )
+    fun registerContentObserverAsync_inputUri_notifyForDescendants_true() =
+        testScope.runTest {
+            mSettings.registerContentObserverAsync(TEST_SETTING_URI, mContentObserver)
+            testScope.launch {
+                verify(mSettings.getContentResolver())
+                    .registerContentObserver(
+                        eq(TEST_SETTING_URI),
+                        eq(false),
+                        eq(mContentObserver),
+                        eq(0)
+                    )
+            }
         }
-    }
 
     @Test
     fun getString_keyPresent_returnValidValue() {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
index 530df8a..001b55b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
@@ -68,16 +68,17 @@
         )
     }
 
-val Kosmos.shortcutHelperInputShortcutsSource by
+var Kosmos.shortcutHelperInputShortcutsSource: KeyboardShortcutGroupsSource by
     Kosmos.Fixture { InputShortcutsSource(mainResources, windowManager) }
 
-val Kosmos.shortcutHelperCurrentAppShortcutsSource by
+var Kosmos.shortcutHelperCurrentAppShortcutsSource: KeyboardShortcutGroupsSource by
     Kosmos.Fixture { CurrentAppShortcutsSource(windowManager) }
 
 val Kosmos.shortcutHelperCategoriesRepository by
     Kosmos.Fixture {
         ShortcutHelperCategoriesRepository(
             applicationContext,
+            applicationCoroutineScope,
             testDispatcher,
             shortcutHelperSystemShortcutsSource,
             shortcutHelperMultiTaskingShortcutsSource,
@@ -96,7 +97,8 @@
             applicationContext,
             broadcastDispatcher,
             fakeCommandQueue,
-            windowManager
+            fakeInputManager,
+            windowManager,
         )
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt
index 40510db..3e09b23 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt
@@ -18,6 +18,7 @@
 
 import android.content.Context
 import android.content.Intent
+import android.hardware.input.FakeInputManager
 import android.view.KeyboardShortcutGroup
 import android.view.WindowManager
 import android.view.WindowManager.KeyboardShortcutsReceiver
@@ -31,6 +32,7 @@
     private val context: Context,
     private val fakeBroadcastDispatcher: FakeBroadcastDispatcher,
     private val fakeCommandQueue: FakeCommandQueue,
+    private val fakeInputManager: FakeInputManager,
     windowManager: WindowManager
 ) {
 
@@ -79,6 +81,7 @@
     }
 
     fun toggle(deviceId: Int) {
+        fakeInputManager.addPhysicalKeyboard(deviceId)
         fakeCommandQueue.doForEachCallback { it.toggleKeyboardShortcutsMenu(deviceId) }
     }
 
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 1cd20ed..9d4310c 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -215,6 +215,7 @@
         "power_hint_flags_lib",
         "biometrics_flags_lib",
         "am_flags_lib",
+        "updates_flags_lib",
         "com_android_server_accessibility_flags_lib",
         "//frameworks/libs/systemui:com_android_systemui_shared_flags_lib",
         "com_android_wm_shell_flags_lib",
diff --git a/services/core/java/com/android/server/updates/Android.bp b/services/core/java/com/android/server/updates/Android.bp
new file mode 100644
index 0000000..10beebb
--- /dev/null
+++ b/services/core/java/com/android/server/updates/Android.bp
@@ -0,0 +1,11 @@
+aconfig_declarations {
+    name: "updates_flags",
+    package: "com.android.server.updates",
+    container: "system",
+    srcs: ["*.aconfig"],
+}
+
+java_aconfig_library {
+    name: "updates_flags_lib",
+    aconfig_declarations: "updates_flags",
+}
diff --git a/services/core/java/com/android/server/updates/CertificateTransparencyLogInstallReceiver.java b/services/core/java/com/android/server/updates/CertificateTransparencyLogInstallReceiver.java
index 5565b6f..af4025e 100644
--- a/services/core/java/com/android/server/updates/CertificateTransparencyLogInstallReceiver.java
+++ b/services/core/java/com/android/server/updates/CertificateTransparencyLogInstallReceiver.java
@@ -16,17 +16,15 @@
 
 package com.android.server.updates;
 
+import android.content.Context;
+import android.content.Intent;
 import android.os.FileUtils;
 import android.system.ErrnoException;
 import android.system.Os;
-import android.util.Base64;
 import android.util.Slog;
 
-import com.android.internal.util.HexDump;
-
 import libcore.io.Streams;
 
-import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
@@ -36,10 +34,7 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.OutputStreamWriter;
 import java.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
 
 public class CertificateTransparencyLogInstallReceiver extends ConfigUpdateInstallReceiver {
 
@@ -52,31 +47,31 @@
 
     @Override
     protected void install(InputStream inputStream, int version) throws IOException {
-        /* Install is complicated here because we translate the input, which is a JSON file
-         * containing log information to a directory with a file per log. To support atomically
-         * replacing the old configuration directory with the new there's a bunch of steps. We
-         * create a new directory with the logs and then do an atomic update of the current symlink
-         * to point to the new directory.
-         */
+        if (!Flags.certificateTransparencyInstaller()) {
+            return;
+        }
+        // To support atomically replacing the old configuration directory with the new there's a
+        // bunch of steps. We create a new directory with the logs and then do an atomic update of
+        // the current symlink to point to the new directory.
         // 1. Ensure that the update dir exists and is readable
         updateDir.mkdir();
         if (!updateDir.isDirectory()) {
             throw new IOException("Unable to make directory " + updateDir.getCanonicalPath());
         }
         if (!updateDir.setReadable(true, false)) {
-            throw new IOException("Unable to set permissions on " +
-                    updateDir.getCanonicalPath());
+            throw new IOException("Unable to set permissions on " + updateDir.getCanonicalPath());
         }
         File currentSymlink = new File(updateDir, "current");
         File newVersion = new File(updateDir, LOGDIR_PREFIX + String.valueOf(version));
-        File oldDirectory;
         // 2. Handle the corner case where the new directory already exists.
         if (newVersion.exists()) {
             // If the symlink has already been updated then the update died between steps 7 and 8
             // and so we cannot delete the directory since its in use. Instead just bump the version
             // and return.
             if (newVersion.getCanonicalPath().equals(currentSymlink.getCanonicalPath())) {
-                writeUpdate(updateDir, updateVersion,
+                writeUpdate(
+                        updateDir,
+                        updateVersion,
                         new ByteArrayInputStream(Long.toString(version).getBytes()));
                 deleteOldLogDirectories();
                 return;
@@ -91,22 +86,12 @@
                 throw new IOException("Unable to make directory " + newVersion.getCanonicalPath());
             }
             if (!newVersion.setReadable(true, false)) {
-                throw new IOException("Failed to set " +newVersion.getCanonicalPath() +
-                        " readable");
+                throw new IOException(
+                        "Failed to set " + newVersion.getCanonicalPath() + " readable");
             }
 
-            // 4. For each log in the log file create the corresponding file in <new_version>/ .
-            try {
-                byte[] content = Streams.readFullyNoClose(inputStream);
-                JSONObject json = new JSONObject(new String(content, StandardCharsets.UTF_8));
-                JSONArray logs = json.getJSONArray("logs");
-                for (int i = 0; i < logs.length(); i++) {
-                    JSONObject log = logs.getJSONObject(i);
-                    installLog(newVersion, log);
-                }
-            } catch (JSONException e) {
-                throw new IOException("Failed to parse logs", e);
-            }
+            // 4. Validate the log list json and move the file in <new_version>/ .
+            installLogList(newVersion, inputStream);
 
             // 5. Create the temp symlink. We'll rename this to the target symlink to get an atomic
             // update.
@@ -125,49 +110,38 @@
         }
         Slog.i(TAG, "CT log directory updated to " + newVersion.getAbsolutePath());
         // 7. Update the current version information
-        writeUpdate(updateDir, updateVersion,
+        writeUpdate(
+                updateDir,
+                updateVersion,
                 new ByteArrayInputStream(Long.toString(version).getBytes()));
         // 8. Cleanup
         deleteOldLogDirectories();
     }
 
-    private void installLog(File directory, JSONObject logObject) throws IOException {
+    @Override
+    protected void postInstall(Context context, Intent intent) {
+        if (!Flags.certificateTransparencyInstaller()) {
+            return;
+        }
+    }
+
+    private void installLogList(File directory, InputStream inputStream) throws IOException {
         try {
-            String logFilename = getLogFileName(logObject.getString("key"));
-            File file = new File(directory, logFilename);
-            try (OutputStreamWriter out =
-                    new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)) {
-                writeLogEntry(out, "key", logObject.getString("key"));
-                writeLogEntry(out, "url", logObject.getString("url"));
-                writeLogEntry(out, "description", logObject.getString("description"));
+            byte[] content = Streams.readFullyNoClose(inputStream);
+            if (new JSONObject(new String(content, StandardCharsets.UTF_8)).length() == 0) {
+                throw new IOException("Log list data not valid");
+            }
+
+            File file = new File(directory, "log_list.json");
+            try (FileOutputStream outputStream = new FileOutputStream(file)) {
+                outputStream.write(content);
             }
             if (!file.setReadable(true, false)) {
                 throw new IOException("Failed to set permissions on " + file.getCanonicalPath());
             }
         } catch (JSONException e) {
-            throw new IOException("Failed to parse log", e);
+            throw new IOException("Malformed json in log list", e);
         }
-
-    }
-
-    /**
-     * Get the filename for a log based on its public key. This must be kept in sync with
-     * org.conscrypt.ct.CTLogStoreImpl.
-     */
-    private String getLogFileName(String base64PublicKey) {
-        byte[] keyBytes = Base64.decode(base64PublicKey, Base64.DEFAULT);
-        try {
-            byte[] id = MessageDigest.getInstance("SHA-256").digest(keyBytes);
-            return HexDump.toHexString(id, false);
-        } catch (NoSuchAlgorithmException e) {
-            // SHA-256 is guaranteed to be available.
-            throw new RuntimeException(e);
-        }
-    }
-
-    private void writeLogEntry(OutputStreamWriter out, String key, String value)
-            throws IOException {
-        out.write(key + ":" + value + "\n");
     }
 
     private void deleteOldLogDirectories() throws IOException {
@@ -175,12 +149,14 @@
             return;
         }
         File currentTarget = new File(updateDir, "current").getCanonicalFile();
-        FileFilter filter = new FileFilter() {
-            @Override
-            public boolean accept(File file) {
-                return !currentTarget.equals(file) && file.getName().startsWith(LOGDIR_PREFIX);
-            }
-        };
+        FileFilter filter =
+                new FileFilter() {
+                    @Override
+                    public boolean accept(File file) {
+                        return !currentTarget.equals(file)
+                                && file.getName().startsWith(LOGDIR_PREFIX);
+                    }
+                };
         for (File f : updateDir.listFiles(filter)) {
             FileUtils.deleteContentsAndDir(f);
         }
diff --git a/services/core/java/com/android/server/updates/flags.aconfig b/services/core/java/com/android/server/updates/flags.aconfig
new file mode 100644
index 0000000..476cb37
--- /dev/null
+++ b/services/core/java/com/android/server/updates/flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.updates"
+container: "system"
+
+flag {
+    name: "certificate_transparency_installer"
+    is_exported: true
+    namespace: "network_security"
+    description: "Enable certificate transparency installer for log list data"
+    bug: "319829948"
+}
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index 5be5bc5..2c73412 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -47,7 +47,7 @@
 import static com.android.server.accessibility.AccessibilityTraceProto.WINDOW_MANAGER_SERVICE;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
-import static com.android.server.wm.WindowTracing.WINSCOPE_EXT;
+import static com.android.server.wm.WindowTracingLegacy.WINSCOPE_EXT;
 
 import android.accessibilityservice.AccessibilityTrace;
 import android.animation.ObjectAnimator;
diff --git a/services/core/java/com/android/server/wm/WindowTracing.java b/services/core/java/com/android/server/wm/WindowTracing.java
index ba5323e..21f7eca 100644
--- a/services/core/java/com/android/server/wm/WindowTracing.java
+++ b/services/core/java/com/android/server/wm/WindowTracing.java
@@ -18,89 +18,52 @@
 
 import static android.os.Build.IS_USER;
 
-import static com.android.server.wm.WindowManagerTraceFileProto.ENTRY;
-import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER;
-import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER_H;
-import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER_L;
-import static com.android.server.wm.WindowManagerTraceFileProto.REAL_TO_ELAPSED_TIME_OFFSET_NANOS;
 import static com.android.server.wm.WindowManagerTraceProto.ELAPSED_REALTIME_NANOS;
 import static com.android.server.wm.WindowManagerTraceProto.WHERE;
 import static com.android.server.wm.WindowManagerTraceProto.WINDOW_MANAGER_SERVICE;
 
 import android.annotation.Nullable;
 import android.os.ShellCommand;
-import android.os.SystemClock;
 import android.os.Trace;
 import android.util.Log;
 import android.util.proto.ProtoOutputStream;
 import android.view.Choreographer;
 
 import com.android.internal.protolog.LegacyProtoLogImpl;
-import com.android.internal.protolog.common.IProtoLog;
 import com.android.internal.protolog.ProtoLog;
-import com.android.internal.util.TraceBuffer;
 
-import java.io.File;
-import java.io.IOException;
 import java.io.PrintWriter;
-import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
- * A class that allows window manager to dump its state continuously to a trace file, such that a
+ * A class that allows window manager to dump its state continuously, such that a
  * time series of window manager state can be analyzed after the fact.
  */
-class WindowTracing {
-
-    /**
-     * Maximum buffer size, currently defined as 5 MB
-     * Size was experimentally defined to fit between 100 to 150 elements.
-     */
-    private static final int BUFFER_CAPACITY_CRITICAL = 5120 * 1024; // 5 MB
-    private static final int BUFFER_CAPACITY_TRIM = 10240 * 1024; // 10 MB
-    private static final int BUFFER_CAPACITY_ALL = 20480 * 1024; // 20 MB
-    static final String WINSCOPE_EXT = ".winscope";
-    private static final String TRACE_FILENAME = "/data/misc/wmtrace/wm_trace" + WINSCOPE_EXT;
-    private static final String TAG = "WindowTracing";
-    private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L;
+abstract class WindowTracing {
+    protected static final String TAG = "WindowTracing";
+    protected static final String WHERE_START_TRACING = "trace.enable";
+    protected static final String WHERE_ON_FRAME = "onFrame";
 
     private final WindowManagerService mService;
     private final Choreographer mChoreographer;
     private final WindowManagerGlobalLock mGlobalLock;
 
-    private final Object mEnabledLock = new Object();
-    private final File mTraceFile;
-    private final TraceBuffer mBuffer;
     private final Choreographer.FrameCallback mFrameCallback = (frameTimeNanos) ->
-            log("onFrame" /* where */);
+            log(WHERE_ON_FRAME);
 
-    private @WindowTraceLogLevel int mLogLevel = WindowTraceLogLevel.TRIM;
-    private boolean mLogOnFrame = false;
-    private boolean mEnabled;
-    private volatile boolean mEnabledLockFree;
-    private boolean mScheduled;
+    private AtomicBoolean mScheduled = new AtomicBoolean(false);
 
-    private final IProtoLog mProtoLog;
 
     static WindowTracing createDefaultAndStartLooper(WindowManagerService service,
             Choreographer choreographer) {
-        File file = new File(TRACE_FILENAME);
-        return new WindowTracing(file, service, choreographer, BUFFER_CAPACITY_TRIM);
+        return new WindowTracingLegacy(service, choreographer);
     }
 
-    private WindowTracing(File file, WindowManagerService service, Choreographer choreographer,
-            int bufferCapacity) {
-        this(file, service, choreographer, service.mGlobalLock, bufferCapacity);
-    }
-
-    WindowTracing(File file, WindowManagerService service, Choreographer choreographer,
-            WindowManagerGlobalLock globalLock, int bufferCapacity) {
+    protected WindowTracing(WindowManagerService service, Choreographer choreographer,
+            WindowManagerGlobalLock globalLock) {
         mChoreographer = choreographer;
         mService = service;
         mGlobalLock = globalLock;
-        mTraceFile = file;
-        mBuffer = new TraceBuffer(bufferCapacity);
-        setLogLevel(WindowTraceLogLevel.TRIM, null /* pw */);
-        mProtoLog = ProtoLog.getSingleInstance();
     }
 
     void startTrace(@Nullable PrintWriter pw) {
@@ -108,44 +71,29 @@
             logAndPrintln(pw, "Error: Tracing is not supported on user builds.");
             return;
         }
-        synchronized (mEnabledLock) {
-            if (!android.tracing.Flags.perfettoProtologTracing()) {
-                ((LegacyProtoLogImpl) ProtoLog.getSingleInstance()).startProtoLog(pw);
-            }
-            logAndPrintln(pw, "Start tracing to " + mTraceFile + ".");
-            mBuffer.resetBuffer();
-            mEnabled = mEnabledLockFree = true;
+        if (!android.tracing.Flags.perfettoProtologTracing()) {
+            ((LegacyProtoLogImpl) ProtoLog.getSingleInstance()).startProtoLog(pw);
         }
-        log("trace.enable");
+        startTraceInternal(pw);
     }
 
-    /**
-     * Stops the trace and write the current buffer to disk
-     * @param pw Print writer
-     */
     void stopTrace(@Nullable PrintWriter pw) {
         if (IS_USER) {
             logAndPrintln(pw, "Error: Tracing is not supported on user builds.");
             return;
         }
-        synchronized (mEnabledLock) {
-            logAndPrintln(pw, "Stop tracing to " + mTraceFile + ". Waiting for traces to flush.");
-            mEnabled = mEnabledLockFree = false;
-
-            if (mEnabled) {
-                logAndPrintln(pw, "ERROR: tracing was re-enabled while waiting for flush.");
-                throw new IllegalStateException("tracing enabled while waiting for flush.");
-            }
-            writeTraceToFileLocked();
-            logAndPrintln(pw, "Trace written to " + mTraceFile + ".");
-        }
         if (!android.tracing.Flags.perfettoProtologTracing()) {
             ((LegacyProtoLogImpl) ProtoLog.getSingleInstance()).stopProtoLog(pw, true);
         }
+        stopTraceInternal(pw);
     }
 
     /**
-     * Stops the trace and write the current buffer to disk then restart, if it's already running.
+     * If legacy tracing is enabled (either WM or ProtoLog):
+     * 1. Stop tracing
+     * 2. Write trace to disk (to be picked by dumpstate)
+     * 3. Restart tracing
+     *
      * @param pw Print writer
      */
     void saveForBugreport(@Nullable PrintWriter pw) {
@@ -153,143 +101,24 @@
             logAndPrintln(pw, "Error: Tracing is not supported on user builds.");
             return;
         }
-        synchronized (mEnabledLock) {
-            if (!mEnabled) {
-                return;
-            }
-            mEnabled = mEnabledLockFree = false;
-            logAndPrintln(pw, "Stop tracing to " + mTraceFile + ". Waiting for traces to flush.");
-            writeTraceToFileLocked();
-            logAndPrintln(pw, "Trace written to " + mTraceFile + ".");
-            if (!android.tracing.Flags.perfettoProtologTracing()) {
-                ((LegacyProtoLogImpl) mProtoLog).stopProtoLog(pw, true);
-            }
-            logAndPrintln(pw, "Start tracing to " + mTraceFile + ".");
-            mBuffer.resetBuffer();
-            mEnabled = mEnabledLockFree = true;
-            if (!android.tracing.Flags.perfettoProtologTracing()) {
-                ((LegacyProtoLogImpl) mProtoLog).startProtoLog(pw);
-            }
+        if (!android.tracing.Flags.perfettoProtologTracing()
+                && ProtoLog.getSingleInstance().isProtoEnabled()) {
+            ((LegacyProtoLogImpl) ProtoLog.getSingleInstance()).stopProtoLog(pw, true);
+            ((LegacyProtoLogImpl) ProtoLog.getSingleInstance()).startProtoLog(pw);
         }
+        saveForBugreportInternal(pw);
     }
 
-    private void setLogLevel(@WindowTraceLogLevel int logLevel, PrintWriter pw) {
-        logAndPrintln(pw, "Setting window tracing log level to " + logLevel);
-        mLogLevel = logLevel;
-
-        switch (logLevel) {
-            case WindowTraceLogLevel.ALL: {
-                setBufferCapacity(BUFFER_CAPACITY_ALL, pw);
-                break;
-            }
-            case WindowTraceLogLevel.TRIM: {
-                setBufferCapacity(BUFFER_CAPACITY_TRIM, pw);
-                break;
-            }
-            case WindowTraceLogLevel.CRITICAL: {
-                setBufferCapacity(BUFFER_CAPACITY_CRITICAL, pw);
-                break;
-            }
-        }
-    }
-
-    private void setLogFrequency(boolean onFrame, PrintWriter pw) {
-        logAndPrintln(pw, "Setting window tracing log frequency to "
-                + ((onFrame) ? "frame" : "transaction"));
-        mLogOnFrame = onFrame;
-    }
-
-    private void setBufferCapacity(int capacity, PrintWriter pw) {
-        logAndPrintln(pw, "Setting window tracing buffer capacity to " + capacity + "bytes");
-        mBuffer.setCapacity(capacity);
-    }
-
-    boolean isEnabled() {
-        return mEnabledLockFree;
-    }
-
-    int onShellCommand(ShellCommand shell) {
-        PrintWriter pw = shell.getOutPrintWriter();
-        String cmd = shell.getNextArgRequired();
-        switch (cmd) {
-            case "start":
-                startTrace(pw);
-                return 0;
-            case "stop":
-                stopTrace(pw);
-                return 0;
-            case "save-for-bugreport":
-                saveForBugreport(pw);
-                return 0;
-            case "status":
-                logAndPrintln(pw, getStatus());
-                return 0;
-            case "frame":
-                setLogFrequency(true /* onFrame */, pw);
-                mBuffer.resetBuffer();
-                return 0;
-            case "transaction":
-                setLogFrequency(false /* onFrame */, pw);
-                mBuffer.resetBuffer();
-                return 0;
-            case "level":
-                String logLevelStr = shell.getNextArgRequired().toLowerCase();
-                switch (logLevelStr) {
-                    case "all": {
-                        setLogLevel(WindowTraceLogLevel.ALL, pw);
-                        break;
-                    }
-                    case "trim": {
-                        setLogLevel(WindowTraceLogLevel.TRIM, pw);
-                        break;
-                    }
-                    case "critical": {
-                        setLogLevel(WindowTraceLogLevel.CRITICAL, pw);
-                        break;
-                    }
-                    default: {
-                        setLogLevel(WindowTraceLogLevel.TRIM, pw);
-                        break;
-                    }
-                }
-                mBuffer.resetBuffer();
-                return 0;
-            case "size":
-                setBufferCapacity(Integer.parseInt(shell.getNextArgRequired()) * 1024, pw);
-                mBuffer.resetBuffer();
-                return 0;
-            default:
-                pw.println("Unknown command: " + cmd);
-                pw.println("Window manager trace options:");
-                pw.println("  start: Start logging");
-                pw.println("  stop: Stop logging");
-                pw.println("  save-for-bugreport: Save logging data to file if it's running.");
-                pw.println("  frame: Log trace once per frame");
-                pw.println("  transaction: Log each transaction");
-                pw.println("  size: Set the maximum log size (in KB)");
-                pw.println("  status: Print trace status");
-                pw.println("  level [lvl]: Set the log level between");
-                pw.println("    lvl may be one of:");
-                pw.println("      critical: Only visible windows with reduced information");
-                pw.println("      trim: All windows with reduced");
-                pw.println("      all: All window and information");
-                return -1;
-        }
-    }
-
-    String getStatus() {
-        return "Status: "
-                + ((isEnabled()) ? "Enabled" : "Disabled")
-                + "\n"
-                + "Log level: "
-                + mLogLevel
-                + "\n"
-                + mBuffer.getStatus();
-    }
+    abstract void setLogLevel(@WindowTraceLogLevel int logLevel, PrintWriter pw);
+    abstract void setLogFrequency(boolean onFrame, PrintWriter pw);
+    abstract void setBufferCapacity(int capacity, PrintWriter pw);
+    abstract boolean isEnabled();
+    abstract int onShellCommand(ShellCommand shell);
+    abstract String getStatus();
 
     /**
      * If tracing is enabled, log the current state or schedule the next frame to be logged,
-     * according to {@link #mLogOnFrame}.
+     * according to the configuration in the derived tracing class.
      *
      * @param where Logging point descriptor
      */
@@ -298,59 +127,63 @@
             return;
         }
 
-        if (mLogOnFrame) {
-            schedule();
-        } else {
+        if (shouldLogOnTransaction()) {
             log(where);
         }
+
+        if (shouldLogOnFrame()) {
+            schedule();
+        }
     }
 
     /**
      * Schedule the log to trace the next frame
      */
     private void schedule() {
-        if (mScheduled) {
+        if (!mScheduled.compareAndSet(false, true)) {
             return;
         }
 
-        mScheduled = true;
         mChoreographer.postFrameCallback(mFrameCallback);
     }
 
     /**
-     * Write the current frame to the buffer
+     * Write the current frame to proto
      *
+     * @param os Proto stream buffer
+     * @param logLevel Log level
      * @param where Logging point descriptor
+     * @param elapsedRealtimeNanos Timestamp
      */
-    private void log(String where) {
+    protected void dumpToProto(ProtoOutputStream os, @WindowTraceLogLevel int logLevel,
+            String where, long elapsedRealtimeNanos) {
         Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "traceStateLocked");
         try {
-            ProtoOutputStream os = new ProtoOutputStream();
-            long tokenOuter = os.start(ENTRY);
-            os.write(ELAPSED_REALTIME_NANOS, SystemClock.elapsedRealtimeNanos());
+            os.write(ELAPSED_REALTIME_NANOS, elapsedRealtimeNanos);
             os.write(WHERE, where);
 
-            long tokenInner = os.start(WINDOW_MANAGER_SERVICE);
+            long token = os.start(WINDOW_MANAGER_SERVICE);
             synchronized (mGlobalLock) {
                 Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "dumpDebugLocked");
                 try {
-                    mService.dumpDebugLocked(os, mLogLevel);
+                    mService.dumpDebugLocked(os, logLevel);
                 } finally {
                     Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
                 }
             }
-            os.end(tokenInner);
-            os.end(tokenOuter);
-            mBuffer.add(os);
-            mScheduled = false;
+            os.end(token);
         } catch (Exception e) {
             Log.wtf(TAG, "Exception while tracing state", e);
         } finally {
+            boolean isOnFrameLogEvent = where == WHERE_ON_FRAME;
+            if (isOnFrameLogEvent) {
+                mScheduled.set(false);
+            }
             Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
         }
     }
 
-    private void logAndPrintln(@Nullable PrintWriter pw, String msg) {
+    protected void logAndPrintln(@Nullable PrintWriter pw, String msg) {
         Log.i(TAG, msg);
         if (pw != null) {
             pw.println(msg);
@@ -358,24 +191,10 @@
         }
     }
 
-    /**
-     * Writes the trace buffer to disk. This method has no internal synchronization and should be
-     * externally synchronized
-     */
-    private void writeTraceToFileLocked() {
-        try {
-            Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "writeTraceToFileLocked");
-            ProtoOutputStream proto = new ProtoOutputStream();
-            proto.write(MAGIC_NUMBER, MAGIC_NUMBER_VALUE);
-            long timeOffsetNs =
-                    TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis())
-                    - SystemClock.elapsedRealtimeNanos();
-            proto.write(REAL_TO_ELAPSED_TIME_OFFSET_NANOS, timeOffsetNs);
-            mBuffer.writeTraceToFile(mTraceFile, proto);
-        } catch (IOException e) {
-            Log.e(TAG, "Unable to write buffer to file", e);
-        } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
-        }
-    }
+    protected abstract void startTraceInternal(@Nullable PrintWriter pw);
+    protected abstract void stopTraceInternal(@Nullable PrintWriter pw);
+    protected abstract void saveForBugreportInternal(@Nullable PrintWriter pw);
+    protected abstract void log(String where);
+    protected abstract boolean shouldLogOnFrame();
+    protected abstract boolean shouldLogOnTransaction();
 }
diff --git a/services/core/java/com/android/server/wm/WindowTracingLegacy.java b/services/core/java/com/android/server/wm/WindowTracingLegacy.java
new file mode 100644
index 0000000..7a36707
--- /dev/null
+++ b/services/core/java/com/android/server/wm/WindowTracingLegacy.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static com.android.server.wm.WindowManagerTraceFileProto.ENTRY;
+import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER;
+import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER_H;
+import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER_L;
+import static com.android.server.wm.WindowManagerTraceFileProto.REAL_TO_ELAPSED_TIME_OFFSET_NANOS;
+
+import android.annotation.Nullable;
+import android.os.ShellCommand;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.util.Log;
+import android.util.proto.ProtoOutputStream;
+import android.view.Choreographer;
+
+import com.android.internal.util.TraceBuffer;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.concurrent.TimeUnit;
+
+class WindowTracingLegacy extends WindowTracing {
+
+    /**
+     * Maximum buffer size, currently defined as 5 MB
+     * Size was experimentally defined to fit between 100 to 150 elements.
+     */
+    private static final int BUFFER_CAPACITY_CRITICAL = 5120 * 1024; // 5 MB
+    private static final int BUFFER_CAPACITY_TRIM = 10240 * 1024; // 10 MB
+    private static final int BUFFER_CAPACITY_ALL = 20480 * 1024; // 20 MB
+    static final String WINSCOPE_EXT = ".winscope";
+    private static final String TRACE_FILENAME = "/data/misc/wmtrace/wm_trace" + WINSCOPE_EXT;
+    private static final String TAG = "WindowTracing";
+    private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L;
+
+    private final Object mEnabledLock = new Object();
+    private final File mTraceFile;
+    private final TraceBuffer mBuffer;
+
+    private boolean mEnabled;
+    private volatile boolean mEnabledLockFree;
+
+    protected @WindowTraceLogLevel int mLogLevel = WindowTraceLogLevel.TRIM;
+    protected boolean mLogOnFrame = false;
+
+    WindowTracingLegacy(WindowManagerService service, Choreographer choreographer) {
+        this(new File(TRACE_FILENAME), service, choreographer,
+                service.mGlobalLock, BUFFER_CAPACITY_TRIM);
+    }
+
+    WindowTracingLegacy(File traceFile, WindowManagerService service, Choreographer choreographer,
+            WindowManagerGlobalLock globalLock, int bufferSize) {
+        super(service, choreographer, globalLock);
+        mTraceFile = traceFile;
+        mBuffer = new TraceBuffer(bufferSize);
+    }
+
+    @Override
+    void setLogLevel(@WindowTraceLogLevel int logLevel, PrintWriter pw) {
+        logAndPrintln(pw, "Setting window tracing log level to " + logLevel);
+        mLogLevel = logLevel;
+
+        switch (logLevel) {
+            case WindowTraceLogLevel.ALL: {
+                setBufferCapacity(BUFFER_CAPACITY_ALL, pw);
+                break;
+            }
+            case WindowTraceLogLevel.TRIM: {
+                setBufferCapacity(BUFFER_CAPACITY_TRIM, pw);
+                break;
+            }
+            case WindowTraceLogLevel.CRITICAL: {
+                setBufferCapacity(BUFFER_CAPACITY_CRITICAL, pw);
+                break;
+            }
+        }
+    }
+
+    @Override
+    void setLogFrequency(boolean onFrame, PrintWriter pw) {
+        logAndPrintln(pw, "Setting window tracing log frequency to "
+                + ((onFrame) ? "frame" : "transaction"));
+        mLogOnFrame = onFrame;
+    }
+
+    @Override
+    void setBufferCapacity(int capacity, PrintWriter pw) {
+        logAndPrintln(pw, "Setting window tracing buffer capacity to " + capacity + "bytes");
+        mBuffer.setCapacity(capacity);
+    }
+
+    @Override
+    boolean isEnabled() {
+        return mEnabledLockFree;
+    }
+
+    @Override
+    int onShellCommand(ShellCommand shell) {
+        PrintWriter pw = shell.getOutPrintWriter();
+        String cmd = shell.getNextArgRequired();
+        switch (cmd) {
+            case "start":
+                startTrace(pw);
+                return 0;
+            case "stop":
+                stopTrace(pw);
+                return 0;
+            case "save-for-bugreport":
+                saveForBugreport(pw);
+                return 0;
+            case "status":
+                logAndPrintln(pw, getStatus());
+                return 0;
+            case "frame":
+                setLogFrequency(true /* onFrame */, pw);
+                mBuffer.resetBuffer();
+                return 0;
+            case "transaction":
+                setLogFrequency(false /* onFrame */, pw);
+                mBuffer.resetBuffer();
+                return 0;
+            case "level":
+                String logLevelStr = shell.getNextArgRequired().toLowerCase();
+                switch (logLevelStr) {
+                    case "all": {
+                        setLogLevel(WindowTraceLogLevel.ALL, pw);
+                        break;
+                    }
+                    case "trim": {
+                        setLogLevel(WindowTraceLogLevel.TRIM, pw);
+                        break;
+                    }
+                    case "critical": {
+                        setLogLevel(WindowTraceLogLevel.CRITICAL, pw);
+                        break;
+                    }
+                    default: {
+                        setLogLevel(WindowTraceLogLevel.TRIM, pw);
+                        break;
+                    }
+                }
+                mBuffer.resetBuffer();
+                return 0;
+            case "size":
+                setBufferCapacity(Integer.parseInt(shell.getNextArgRequired()) * 1024, pw);
+                mBuffer.resetBuffer();
+                return 0;
+            default:
+                pw.println("Unknown command: " + cmd);
+                pw.println("Window manager trace options:");
+                pw.println("  start: Start logging");
+                pw.println("  stop: Stop logging");
+                pw.println("  save-for-bugreport: Save logging data to file if it's running.");
+                pw.println("  frame: Log trace once per frame");
+                pw.println("  transaction: Log each transaction");
+                pw.println("  size: Set the maximum log size (in KB)");
+                pw.println("  status: Print trace status");
+                pw.println("  level [lvl]: Set the log level between");
+                pw.println("    lvl may be one of:");
+                pw.println("      critical: Only visible windows with reduced information");
+                pw.println("      trim: All windows with reduced");
+                pw.println("      all: All window and information");
+                return -1;
+        }
+    }
+
+    @Override
+    String getStatus() {
+        return "Status: "
+                + ((isEnabled()) ? "Enabled" : "Disabled")
+                + "\n"
+                + "Log level: "
+                + mLogLevel
+                + "\n"
+                + mBuffer.getStatus();
+    }
+
+    @Override
+    protected void startTraceInternal(@Nullable PrintWriter pw) {
+        synchronized (mEnabledLock) {
+            logAndPrintln(pw, "Start tracing to " + mTraceFile + ".");
+            mBuffer.resetBuffer();
+            mEnabled = mEnabledLockFree = true;
+        }
+        log(WHERE_START_TRACING);
+    }
+
+    @Override
+    protected void stopTraceInternal(@Nullable PrintWriter pw) {
+        synchronized (mEnabledLock) {
+            logAndPrintln(pw, "Stop tracing to " + mTraceFile + ". Waiting for traces to flush.");
+            mEnabled = mEnabledLockFree = false;
+
+            if (mEnabled) {
+                logAndPrintln(pw, "ERROR: tracing was re-enabled while waiting for flush.");
+                throw new IllegalStateException("tracing enabled while waiting for flush.");
+            }
+            writeTraceToFileLocked();
+            logAndPrintln(pw, "Trace written to " + mTraceFile + ".");
+        }
+    }
+
+    @Override
+    protected void saveForBugreportInternal(@Nullable PrintWriter pw) {
+        synchronized (mEnabledLock) {
+            if (!mEnabled) {
+                return;
+            }
+            mEnabled = mEnabledLockFree = false;
+            logAndPrintln(pw, "Stop tracing to " + mTraceFile + ". Waiting for traces to flush.");
+            writeTraceToFileLocked();
+            logAndPrintln(pw, "Trace written to " + mTraceFile + ".");
+            logAndPrintln(pw, "Start tracing to " + mTraceFile + ".");
+            mBuffer.resetBuffer();
+            mEnabled = mEnabledLockFree = true;
+        }
+    }
+
+    @Override
+    protected void log(String where) {
+        try {
+            ProtoOutputStream os = new ProtoOutputStream();
+            long token = os.start(ENTRY);
+            dumpToProto(os, mLogLevel, where, SystemClock.elapsedRealtimeNanos());
+            os.end(token);
+            mBuffer.add(os);
+        } catch (Exception e) {
+            Log.wtf(TAG, "Exception while tracing state", e);
+        }
+    }
+
+    @Override
+    protected boolean shouldLogOnFrame() {
+        return mLogOnFrame;
+    }
+
+    @Override
+    protected boolean shouldLogOnTransaction() {
+        return !mLogOnFrame;
+    }
+
+    private void writeTraceToFileLocked() {
+        try {
+            Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "writeTraceToFileLocked");
+            ProtoOutputStream proto = new ProtoOutputStream();
+            proto.write(MAGIC_NUMBER, MAGIC_NUMBER_VALUE);
+            long timeOffsetNs =
+                    TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis())
+                    - SystemClock.elapsedRealtimeNanos();
+            proto.write(REAL_TO_ELAPSED_TIME_OFFSET_NANOS, timeOffsetNs);
+            mBuffer.writeTraceToFile(mTraceFile, proto);
+        } catch (IOException e) {
+            Log.e(TAG, "Unable to write buffer to file", e);
+        } finally {
+            Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
+        }
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTracingTest.java b/services/tests/wmtests/src/com/android/server/wm/WindowTracingLegacyTest.java
similarity index 97%
rename from services/tests/wmtests/src/com/android/server/wm/WindowTracingTest.java
rename to services/tests/wmtests/src/com/android/server/wm/WindowTracingLegacyTest.java
index c183403..48a8d55 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTracingTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTracingLegacyTest.java
@@ -63,7 +63,7 @@
  */
 @SmallTest
 @Presubmit
-public class WindowTracingTest {
+public class WindowTracingLegacyTest {
 
     private static final byte[] MAGIC_HEADER = new byte[]{
             0x9, 0x57, 0x49, 0x4e, 0x54, 0x52, 0x41, 0x43, 0x45,
@@ -88,7 +88,7 @@
         mFile = testContext.getFileStreamPath("tracing_test.dat");
         mFile.delete();
 
-        mWindowTracing = new WindowTracing(mFile, mWmMock, mChoreographer,
+        mWindowTracing = new WindowTracingLegacy(mFile, mWmMock, mChoreographer,
                 new WindowManagerGlobalLock(), 1024);
     }