Merge "Short term solution for the in-animation-dismissal-freeze bug." into main
diff --git a/core/java/android/security/FileIntegrityManager.java b/core/java/android/security/FileIntegrityManager.java
index dae3202..025aac9 100644
--- a/core/java/android/security/FileIntegrityManager.java
+++ b/core/java/android/security/FileIntegrityManager.java
@@ -53,10 +53,10 @@
      * verification, although the app APIs are only made available to apps in a later SDK version.
      * Only when this method returns true, the other fs-verity APIs in the same class can succeed.
      *
-     * <p>The app may not need this method and just call the other APIs (i.e. {@link
-     * #setupFsVerity(File)} and {@link #getFsVerityDigest(File)}) normally and handle any failure.
-     * If some app feature really depends on fs-verity (e.g. protecting integrity of a large file
-     * download), an early check of support status may avoid any cost if it is to fail late.
+     * <p>The app may not need this method and just call the other APIs normally and handle any
+     * failure. If some app feature really depends on fs-verity (e.g. protecting integrity of a
+     * large file download), an early check of support status may avoid any cost if it is to fail
+     * late.
      *
      * <p>Note: for historical reasons this is named {@code isApkVeritySupported()} instead of
      * {@code isFsVeritySupported()}. It has also been available since API level 30, predating the
diff --git a/core/java/android/view/HdrRenderState.java b/core/java/android/view/HdrRenderState.java
new file mode 100644
index 0000000..2fbbf48
--- /dev/null
+++ b/core/java/android/view/HdrRenderState.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.os.SystemClock;
+
+import com.android.graphics.hwui.flags.Flags;
+
+import java.util.function.Consumer;
+
+/** @hide */
+class HdrRenderState implements Consumer<Display> {
+    // Targeting an animation from 1x to 5x over 400ms means we need to increase by 0.01/ms
+    private static final float TRANSITION_PER_MS = 0.01f;
+
+    private static final boolean FLAG_ANIMATE_ENABLED = Flags.animateHdrTransitions();
+
+    private final ViewRootImpl mViewRoot;
+
+    private boolean mIsListenerRegistered = false;
+    private boolean mUpdateHdrSdrRatioInfo = false;
+    private float mDesiredHdrSdrRatio = 1f;
+    private float mTargetHdrSdrRatio = 1f;
+    private float mRenderHdrSdrRatio = 1f;
+    private float mPreviousRenderRatio = 1f;
+    private long mLastUpdateMillis = -1;
+
+    HdrRenderState(ViewRootImpl viewRoot) {
+        mViewRoot = viewRoot;
+    }
+
+    @Override
+    public void accept(Display display) {
+        forceUpdateHdrSdrRatio();
+        mViewRoot.invalidate();
+    }
+
+    boolean isHdrEnabled() {
+        return mDesiredHdrSdrRatio >= 1.01f;
+    }
+
+    void stopListening() {
+        if (mIsListenerRegistered) {
+            mViewRoot.mDisplay.unregisterHdrSdrRatioChangedListener(this);
+            mIsListenerRegistered = false;
+        }
+    }
+
+    void startListening() {
+        if (isHdrEnabled() && !mIsListenerRegistered && mViewRoot.mDisplay != null) {
+            mViewRoot.mDisplay.registerHdrSdrRatioChangedListener(mViewRoot.mExecutor, this);
+        }
+    }
+
+    /** @return true if something changed, else false */
+    boolean updateForFrame(long frameTimeMillis) {
+        boolean hasUpdate = mUpdateHdrSdrRatioInfo;
+        mUpdateHdrSdrRatioInfo = false;
+        mRenderHdrSdrRatio = mTargetHdrSdrRatio;
+        long timeDelta = Math.max(Math.min(32, frameTimeMillis - mLastUpdateMillis), 8);
+        final float maxStep = timeDelta * TRANSITION_PER_MS;
+        mLastUpdateMillis = frameTimeMillis;
+        if (hasUpdate && FLAG_ANIMATE_ENABLED) {
+            if (mTargetHdrSdrRatio == 1.0f) {
+                mPreviousRenderRatio = mTargetHdrSdrRatio;
+            } else {
+                float delta = mTargetHdrSdrRatio - mPreviousRenderRatio;
+                if (delta > maxStep) {
+                    mRenderHdrSdrRatio = mPreviousRenderRatio + maxStep;
+                    mUpdateHdrSdrRatioInfo = true;
+                    mViewRoot.invalidate();
+                }
+                mPreviousRenderRatio = mRenderHdrSdrRatio;
+            }
+        }
+        return hasUpdate;
+    }
+
+    float getDesiredHdrSdrRatio() {
+        return mDesiredHdrSdrRatio;
+    }
+
+    float getRenderHdrSdrRatio() {
+        return mRenderHdrSdrRatio;
+    }
+
+    void forceUpdateHdrSdrRatio() {
+        mTargetHdrSdrRatio = Math.min(mDesiredHdrSdrRatio, mViewRoot.mDisplay.getHdrSdrRatio());
+        mUpdateHdrSdrRatioInfo = true;
+    }
+
+    void setDesiredHdrSdrRatio(float desiredRatio) {
+        mLastUpdateMillis = SystemClock.uptimeMillis();
+        // TODO: When decreasing the desired ratio we need to animate it downwards
+        if (desiredRatio != mDesiredHdrSdrRatio) {
+            mDesiredHdrSdrRatio = desiredRatio;
+            forceUpdateHdrSdrRatio();
+            mViewRoot.invalidate();
+
+            if (isHdrEnabled()) {
+                startListening();
+            } else {
+                stopListening();
+            }
+        }
+    }
+}
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 870ec4b..1530aa7 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -735,10 +735,7 @@
 
     private BLASTBufferQueue mBlastBufferQueue;
 
-    private boolean mUpdateHdrSdrRatioInfo = false;
-    private float mDesiredHdrSdrRatio = 1f;
-    private float mRenderHdrSdrRatio = 1f;
-    private Consumer<Display> mHdrSdrRatioChangedListener = null;
+    private final HdrRenderState mHdrRenderState = new HdrRenderState(this);
 
     /**
      * Child container layer of {@code mSurface} with the same bounds as its parent, and cropped to
@@ -1813,7 +1810,7 @@
                 mAttachInfo.mThreadedRenderer = renderer;
                 renderer.setSurfaceControl(mSurfaceControl, mBlastBufferQueue);
                 updateColorModeIfNeeded(attrs.getColorMode(), attrs.getDesiredHdrHeadroom());
-                updateRenderHdrSdrRatio();
+                mHdrRenderState.forceUpdateHdrSdrRatio();
                 updateForceDarkMode();
                 mAttachInfo.mHardwareAccelerated = true;
                 mAttachInfo.mHardwareAccelerationRequested = true;
@@ -2156,9 +2153,7 @@
     private void updateInternalDisplay(int displayId, Resources resources) {
         final Display preferredDisplay =
                 ResourcesManager.getInstance().getAdjustedDisplay(displayId, resources);
-        if (mHdrSdrRatioChangedListener != null && mDisplay != null) {
-            mDisplay.unregisterHdrSdrRatioChangedListener(mHdrSdrRatioChangedListener);
-        }
+        mHdrRenderState.stopListening();
         if (preferredDisplay == null) {
             // Fallback to use default display.
             Slog.w(TAG, "Cannot get desired display with Id: " + displayId);
@@ -2167,9 +2162,7 @@
         } else {
             mDisplay = preferredDisplay;
         }
-        if (mHdrSdrRatioChangedListener != null && mDisplay != null) {
-            mDisplay.registerHdrSdrRatioChangedListener(mExecutor, mHdrSdrRatioChangedListener);
-        }
+        mHdrRenderState.startListening();
         mContext.updateDisplay(mDisplay.getDisplayId());
     }
 
@@ -5154,11 +5147,12 @@
 
                 useAsyncReport = true;
 
-                if (mUpdateHdrSdrRatioInfo) {
-                    mUpdateHdrSdrRatioInfo = false;
+                if (mHdrRenderState.updateForFrame(mAttachInfo.mDrawingTime)) {
+                    final float renderRatio = mHdrRenderState.getRenderHdrSdrRatio();
                     applyTransactionOnDraw(mTransaction.setExtendedRangeBrightness(
-                            getSurfaceControl(), mRenderHdrSdrRatio, mDesiredHdrSdrRatio));
-                    mAttachInfo.mThreadedRenderer.setTargetHdrSdrRatio(mRenderHdrSdrRatio);
+                            getSurfaceControl(), renderRatio,
+                            mHdrRenderState.getDesiredHdrSdrRatio()));
+                    mAttachInfo.mThreadedRenderer.setTargetHdrSdrRatio(renderRatio);
                 }
 
                 if (activeSyncGroup != null) {
@@ -5769,11 +5763,6 @@
         }
     }
 
-    private void updateRenderHdrSdrRatio() {
-        mRenderHdrSdrRatio = Math.min(mDesiredHdrSdrRatio, mDisplay.getHdrSdrRatio());
-        mUpdateHdrSdrRatioInfo = true;
-    }
-
     private void updateColorModeIfNeeded(@ActivityInfo.ColorMode int colorMode,
             float desiredRatio) {
         if (mAttachInfo.mThreadedRenderer == null) {
@@ -5793,22 +5782,8 @@
         if (desiredRatio == 0 || desiredRatio > automaticRatio) {
             desiredRatio = automaticRatio;
         }
-        if (desiredRatio != mDesiredHdrSdrRatio) {
-            mDesiredHdrSdrRatio = desiredRatio;
-            updateRenderHdrSdrRatio();
-            invalidate();
 
-            if (mDesiredHdrSdrRatio < 1.01f) {
-                mDisplay.unregisterHdrSdrRatioChangedListener(mHdrSdrRatioChangedListener);
-                mHdrSdrRatioChangedListener = null;
-            } else {
-                mHdrSdrRatioChangedListener = display -> {
-                    updateRenderHdrSdrRatio();
-                    invalidate();
-                };
-                mDisplay.registerHdrSdrRatioChangedListener(mExecutor, mHdrSdrRatioChangedListener);
-            }
-        }
+        mHdrRenderState.setDesiredHdrSdrRatio(desiredRatio);
     }
 
     @Override
@@ -6428,7 +6403,7 @@
     }
 
     final ViewRootHandler mHandler = new ViewRootHandler();
-    private final Executor mExecutor = (Runnable r) -> {
+    final Executor mExecutor = (Runnable r) -> {
         mHandler.post(r);
     };
 
@@ -8764,7 +8739,7 @@
             if (mAttachInfo.mThreadedRenderer != null) {
                 mAttachInfo.mThreadedRenderer.setSurfaceControl(mSurfaceControl, mBlastBufferQueue);
             }
-            updateRenderHdrSdrRatio();
+            mHdrRenderState.forceUpdateHdrSdrRatio();
             if (mPreviousTransformHint != transformHint) {
                 mPreviousTransformHint = transformHint;
                 dispatchTransformHintChanged(transformHint);
@@ -9312,9 +9287,7 @@
     private void destroyHardwareRenderer() {
         ThreadedRenderer hardwareRenderer = mAttachInfo.mThreadedRenderer;
 
-        if (mHdrSdrRatioChangedListener != null) {
-            mDisplay.unregisterHdrSdrRatioChangedListener(mHdrSdrRatioChangedListener);
-        }
+        mHdrRenderState.stopListening();
 
         if (hardwareRenderer != null) {
             if (mHardwareRendererObserver != null) {
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromAllApps.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromAllApps.kt
index d7b306c..03170a3 100644
--- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromAllApps.kt
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromAllApps.kt
@@ -57,10 +57,13 @@
 
         tapl.setEnableRotation(true)
         tapl.setExpectedRotation(rotation.value)
+
+        tapl.enableBlockTimeout(true)
     }
 
     @Test
     open fun enterSplitScreenByDragFromAllApps() {
+        tapl.showTaskbarIfHidden()
         tapl.launchedAppState.taskbar
             .openAllApps()
             .getAppIcon(secondaryApp.appName)
@@ -72,5 +75,6 @@
     fun teardown() {
         primaryApp.exit(wmHelper)
         secondaryApp.exit(wmHelper)
+        tapl.enableBlockTimeout(false)
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt
index 8134fdd..479d01d 100644
--- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt
@@ -59,10 +59,13 @@
 
         tapl.setEnableRotation(true)
         tapl.setExpectedRotation(rotation.value)
+
+        tapl.enableBlockTimeout(true)
     }
 
     @Test
     open fun enterSplitScreenByDragFromShortcut() {
+        tapl.showTaskbarIfHidden()
         tapl.launchedAppState.taskbar
             .getAppIcon(secondaryApp.appName)
             .openDeepShortcutMenu()
@@ -83,6 +86,7 @@
     fun teardwon() {
         primaryApp.exit(wmHelper)
         secondaryApp.exit(wmHelper)
+        tapl.enableBlockTimeout(false)
     }
 
     companion object {
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt
index 3417744..625c56b 100644
--- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt
@@ -54,6 +54,8 @@
         tapl.setEnableRotation(true)
         tapl.setExpectedRotation(rotation.value)
 
+        tapl.enableBlockTimeout(true)
+
         tapl.goHome()
         SplitScreenUtils.createShortcutOnHotseatIfNotExist(tapl, secondaryApp.appName)
         primaryApp.launchViaIntent(wmHelper)
@@ -61,6 +63,7 @@
 
     @Test
     open fun enterSplitScreenByDragFromTaskbar() {
+        tapl.showTaskbarIfHidden()
         tapl.launchedAppState.taskbar
             .getAppIcon(secondaryApp.appName)
             .dragToSplitscreen(secondaryApp.packageName, primaryApp.packageName)
@@ -71,6 +74,7 @@
     fun teardown() {
         primaryApp.exit(wmHelper)
         secondaryApp.exit(wmHelper)
+        tapl.enableBlockTimeout(false)
     }
 
     companion object {
diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt
index 394864a..5c43cbd 100644
--- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt
@@ -23,6 +23,7 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.utils.SplitScreenUtils
+import org.junit.After
 import org.junit.Assume
 import org.junit.Before
 import org.junit.FixMethodOrder
@@ -42,8 +43,10 @@
             setup {
                 tapl.goHome()
                 primaryApp.launchViaIntent(wmHelper)
+                tapl.enableBlockTimeout(true)
             }
             transitions {
+                tapl.showTaskbarIfHidden()
                 tapl.launchedAppState.taskbar
                     .openAllApps()
                     .getAppIcon(secondaryApp.appName)
@@ -57,6 +60,11 @@
         Assume.assumeTrue(tapl.isTablet)
     }
 
+    @After
+    fun after() {
+        tapl.enableBlockTimeout(false)
+    }
+
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt
index 3b3be84..15ad0c1 100644
--- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt
@@ -23,6 +23,7 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.utils.SplitScreenUtils
+import org.junit.After
 import org.junit.Assume
 import org.junit.Before
 import org.junit.FixMethodOrder
@@ -42,13 +43,20 @@
         Assume.assumeTrue(tapl.isTablet)
     }
 
+    @After
+    fun after() {
+        tapl.enableBlockTimeout(false)
+    }
+
     protected val thisTransition: FlickerBuilder.() -> Unit = {
         setup {
             tapl.goHome()
             SplitScreenUtils.createShortcutOnHotseatIfNotExist(tapl, secondaryApp.appName)
             primaryApp.launchViaIntent(wmHelper)
+            tapl.enableBlockTimeout(true)
         }
         transitions {
+            tapl.showTaskbarIfHidden()
             tapl.launchedAppState.taskbar
                 .getAppIcon(secondaryApp.appName)
                 .openDeepShortcutMenu()
diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt
index eff3559..ca8adb1 100644
--- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt
@@ -23,6 +23,7 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.utils.SplitScreenUtils
+import org.junit.After
 import org.junit.Assume
 import org.junit.Before
 import org.junit.FixMethodOrder
@@ -44,6 +45,7 @@
                 primaryApp.launchViaIntent(wmHelper)
             }
             transitions {
+                tapl.showTaskbarIfHidden()
                 tapl.launchedAppState.taskbar
                     .getAppIcon(secondaryApp.appName)
                     .dragToSplitscreen(secondaryApp.packageName, primaryApp.packageName)
@@ -54,6 +56,12 @@
     @Before
     fun before() {
         Assume.assumeTrue(tapl.isTablet)
+        tapl.enableBlockTimeout(true)
+    }
+
+    @After
+    fun after() {
+        tapl.enableBlockTimeout(false)
     }
 
     companion object {
diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig
index 64b2a93..ca11975 100644
--- a/libs/hwui/aconfig/hwui_flags.aconfig
+++ b/libs/hwui/aconfig/hwui_flags.aconfig
@@ -48,3 +48,10 @@
   description: "Enable r_8, r_16_uint, rg_1616_uint, and rgba_10101010 in the SDK"
   bug: "292545615"
 }
+
+flag {
+  name: "animate_hdr_transitions"
+  namespace: "core_graphics"
+  description: "Automatically animate all changes in HDR headroom"
+  bug: "314810174"
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
index ea3006f..8896e6e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
@@ -39,6 +39,7 @@
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
+import javax.inject.Provider
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -83,7 +84,7 @@
         underTest =
             CommunalEditModeViewModel(
                 withDeps.communalInteractor,
-                shadeViewController,
+                Provider { shadeViewController },
                 powerManager,
                 mediaHost,
             )
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index 9bd0835..7fbcae0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -39,6 +39,7 @@
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
+import javax.inject.Provider
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -84,7 +85,7 @@
             CommunalViewModel(
                 withDeps.communalInteractor,
                 withDeps.tutorialInteractor,
-                shadeViewController,
+                Provider { shadeViewController },
                 powerManager,
                 mediaHost,
             )
diff --git a/packages/SystemUI/res/layout/screen_record_options.xml b/packages/SystemUI/res/layout/screen_record_options.xml
index 8916e42..fa345c9 100644
--- a/packages/SystemUI/res/layout/screen_record_options.xml
+++ b/packages/SystemUI/res/layout/screen_record_options.xml
@@ -40,16 +40,22 @@
             android:popupBackground="@drawable/screenrecord_spinner_background"
             android:dropDownWidth="274dp"
             android:importantForAccessibility="yes"/>
-        <Switch
+        <FrameLayout
+            android:id="@+id/screenrecord_audio_switch_container"
             android:layout_width="wrap_content"
-            android:minWidth="48dp"
-            android:layout_height="48dp"
-            android:layout_weight="0"
-            android:layout_gravity="end"
-            android:id="@+id/screenrecord_audio_switch"
-            android:contentDescription="@string/screenrecord_audio_label"
-            style="@style/ScreenRecord.Switch"
-            android:importantForAccessibility="yes"/>
+            android:layout_height="wrap_content">
+            <Switch
+                android:layout_width="wrap_content"
+                android:minWidth="48dp"
+                android:layout_height="48dp"
+                android:layout_gravity="end"
+                android:focusable="false"
+                android:clickable="false"
+                android:id="@+id/screenrecord_audio_switch"
+                android:contentDescription="@string/screenrecord_audio_label"
+                style="@style/ScreenRecord.Switch"
+                android:importantForAccessibility="yes"/>
+        </FrameLayout>
     </LinearLayout>
     <LinearLayout
         android:id="@+id/show_taps"
@@ -75,13 +81,20 @@
             android:fontFamily="@*android:string/config_bodyFontFamily"
             android:textColor="?android:attr/textColorPrimary"
             android:contentDescription="@string/screenrecord_taps_label"/>
-        <Switch
+        <FrameLayout
+            android:id="@+id/screenrecord_taps_switch_container"
             android:layout_width="wrap_content"
-            android:minWidth="48dp"
-            android:layout_height="48dp"
-            android:id="@+id/screenrecord_taps_switch"
-            android:contentDescription="@string/screenrecord_taps_label"
-            style="@style/ScreenRecord.Switch"
-            android:importantForAccessibility="yes"/>
+            android:layout_height="wrap_content">
+            <Switch
+                android:layout_width="wrap_content"
+                android:minWidth="48dp"
+                android:layout_height="48dp"
+                android:focusable="false"
+                android:clickable="false"
+                android:id="@+id/screenrecord_taps_switch"
+                android:contentDescription="@string/screenrecord_taps_label"
+                style="@style/ScreenRecord.Switch"
+                android:importantForAccessibility="yes"/>
+        </FrameLayout>
     </LinearLayout>
 </LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
index bed4283..333fc19 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
@@ -24,13 +24,14 @@
 import com.android.systemui.communal.shared.model.CommunalSceneKey
 import com.android.systemui.media.controls.ui.MediaHost
 import com.android.systemui.shade.ShadeViewController
+import javax.inject.Provider
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
 
 /** The base view model for the communal hub. */
 abstract class BaseCommunalViewModel(
     private val communalInteractor: CommunalInteractor,
-    private val shadeViewController: ShadeViewController,
+    private val shadeViewController: Provider<ShadeViewController>,
     private val powerManager: PowerManager,
     val mediaHost: MediaHost,
 ) {
@@ -48,7 +49,7 @@
     fun onOuterTouch(motionEvent: MotionEvent) {
         // Forward the touch to the shade so that basic gestures like swipe up/down for
         // shade/bouncer work.
-        shadeViewController.handleExternalTouch(motionEvent)
+        shadeViewController.get().handleExternalTouch(motionEvent)
     }
 
     // TODO(b/308813166): remove once CommunalContainer is moved lower in z-order and doesn't block
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
index b6843c5..c82e000 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.shade.ShadeViewController
 import javax.inject.Inject
 import javax.inject.Named
+import javax.inject.Provider
 import kotlinx.coroutines.flow.Flow
 
 /** The view model for communal hub in edit mode. */
@@ -33,7 +34,7 @@
 @Inject
 constructor(
     private val communalInteractor: CommunalInteractor,
-    shadeViewController: ShadeViewController,
+    shadeViewController: Provider<ShadeViewController>,
     powerManager: PowerManager,
     @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost,
 ) : BaseCommunalViewModel(communalInteractor, shadeViewController, powerManager, mediaHost) {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
index d7dcdb9..abf1986 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.shade.ShadeViewController
 import javax.inject.Inject
 import javax.inject.Named
+import javax.inject.Provider
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
@@ -39,7 +40,7 @@
 constructor(
     private val communalInteractor: CommunalInteractor,
     tutorialInteractor: CommunalTutorialInteractor,
-    shadeViewController: ShadeViewController,
+    shadeViewController: Provider<ShadeViewController>,
     powerManager: PowerManager,
     @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost,
 ) : BaseCommunalViewModel(communalInteractor, shadeViewController, powerManager, mediaHost) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt
index 10d51a5..3eb26f4 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt
@@ -28,6 +28,7 @@
 import android.view.View
 import android.view.View.GONE
 import android.view.View.VISIBLE
+import android.view.ViewGroup
 import android.view.accessibility.AccessibilityNodeInfo
 import android.widget.AdapterView
 import android.widget.ArrayAdapter
@@ -64,10 +65,13 @@
         mediaProjectionMetricsLogger,
         R.drawable.ic_screenrecord,
         R.color.screenrecord_icon_color
-    ), SystemUIDialog.Delegate {
+    ),
+    SystemUIDialog.Delegate {
     private lateinit var tapsSwitch: Switch
+    private lateinit var tapsSwitchContainer: ViewGroup
     private lateinit var tapsView: View
     private lateinit var audioSwitch: Switch
+    private lateinit var audioSwitchContainer: ViewGroup
     private lateinit var options: Spinner
 
     override fun createDialog(): SystemUIDialog {
@@ -114,12 +118,17 @@
     private fun initRecordOptionsView() {
         audioSwitch = dialog.requireViewById(R.id.screenrecord_audio_switch)
         tapsSwitch = dialog.requireViewById(R.id.screenrecord_taps_switch)
+        audioSwitchContainer = dialog.requireViewById(R.id.screenrecord_audio_switch_container)
+        tapsSwitchContainer = dialog.requireViewById(R.id.screenrecord_taps_switch_container)
 
         // Add these listeners so that the switch only responds to movement
         // within its target region, to meet accessibility requirements
         audioSwitch.setOnTouchListener { _, event -> event.action == ACTION_MOVE }
         tapsSwitch.setOnTouchListener { _, event -> event.action == ACTION_MOVE }
 
+        audioSwitchContainer.setOnClickListener { audioSwitch.toggle() }
+        tapsSwitchContainer.setOnClickListener { tapsSwitch.toggle() }
+
         tapsView = dialog.requireViewById(R.id.show_taps)
         updateTapsViewVisibility()
 
diff --git a/packages/SystemUI/src/com/android/systemui/volume/CsdWarningDialog.java b/packages/SystemUI/src/com/android/systemui/volume/CsdWarningDialog.java
index d6e6f3f..bd698ab 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/CsdWarningDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/CsdWarningDialog.java
@@ -157,6 +157,7 @@
         if (mCsdWarning == AudioManager.CSD_WARNING_DOSE_REPEATED_5X) {
             // only show a notification in case we reached 500% of dose
             show5XNotification();
+            dismissCsdDialog();
             return;
         }
         super.show();
@@ -217,6 +218,10 @@
 
     @Override
     public void onDismiss(DialogInterface unused) {
+        dismissCsdDialog();
+    }
+
+    private void dismissCsdDialog() {
         try {
             mContext.unregisterReceiver(mReceiver);
         } catch (IllegalArgumentException e) {
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 8ed3fd6..b4cf34e 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -201,6 +201,7 @@
         "biometrics_flags_lib",
         "am_flags_lib",
         "com_android_wm_shell_flags_lib",
+        "com.android.server.utils_aconfig-java",
         "service-jobscheduler-deviceidle.flags-aconfig-java",
     ],
     javac_shard_size: 50,
diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java
index eb3ec24..05d07ae 100644
--- a/services/core/java/com/android/server/BinaryTransparencyService.java
+++ b/services/core/java/com/android/server/BinaryTransparencyService.java
@@ -1464,15 +1464,17 @@
         FrameworkStatsLog.write(FrameworkStatsLog.VBMETA_DIGEST_REPORTED, mVbmetaDigest);
 
         if (android.security.Flags.binaryTransparencySepolicyHash()) {
-            byte[] sepolicyHash = PackageUtils.computeSha256DigestForLargeFileAsBytes(
-                    "/sys/fs/selinux/policy", PackageUtils.createLargeFileBuffer());
-            String sepolicyHashEncoded = null;
-            if (sepolicyHash != null) {
-                sepolicyHashEncoded = HexEncoding.encodeToString(sepolicyHash, false);
-                Slog.d(TAG, "sepolicy hash: " + sepolicyHashEncoded);
-            }
-            FrameworkStatsLog.write(FrameworkStatsLog.BOOT_INTEGRITY_INFO_REPORTED,
-                    sepolicyHashEncoded, mVbmetaDigest);
+            IoThread.getExecutor().execute(() -> {
+                byte[] sepolicyHash = PackageUtils.computeSha256DigestForLargeFileAsBytes(
+                        "/sys/fs/selinux/policy", PackageUtils.createLargeFileBuffer());
+                String sepolicyHashEncoded = null;
+                if (sepolicyHash != null) {
+                    sepolicyHashEncoded = HexEncoding.encodeToString(sepolicyHash, false);
+                    Slog.d(TAG, "sepolicy hash: " + sepolicyHashEncoded);
+                }
+                FrameworkStatsLog.write(FrameworkStatsLog.BOOT_INTEGRITY_INFO_REPORTED,
+                        sepolicyHashEncoded, mVbmetaDigest);
+            });
         }
     }
 
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 5f1a7e7..7191684 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -241,6 +241,7 @@
 import com.android.server.am.ServiceRecord.ShortFgsInfo;
 import com.android.server.pm.KnownPackages;
 import com.android.server.uri.NeededUriGrants;
+import com.android.server.utils.AnrTimer;
 import com.android.server.wm.ActivityServiceConnectionsHolder;
 
 import java.io.FileDescriptor;
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 469f209..2d687de 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -475,6 +475,7 @@
 import com.android.server.uri.GrantUri;
 import com.android.server.uri.NeededUriGrants;
 import com.android.server.uri.UriGrantsManagerInternal;
+import com.android.server.utils.AnrTimer;
 import com.android.server.utils.PriorityDump;
 import com.android.server.utils.Slogf;
 import com.android.server.utils.TimingsTraceAndSlog;
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index ad49991..2cac7a0 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -88,6 +88,7 @@
 import com.android.server.am.BroadcastProcessQueue.BroadcastConsumer;
 import com.android.server.am.BroadcastProcessQueue.BroadcastPredicate;
 import com.android.server.am.BroadcastRecord.DeliveryState;
+import com.android.server.utils.AnrTimer;
 
 import dalvik.annotation.optimization.NeverCompile;
 
diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig
index 2ed079a..d9e8ddd 100644
--- a/services/core/java/com/android/server/am/flags.aconfig
+++ b/services/core/java/com/android/server/am/flags.aconfig
@@ -9,14 +9,6 @@
 }
 
 flag {
-     name: "anr_timer_service_enabled"
-     namespace: "system_performance"
-     is_fixed_read_only: true
-     description: "Feature flag for the ANR timer service"
-     bug: "282428924"
-}
-
-flag {
     name: "fgs_abuse_detection"
     namespace: "backstage_power"
     description: "Detect abusive FGS behavior for certain types (camera, mic, media, location)."
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index 90c4063..df9e741 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -2908,46 +2908,69 @@
             if (service == null) {
                 return;
             }
-            List<RouterRecord> activeRouterRecords = Collections.emptyList();
+            List<RouterRecord> activeRouterRecords;
             List<RouterRecord> allRouterRecords = getRouterRecords();
-            List<ManagerRecord> managerRecords = getManagerRecords();
 
-            boolean isManagerScanning = false;
-            if (Flags.disableScreenOffBroadcastReceiver()
-                    || service.mPowerManager.isInteractive()) {
-                isManagerScanning = managerRecords.stream().anyMatch(manager ->
-                        manager.mIsScanning && service.mActivityManager
-                                .getPackageImportance(manager.mOwnerPackageName)
-                                <= sPackageImportanceForScanning);
+            boolean areManagersScanning = areManagersScanning(service, getManagerRecords());
 
-                if (isManagerScanning) {
-                    activeRouterRecords = allRouterRecords;
-                } else {
-                    activeRouterRecords =
-                            allRouterRecords.stream()
-                                    .filter(
-                                            record ->
-                                                    service.mActivityManager.getPackageImportance(
-                                                                    record.mPackageName)
-                                                            <= sPackageImportanceForScanning)
-                                    .collect(Collectors.toList());
-                }
+            if (areManagersScanning) {
+                activeRouterRecords = allRouterRecords;
+            } else {
+                activeRouterRecords = getIndividuallyActiveRouters(service, allRouterRecords);
             }
 
-            for (MediaRoute2Provider provider : mRouteProviders) {
-                if (provider instanceof MediaRoute2ProviderServiceProxy) {
-                    ((MediaRoute2ProviderServiceProxy) provider)
-                            .setManagerScanning(isManagerScanning);
-                }
-            }
+            updateManagerScanningForProviders(areManagersScanning);
 
-            // Build a composite RouteDiscoveryPreference that matches all of the routes
-            // that match one or more of the individual discovery preferences. It may also
-            // match additional routes. The composite RouteDiscoveryPreference can be used
-            // to query route providers once to obtain all of the routes of interest, which
-            // can be subsequently filtered for the individual discovery preferences.
-            Set<String> preferredFeatures = new HashSet<>();
             Set<String> activelyScanningPackages = new HashSet<>();
+            RouteDiscoveryPreference newPreference =
+                    buildCompositeDiscoveryPreference(
+                            activeRouterRecords, areManagersScanning, activelyScanningPackages);
+
+            if (updateScanningOnUserRecord(service, activelyScanningPackages, newPreference)) {
+                updateDiscoveryPreferenceForProviders(activelyScanningPackages);
+            }
+        }
+
+        private void updateDiscoveryPreferenceForProviders(Set<String> activelyScanningPackages) {
+            for (MediaRoute2Provider provider : mRouteProviders) {
+                provider.updateDiscoveryPreference(
+                        activelyScanningPackages, mUserRecord.mCompositeDiscoveryPreference);
+            }
+        }
+
+        private boolean updateScanningOnUserRecord(
+                MediaRouter2ServiceImpl service,
+                Set<String> activelyScanningPackages,
+                RouteDiscoveryPreference newPreference) {
+            synchronized (service.mLock) {
+                if (newPreference.equals(mUserRecord.mCompositeDiscoveryPreference)
+                        && activelyScanningPackages.equals(mUserRecord.mActivelyScanningPackages)) {
+                    return false;
+                }
+                mUserRecord.mCompositeDiscoveryPreference = newPreference;
+                mUserRecord.mActivelyScanningPackages = activelyScanningPackages;
+            }
+            return true;
+        }
+
+        /**
+         * Returns a composite {@link RouteDiscoveryPreference} that aggregates every router
+         * record's individual discovery preference.
+         *
+         * <p>The {@link RouteDiscoveryPreference#shouldPerformActiveScan() active scan value} of
+         * the composite discovery preference is true if one of the router records is actively
+         * scanning or if {@code shouldForceActiveScan} is true.
+         *
+         * <p>The composite RouteDiscoveryPreference is used to query route providers once to obtain
+         * all the routes of interest, which can be subsequently filtered for the individual
+         * discovery preferences.
+         */
+        @NonNull
+        private static RouteDiscoveryPreference buildCompositeDiscoveryPreference(
+                List<RouterRecord> activeRouterRecords,
+                boolean shouldForceActiveScan,
+                Set<String> activelyScanningPackages) {
+            Set<String> preferredFeatures = new HashSet<>();
             boolean activeScan = false;
             for (RouterRecord activeRouterRecord : activeRouterRecords) {
                 RouteDiscoveryPreference preference = activeRouterRecord.mDiscoveryPreference;
@@ -2957,23 +2980,53 @@
                     activelyScanningPackages.add(activeRouterRecord.mPackageName);
                 }
             }
-            RouteDiscoveryPreference newPreference = new RouteDiscoveryPreference.Builder(
-                    List.copyOf(preferredFeatures), activeScan || isManagerScanning).build();
+            return new RouteDiscoveryPreference.Builder(
+                            List.copyOf(preferredFeatures), activeScan || shouldForceActiveScan)
+                    .build();
+        }
 
-            synchronized (service.mLock) {
-                if (newPreference.equals(mUserRecord.mCompositeDiscoveryPreference)
-                        && activelyScanningPackages.equals(mUserRecord.mActivelyScanningPackages)) {
-                    return;
-                }
-                mUserRecord.mCompositeDiscoveryPreference = newPreference;
-                mUserRecord.mActivelyScanningPackages = activelyScanningPackages;
-            }
+        private void updateManagerScanningForProviders(boolean isManagerScanning) {
             for (MediaRoute2Provider provider : mRouteProviders) {
-                provider.updateDiscoveryPreference(
-                        activelyScanningPackages, mUserRecord.mCompositeDiscoveryPreference);
+                if (provider instanceof MediaRoute2ProviderServiceProxy) {
+                    ((MediaRoute2ProviderServiceProxy) provider)
+                            .setManagerScanning(isManagerScanning);
+                }
             }
         }
 
+        @NonNull
+        private static List<RouterRecord> getIndividuallyActiveRouters(
+                MediaRouter2ServiceImpl service, List<RouterRecord> allRouterRecords) {
+            if (!Flags.disableScreenOffBroadcastReceiver()
+                    && !service.mPowerManager.isInteractive()) {
+                return Collections.emptyList();
+            }
+
+            return allRouterRecords.stream()
+                    .filter(
+                            record ->
+                                    service.mActivityManager.getPackageImportance(
+                                                    record.mPackageName)
+                                            <= sPackageImportanceForScanning)
+                    .collect(Collectors.toList());
+        }
+
+        private static boolean areManagersScanning(
+                MediaRouter2ServiceImpl service, List<ManagerRecord> managerRecords) {
+            if (!Flags.disableScreenOffBroadcastReceiver()
+                    && !service.mPowerManager.isInteractive()) {
+                return false;
+            }
+
+            return managerRecords.stream()
+                    .anyMatch(
+                            manager ->
+                                    manager.mIsScanning
+                                            && service.mActivityManager.getPackageImportance(
+                                                            manager.mOwnerPackageName)
+                                                    <= sPackageImportanceForScanning);
+        }
+
         private MediaRoute2Provider findProvider(@Nullable String providerId) {
             for (MediaRoute2Provider provider : mRouteProviders) {
                 if (TextUtils.equals(provider.getUniqueId(), providerId)) {
diff --git a/services/core/java/com/android/server/utils/Android.bp b/services/core/java/com/android/server/utils/Android.bp
new file mode 100644
index 0000000..3a334be
--- /dev/null
+++ b/services/core/java/com/android/server/utils/Android.bp
@@ -0,0 +1,10 @@
+aconfig_declarations {
+    name: "com.android.server.utils-aconfig",
+    package: "com.android.server.utils",
+    srcs: ["*.aconfig"],
+}
+
+java_aconfig_library {
+    name: "com.android.server.utils_aconfig-java",
+    aconfig_declarations: "com.android.server.utils-aconfig",
+}
diff --git a/services/core/java/com/android/server/am/AnrTimer.java b/services/core/java/com/android/server/utils/AnrTimer.java
similarity index 98%
rename from services/core/java/com/android/server/am/AnrTimer.java
rename to services/core/java/com/android/server/utils/AnrTimer.java
index 3e17930..2b6dffb 100644
--- a/services/core/java/com/android/server/am/AnrTimer.java
+++ b/services/core/java/com/android/server/utils/AnrTimer.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.am;
+package com.android.server.utils;
 
 import static android.text.TextUtils.formatSimple;
 
@@ -77,7 +77,7 @@
  *
  * @hide
  */
-class AnrTimer<V> {
+public class AnrTimer<V> {
 
     /**
      * The log tag.
@@ -568,7 +568,7 @@
      * @param label A name for this instance.
      * @param extend A flag to indicate if expired timers can be granted extensions.
      */
-    AnrTimer(@NonNull Handler handler, int what, @NonNull String label, boolean extend) {
+    public AnrTimer(@NonNull Handler handler, int what, @NonNull String label, boolean extend) {
         this(handler, what, label, extend, new Injector(handler));
     }
 
@@ -580,7 +580,7 @@
      * @param what The "what" parameter for the expiration message.
      * @param label A name for this instance.
      */
-    AnrTimer(@NonNull Handler handler, int what, @NonNull String label) {
+    public AnrTimer(@NonNull Handler handler, int what, @NonNull String label) {
         this(handler, what, label, false);
     }
 
@@ -591,7 +591,7 @@
      *
      * @return true if the service is flag-enabled.
      */
-    boolean serviceEnabled() {
+    public boolean serviceEnabled() {
         return mFeature.enabled();
     }
 
@@ -856,7 +856,7 @@
      * @param timeoutMs The timer timeout, in milliseconds.
      * @return true if the timer was successfully created.
      */
-    boolean start(@NonNull V arg, int pid, int uid, long timeoutMs) {
+    public boolean start(@NonNull V arg, int pid, int uid, long timeoutMs) {
         return mFeature.start(arg, pid, uid, timeoutMs);
     }
 
@@ -867,7 +867,7 @@
      *
      * @return true if the timer was found and was running.
      */
-    boolean cancel(@NonNull V arg) {
+    public boolean cancel(@NonNull V arg) {
         return mFeature.cancel(arg);
     }
 
@@ -878,7 +878,7 @@
      *
      * @return true if the timer was found and was expired.
      */
-    boolean accept(@NonNull V arg) {
+    public boolean accept(@NonNull V arg) {
         return mFeature.accept(arg);
     }
 
@@ -892,7 +892,7 @@
      *
      * @return true if the timer was found and was expired.
      */
-    boolean discard(@NonNull V arg) {
+    public boolean discard(@NonNull V arg) {
         return mFeature.discard(arg);
     }
 
@@ -1010,7 +1010,7 @@
     /**
      * Dumpsys output.
      */
-    static void dump(@NonNull PrintWriter pw, boolean verbose) {
+    public static void dump(@NonNull PrintWriter pw, boolean verbose) {
         final IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
         ipw.println("AnrTimer statistics");
         ipw.increaseIndent();
diff --git a/services/core/java/com/android/server/utils/flags.aconfig b/services/core/java/com/android/server/utils/flags.aconfig
new file mode 100644
index 0000000..489e21a
--- /dev/null
+++ b/services/core/java/com/android/server/utils/flags.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.server.utils"
+
+flag {
+     name: "anr_timer_service_enabled"
+     namespace: "system_performance"
+     is_fixed_read_only: true
+     description: "Feature flag for the ANR timer service"
+     bug: "282428924"
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/AnrTimerTest.java b/services/tests/servicestests/src/com/android/server/am/AnrTimerTest.java
deleted file mode 100644
index 44d6760..0000000
--- a/services/tests/servicestests/src/com/android/server/am/AnrTimerTest.java
+++ /dev/null
@@ -1,389 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.am;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import android.platform.test.annotations.Presubmit;
-
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.os.SystemClock;
-
-import android.util.Log;
-
-import androidx.test.filters.SmallTest;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.util.ArrayList;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Build/Install/Run:
- *  atest FrameworksServicesTests:AnrTimerTest
- */
-@SmallTest
-@Presubmit
-public class AnrTimerTest {
-
-    /**
-     * A handler that allows control over when to dispatch messages and callbacks. Because most
-     * Handler methods are final, the only thing this handler can intercept is sending messages.
-     * This handler allows unit tests to be written without a need to sleep (which leads to flaky
-     * tests).
-     *
-     * This code was cloned from {@link com.android.systemui.utils.os.FakeHandler}.
-     */
-    static class TestHandler extends Handler {
-
-        private boolean mImmediate = true;
-        private ArrayList<Message> mQueuedMessages = new ArrayList<>();
-
-        ArrayList<Long> mDelays = new ArrayList<>();
-
-        TestHandler(Looper looper, Callback callback, boolean immediate) {
-            super(looper, callback);
-            mImmediate = immediate;
-        }
-
-        TestHandler(Looper looper, Callback callback) {
-            this(looper, callback, true);
-        }
-
-        /**
-         * Override sendMessageAtTime.  In immediate mode, the message is immediately dispatched.
-         * In non-immediate mode, the message is enqueued to the real handler.  In both cases, the
-         * original delay is computed by comparing the target dispatch time with 'now'.  This
-         * computation is prone to errors if the code experiences delays.  The computed time is
-         * captured in the mDelays list.
-         */
-        @Override
-        public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
-            long delay = uptimeMillis - SystemClock.uptimeMillis();
-            mDelays.add(delay);
-            if (mImmediate) {
-                mQueuedMessages.add(msg);
-                dispatchQueuedMessages();
-            } else {
-                super.sendMessageAtTime(msg, uptimeMillis);
-            }
-            return true;
-        }
-
-        void setImmediate(boolean immediate) {
-            mImmediate = immediate;
-        }
-
-        /** Dispatch any messages that have been queued on the calling thread. */
-        void dispatchQueuedMessages() {
-            ArrayList<Message> messages = new ArrayList<>(mQueuedMessages);
-            mQueuedMessages.clear();
-            for (Message msg : messages) {
-                dispatchMessage(msg);
-            }
-        }
-
-        /**
-         * Compare the captured delays with the input array.  The comparison is fuzzy because the
-         * captured delay (see sendMessageAtTime) is affected by process delays.
-         */
-        void verifyDelays(long[] r) {
-            final long FUZZ = 10;
-            assertEquals(r.length, mDelays.size());
-            for (int i = 0; i < mDelays.size(); i++) {
-                long t = r[i];
-                long v = mDelays.get(i);
-                assertTrue(v >= t - FUZZ && v <= t + FUZZ);
-            }
-        }
-    }
-
-    private Handler mHandler;
-    private CountDownLatch mLatch = null;
-    private ArrayList<Message> mMessages;
-
-    // The commonly used message timeout key.
-    private static final int MSG_TIMEOUT = 1;
-
-    @Before
-    public void setUp() {
-        mHandler = new Handler(Looper.getMainLooper(), this::expirationHandler);
-        mMessages = new ArrayList<>();
-        mLatch = new CountDownLatch(1);
-        AnrTimer.resetTimerListForHermeticTest();
-    }
-
-    @After
-    public void tearDown() {
-        mHandler = null;
-        mMessages = null;
-    }
-
-    // When a timer expires, set the expiration time in the message and add it to the queue.
-    private boolean expirationHandler(Message msg) {
-        mMessages.add(Message.obtain(msg));
-        mLatch.countDown();
-        return false;
-    }
-
-    // The test argument includes a pid and uid, and a tag.  The tag is used to distinguish
-    // different message instances.
-    private static class TestArg {
-        final int pid;
-        final int uid;
-        final int tag;
-
-        TestArg(int pid, int uid, int tag) {
-            this.pid = pid;
-            this.uid = uid;
-            this.tag = tag;
-        }
-        @Override
-        public String toString() {
-            return String.format("pid=%d uid=%d tag=%d", pid, uid, tag);
-        }
-    }
-
-    /**
-     * An instrumented AnrTimer.
-     */
-    private class TestAnrTimer extends AnrTimer {
-        // A local copy of 'what'.  The field in AnrTimer is private.
-        final int mWhat;
-
-        TestAnrTimer(Handler h, int key, String tag) {
-            super(h, key, tag);
-            mWhat = key;
-        }
-
-        TestAnrTimer() {
-            this(mHandler, MSG_TIMEOUT, caller());
-        }
-
-        TestAnrTimer(Handler h, int key, String tag, boolean extend, TestInjector injector) {
-            super(h, key, tag, extend, injector);
-            mWhat = key;
-        }
-
-        TestAnrTimer(boolean extend, TestInjector injector) {
-            this(mHandler, MSG_TIMEOUT, caller(), extend, injector);
-        }
-
-        // Return the name of method that called the constructor, assuming that this function is
-        // called from inside the constructor.  The calling method is used to name the AnrTimer
-        // instance so that logs are easier to understand.
-        private static String caller() {
-            final int n = 4;
-            StackTraceElement[] stack = Thread.currentThread().getStackTrace();
-            if (stack.length < n+1) return "test";
-            return stack[n].getMethodName();
-        }
-
-        boolean start(TestArg arg, long millis) {
-            return start(arg, arg.pid, arg.uid, millis);
-        }
-
-        int what() {
-            return mWhat;
-        }
-    }
-
-    private static class TestTracker extends AnrTimer.CpuTracker {
-        long index = 0;
-        final int skip;
-        TestTracker(int skip) {
-            this.skip = skip;
-        }
-        long delay(int pid) {
-            return index++ * skip;
-        }
-    }
-
-    private class TestInjector extends AnrTimer.Injector {
-        final boolean mImmediate;
-        final AnrTimer.CpuTracker mTracker;
-        TestHandler mTestHandler;
-
-        TestInjector(int skip, boolean immediate) {
-            super(mHandler);
-            mTracker = new TestTracker(skip);
-            mImmediate = immediate;
-        }
-
-        TestInjector(int skip) {
-            this(skip, true);
-        }
-
-        @Override
-        Handler newHandler(Handler.Callback callback) {
-            if (mTestHandler == null) {
-                mTestHandler = new TestHandler(mHandler.getLooper(), callback, mImmediate);
-            }
-            return mTestHandler;
-        }
-
-        /** Fetch the allocated handle. This does not check for nulls. */
-        TestHandler getHandler() {
-            return mTestHandler;
-        }
-
-        /**
-         * This override returns the tracker supplied in the constructor.  It does not create a
-         * new one.
-         */
-        @Override
-        AnrTimer.CpuTracker newTracker() {
-            return mTracker;
-        }
-
-        /** For test purposes, always enable the feature. */
-        @Override
-        boolean isFeatureEnabled() {
-            return true;
-        }
-    }
-
-    // Tests
-    // 1. Start a timer and wait for expiration.
-    // 2. Start a timer and cancel it.  Verify no expiration.
-    // 3. Start a timer.  Shortly thereafter, restart it.  Verify only one expiration.
-    // 4. Start a couple of timers.  Verify max active timers.  Discard one and verify the active
-    //    count drops by 1.  Accept one and verify the active count drops by 1.
-
-    @Test
-    public void testSimpleTimeout() throws Exception {
-        // Create an immediate TestHandler.
-        TestInjector injector = new TestInjector(0);
-        TestAnrTimer timer = new TestAnrTimer(false, injector);
-        TestArg t = new TestArg(1, 1, 3);
-        assertTrue(timer.start(t, 10));
-        // Delivery is immediate but occurs on a different thread.
-        assertTrue(mLatch.await(100, TimeUnit.MILLISECONDS));
-        assertEquals(1, mMessages.size());
-        Message m = mMessages.get(0);
-        assertEquals(timer.what(), m.what);
-        assertEquals(t, m.obj);
-
-        // Verify that the timer is still present.
-        assertEquals(1, AnrTimer.sizeOfTimerList());
-        assertTrue(timer.accept(t));
-        assertEquals(0, AnrTimer.sizeOfTimerList());
-
-        // Verify that the timer no longer exists.
-        assertFalse(timer.accept(t));
-    }
-
-    @Test
-    public void testCancel() throws Exception {
-        // Create an non-immediate TestHandler.
-        TestInjector injector = new TestInjector(0, false);
-        TestAnrTimer timer = new TestAnrTimer(false, injector);
-
-        Handler handler = injector.getHandler();
-        assertNotNull(handler);
-        assertTrue(handler instanceof TestHandler);
-
-        // The tests that follow check for a 'what' of 0 (zero), which is the message key used
-        // by AnrTimer internally.
-        TestArg t = new TestArg(1, 1, 3);
-        assertFalse(handler.hasMessages(0));
-        assertTrue(timer.start(t, 100));
-        assertTrue(handler.hasMessages(0));
-        assertTrue(timer.cancel(t));
-        assertFalse(handler.hasMessages(0));
-
-        // Verify that no expiration messages were delivered.
-        assertEquals(0, mMessages.size());
-        assertEquals(0, AnrTimer.sizeOfTimerList());
-    }
-
-    @Test
-    public void testRestart() throws Exception {
-        // Create an non-immediate TestHandler.
-        TestInjector injector = new TestInjector(0, false);
-        TestAnrTimer timer = new TestAnrTimer(false, injector);
-
-        TestArg t = new TestArg(1, 1, 3);
-        assertTrue(timer.start(t, 2500));
-        assertTrue(timer.start(t, 1000));
-
-        // Verify that the test handler saw two timeouts.
-        injector.getHandler().verifyDelays(new long[] { 2500, 1000 });
-
-        // Verify that there is a single timer.  Then cancel it.
-        assertEquals(1, AnrTimer.sizeOfTimerList());
-        assertTrue(timer.cancel(t));
-        assertEquals(0, AnrTimer.sizeOfTimerList());
-    }
-
-    @Test
-    public void testExtendNormal() throws Exception {
-        // Create an immediate TestHandler.
-        TestInjector injector = new TestInjector(5);
-        TestAnrTimer timer = new TestAnrTimer(true, injector);
-        TestArg t = new TestArg(1, 1, 3);
-        assertTrue(timer.start(t, 10));
-
-        assertTrue(mLatch.await(100, TimeUnit.MILLISECONDS));
-        assertEquals(1, mMessages.size());
-        Message m = mMessages.get(0);
-        assertEquals(timer.what(), m.what);
-        assertEquals(t, m.obj);
-
-        // Verify that the test handler saw two timeouts: one of 10ms and one of 5ms.
-        injector.getHandler().verifyDelays(new long[] { 10, 5 });
-
-        // Verify that the timer is still present. Then remove it and verify that the list is
-        // empty.
-        assertEquals(1, AnrTimer.sizeOfTimerList());
-        assertTrue(timer.accept(t));
-        assertEquals(0, AnrTimer.sizeOfTimerList());
-    }
-
-    @Test
-    public void testExtendOversize() throws Exception {
-        // Create an immediate TestHandler.
-        TestInjector injector = new TestInjector(25);
-        TestAnrTimer timer = new TestAnrTimer(true, injector);
-        TestArg t = new TestArg(1, 1, 3);
-        assertTrue(timer.start(t, 10));
-
-        assertTrue(mLatch.await(100, TimeUnit.MILLISECONDS));
-        assertEquals(1, mMessages.size());
-        Message m = mMessages.get(0);
-        assertEquals(timer.what(), m.what);
-        assertEquals(t, m.obj);
-
-        // Verify that the test handler saw two timeouts: one of 10ms and one of 10ms.
-        injector.getHandler().verifyDelays(new long[] { 10, 10 });
-
-        // Verify that the timer is still present. Then remove it and verify that the list is
-        // empty.
-        assertEquals(1, AnrTimer.sizeOfTimerList());
-        assertTrue(timer.accept(t));
-        assertEquals(0, AnrTimer.sizeOfTimerList());
-    }
-}
diff --git a/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java b/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java
new file mode 100644
index 0000000..330dbb8
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.utils;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+
+import android.platform.test.annotations.Presubmit;
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.annotations.GuardedBy;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@SmallTest
+@Presubmit
+public class AnrTimerTest {
+
+    // The commonly used message timeout key.
+    private static final int MSG_TIMEOUT = 1;
+
+    // The test argument includes a pid and uid, and a tag.  The tag is used to distinguish
+    // different message instances.  Additional fields (like what) capture delivery information
+    // that is checked by the test.
+    private static class TestArg {
+        final int pid;
+        final int uid;
+        int what;
+
+        TestArg(int pid, int uid) {
+            this.pid = pid;
+            this.uid = uid;
+            this.what = 0;
+        }
+    }
+
+    /**
+     * The test handler is a self-contained object for a single test.
+     */
+    private static class Helper {
+        final Object mLock = new Object();
+
+        final Handler mHandler;
+        final CountDownLatch mLatch;
+        @GuardedBy("mLock")
+        final ArrayList<TestArg> mMessages;
+
+        Helper(int expect) {
+            mHandler = new Handler(Looper.getMainLooper(), this::expirationHandler);
+            mMessages = new ArrayList<>();
+            mLatch = new CountDownLatch(expect);
+        }
+
+        /**
+         * When a timer expires, the object must be a TestArg.  Update the TestArg with
+         * expiration metadata and save it.
+         */
+        private boolean expirationHandler(Message msg) {
+            synchronized (mLock) {
+                TestArg arg = (TestArg) msg.obj;
+                arg.what = msg.what;
+                mMessages.add(arg);
+                mLatch.countDown();
+                return false;
+            }
+        }
+
+        boolean await(long timeout) throws InterruptedException {
+            // No need to synchronize, as the CountDownLatch is already thread-safe.
+            return mLatch.await(timeout, TimeUnit.MILLISECONDS);
+        }
+
+        /**
+         * Fetch the received messages.  Fail if the count of received messages is other than the
+         * expected count.
+         */
+        TestArg[] messages(int expected) {
+            synchronized (mLock) {
+                assertEquals(expected, mMessages.size());
+                return mMessages.toArray(new TestArg[expected]);
+            }
+        }
+    }
+
+    /**
+     * An instrumented AnrTimer.
+     */
+    private static class TestAnrTimer extends AnrTimer<TestArg> {
+        private TestAnrTimer(Handler h, int key, String tag) {
+            super(h, key, tag);
+        }
+
+        TestAnrTimer(Helper helper) {
+            this(helper.mHandler, MSG_TIMEOUT, caller());
+        }
+
+        void start(TestArg arg, long millis) {
+            start(arg, arg.pid, arg.uid, millis);
+        }
+
+        // Return the name of method that called the constructor, assuming that this function is
+        // called from inside the constructor.  The calling method is used to name the AnrTimer
+        // instance so that logs are easier to understand.
+        private static String caller() {
+            final int n = 4;
+            StackTraceElement[] stack = Thread.currentThread().getStackTrace();
+            if (stack.length < n+1) return "test";
+            return stack[n].getMethodName();
+        }
+    }
+
+    void validate(TestArg expected, TestArg actual) {
+        assertEquals(expected, actual);
+        assertEquals(actual.what, MSG_TIMEOUT);
+    }
+
+
+    /**
+     * Verify that a simple expiration succeeds.  The timer is started for 10ms.  The test
+     * procedure waits 5s for the expiration message, but under correct operation, the test will
+     * only take 10ms
+     */
+    @Test
+    public void testSimpleTimeout() throws Exception {
+        Helper helper = new Helper(1);
+        TestAnrTimer timer = new TestAnrTimer(helper);
+        TestArg t = new TestArg(1, 1);
+        timer.start(t, 10);
+        // Delivery is immediate but occurs on a different thread.
+        assertTrue(helper.await(5000));
+        TestArg[] result = helper.messages(1);
+        validate(t, result[0]);
+    }
+
+    /**
+     * Verify that if three timers are scheduled, they are delivered in time order.
+     */
+    @Test
+    public void testMultipleTimers() throws Exception {
+        // Expect three messages.
+        Helper helper = new Helper(3);
+        TestAnrTimer timer = new TestAnrTimer(helper);
+        TestArg t1 = new TestArg(1, 1);
+        TestArg t2 = new TestArg(1, 2);
+        TestArg t3 = new TestArg(1, 3);
+        timer.start(t1, 50);
+        timer.start(t2, 60);
+        timer.start(t3, 40);
+        // Delivery is immediate but occurs on a different thread.
+        assertTrue(helper.await(5000));
+        TestArg[] result = helper.messages(3);
+        validate(t3, result[0]);
+        validate(t1, result[1]);
+        validate(t2, result[2]);
+    }
+
+    /**
+     * Verify that a canceled timer is not delivered.
+     */
+    @Test
+    public void testCancelTimer() throws Exception {
+        // Expect two messages.
+        Helper helper = new Helper(2);
+        TestAnrTimer timer = new TestAnrTimer(helper);
+        TestArg t1 = new TestArg(1, 1);
+        TestArg t2 = new TestArg(1, 2);
+        TestArg t3 = new TestArg(1, 3);
+        timer.start(t1, 50);
+        timer.start(t2, 60);
+        timer.start(t3, 40);
+        // Briefly pause.
+        assertFalse(helper.await(10));
+        timer.cancel(t1);
+        // Delivery is immediate but occurs on a different thread.
+        assertTrue(helper.await(5000));
+        TestArg[] result = helper.messages(2);
+        validate(t3, result[0]);
+        validate(t2, result[1]);
+    }
+}