Merge "Do not use rootview view binders." into main
diff --git a/Android.bp b/Android.bp
index 13b1703..f6a9328 100644
--- a/Android.bp
+++ b/Android.bp
@@ -220,6 +220,7 @@
         "updatable-driver-protos",
         "ota_metadata_proto_java",
         "android.hidl.base-V1.0-java",
+        "android.hidl.manager-V1.2-java",
         "android.hardware.cas-V1-java", // AIDL
         "android.hardware.cas-V1.0-java",
         "android.hardware.cas-V1.1-java",
diff --git a/core/java/android/os/HwNoService.java b/core/java/android/os/HwNoService.java
index 117c3ad..0840314 100644
--- a/core/java/android/os/HwNoService.java
+++ b/core/java/android/os/HwNoService.java
@@ -16,37 +16,127 @@
 
 package android.os;
 
+import android.hidl.manager.V1_2.IServiceManager;
+import android.util.Log;
+
+import java.util.ArrayList;
+
 /**
  * A fake hwservicemanager that is used locally when HIDL isn't supported on the device.
  *
  * @hide
  */
-final class HwNoService implements IHwBinder, IHwInterface {
-    /** @hide */
-    @Override
-    public void transact(int code, HwParcel request, HwParcel reply, int flags) {}
+final class HwNoService extends IServiceManager.Stub implements IHwBinder, IHwInterface {
+    private static final String TAG = "HwNoService";
 
     /** @hide */
     @Override
-    public IHwInterface queryLocalInterface(String descriptor) {
-        return new HwNoService();
+    public String toString() {
+        return "[HwNoService]";
     }
 
-    /** @hide */
     @Override
-    public boolean linkToDeath(DeathRecipient recipient, long cookie) {
+    public android.hidl.base.V1_0.IBase get(String fqName, String name)
+            throws android.os.RemoteException {
+        Log.i(TAG, "get " + fqName + "/" + name + " with no hwservicemanager");
+        return null;
+    }
+
+    @Override
+    public boolean add(String name, android.hidl.base.V1_0.IBase service)
+            throws android.os.RemoteException {
+        Log.i(TAG, "get " + name + " with no hwservicemanager");
+        return false;
+    }
+
+    @Override
+    public byte getTransport(String fqName, String name) throws android.os.RemoteException {
+        Log.i(TAG, "getTransoport " + fqName + "/" + name + " with no hwservicemanager");
+        return 0x0;
+    }
+
+    @Override
+    public java.util.ArrayList<String> list() throws android.os.RemoteException {
+        Log.i(TAG, "list with no hwservicemanager");
+        return new ArrayList<String>();
+    }
+
+    @Override
+    public java.util.ArrayList<String> listByInterface(String fqName)
+            throws android.os.RemoteException {
+        Log.i(TAG, "listByInterface with no hwservicemanager");
+        return new ArrayList<String>();
+    }
+
+    @Override
+    public boolean registerForNotifications(
+            String fqName, String name, android.hidl.manager.V1_0.IServiceNotification callback)
+            throws android.os.RemoteException {
+        Log.i(TAG, "registerForNotifications with no hwservicemanager");
         return true;
     }
 
-    /** @hide */
     @Override
-    public boolean unlinkToDeath(DeathRecipient recipient) {
+    public ArrayList<android.hidl.manager.V1_0.IServiceManager.InstanceDebugInfo> debugDump()
+            throws android.os.RemoteException {
+        Log.i(TAG, "debugDump with no hwservicemanager");
+        return new ArrayList<android.hidl.manager.V1_0.IServiceManager.InstanceDebugInfo>();
+    }
+
+    @Override
+    public void registerPassthroughClient(String fqName, String name)
+            throws android.os.RemoteException {
+        Log.i(TAG, "registerPassthroughClient with no hwservicemanager");
+    }
+
+    @Override
+    public boolean unregisterForNotifications(
+            String fqName, String name, android.hidl.manager.V1_0.IServiceNotification callback)
+            throws android.os.RemoteException {
+        Log.i(TAG, "unregisterForNotifications with no hwservicemanager");
         return true;
     }
 
-    /** @hide */
     @Override
-    public IHwBinder asBinder() {
-        return this;
+    public boolean registerClientCallback(
+            String fqName,
+            String name,
+            android.hidl.base.V1_0.IBase server,
+            android.hidl.manager.V1_2.IClientCallback cb)
+            throws android.os.RemoteException {
+        Log.i(
+                TAG,
+                "registerClientCallback for " + fqName + "/" + name + " with no hwservicemanager");
+        return true;
+    }
+
+    @Override
+    public boolean unregisterClientCallback(
+            android.hidl.base.V1_0.IBase server, android.hidl.manager.V1_2.IClientCallback cb)
+            throws android.os.RemoteException {
+        Log.i(TAG, "unregisterClientCallback with no hwservicemanager");
+        return true;
+    }
+
+    @Override
+    public boolean addWithChain(
+            String name, android.hidl.base.V1_0.IBase service, java.util.ArrayList<String> chain)
+            throws android.os.RemoteException {
+        Log.i(TAG, "addWithChain with no hwservicemanager");
+        return true;
+    }
+
+    @Override
+    public java.util.ArrayList<String> listManifestByInterface(String fqName)
+            throws android.os.RemoteException {
+        Log.i(TAG, "listManifestByInterface for " + fqName + " with no hwservicemanager");
+        return new ArrayList<String>();
+    }
+
+    @Override
+    public boolean tryUnregister(String fqName, String name, android.hidl.base.V1_0.IBase service)
+            throws android.os.RemoteException {
+        Log.i(TAG, "tryUnregister for " + fqName + "/" + name + " with no hwservicemanager");
+        return true;
     }
 }
diff --git a/core/jni/android_os_HwBinder.cpp b/core/jni/android_os_HwBinder.cpp
index 781895e..477bd09 100644
--- a/core/jni/android_os_HwBinder.cpp
+++ b/core/jni/android_os_HwBinder.cpp
@@ -258,14 +258,59 @@
     JHwBinder::SetNativeContext(env, thiz, context);
 }
 
-static void JHwBinder_native_transact(
-        JNIEnv * /* env */,
-        jobject /* thiz */,
-        jint /* code */,
-        jobject /* requestObj */,
-        jobject /* replyObj */,
-        jint /* flags */) {
-    CHECK(!"Should not be here");
+static void JHwBinder_native_transact(JNIEnv *env, jobject thiz, jint code, jobject requestObj,
+                                      jobject replyObj, jint flags) {
+    if (requestObj == NULL) {
+        jniThrowException(env, "java/lang/NullPointerException", NULL);
+        return;
+    }
+    sp<hardware::IBinder> binder = JHwBinder::GetNativeBinder(env, thiz);
+    sp<android::hidl::base::V1_0::IBase> base = new android::hidl::base::V1_0::BpHwBase(binder);
+    hidl_string desc;
+    auto ret = base->interfaceDescriptor(
+            [&desc](const hidl_string &descriptor) { desc = descriptor; });
+    ret.assertOk();
+    // Only the fake hwservicemanager is allowed to be used locally like this.
+    if (desc != "android.hidl.manager@1.2::IServiceManager" &&
+        desc != "android.hidl.manager@1.1::IServiceManager" &&
+        desc != "android.hidl.manager@1.0::IServiceManager") {
+        LOG(FATAL) << "Local binders are not supported!";
+    }
+    if (replyObj == nullptr) {
+        LOG(FATAL) << "Unexpected null replyObj. code: " << code;
+        return;
+    }
+    const hardware::Parcel *request = JHwParcel::GetNativeContext(env, requestObj)->getParcel();
+    sp<JHwParcel> replyContext = JHwParcel::GetNativeContext(env, replyObj);
+    hardware::Parcel *reply = replyContext->getParcel();
+
+    request->setDataPosition(0);
+
+    bool isOneway = (flags & IBinder::FLAG_ONEWAY) != 0;
+    if (!isOneway) {
+        replyContext->setTransactCallback([](auto &replyParcel) {});
+    }
+
+    env->CallVoidMethod(thiz, gFields.onTransactID, code, requestObj, replyObj, flags);
+
+    if (env->ExceptionCheck()) {
+        jthrowable excep = env->ExceptionOccurred();
+        env->ExceptionDescribe();
+        env->ExceptionClear();
+
+        binder_report_exception(env, excep, "Uncaught error or exception in hwbinder!");
+
+        env->DeleteLocalRef(excep);
+    }
+
+    if (!isOneway) {
+        if (!replyContext->wasSent()) {
+            // The implementation never finished the transaction.
+            LOG(ERROR) << "The reply failed to send!";
+        }
+    }
+
+    reply->setDataPosition(0);
 }
 
 static void JHwBinder_native_registerService(
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index 4741170..eebf8aa 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -38,6 +38,7 @@
 
 cc_aconfig_library {
     name: "hwui_flags_cc_lib",
+    host_supported: true,
     aconfig_declarations: "hwui_flags",
 }
 
@@ -109,12 +110,15 @@
         "libbase",
         "libharfbuzz_ng",
         "libminikin",
+        "server_configurable_flags",
     ],
 
     static_libs: [
         "libui-types",
     ],
 
+    whole_static_libs: ["hwui_flags_cc_lib"],
+
     target: {
         android: {
             shared_libs: [
@@ -146,7 +150,6 @@
                 "libstatspull_lazy",
                 "libstatssocket_lazy",
                 "libtonemap",
-                "hwui_flags_cc_lib",
             ],
         },
         host: {
diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig
index ca11975..c156c46 100644
--- a/libs/hwui/aconfig/hwui_flags.aconfig
+++ b/libs/hwui/aconfig/hwui_flags.aconfig
@@ -15,6 +15,13 @@
 }
 
 flag {
+  name: "high_contrast_text_luminance"
+  namespace: "accessibility"
+  description: "Use luminance to determine how to make text more high contrast, instead of RGB heuristic"
+  bug: "186567103"
+}
+
+flag {
   name: "hdr_10bit_plus"
   namespace: "core_graphics"
   description: "Use 10101010 and FP16 formats for HDR-UI when available"
diff --git a/libs/hwui/hwui/DrawTextFunctor.h b/libs/hwui/hwui/DrawTextFunctor.h
index 2e6e976..8f99990 100644
--- a/libs/hwui/hwui/DrawTextFunctor.h
+++ b/libs/hwui/hwui/DrawTextFunctor.h
@@ -16,7 +16,9 @@
 
 #include <SkFontMetrics.h>
 #include <SkRRect.h>
+#include <com_android_graphics_hwui_flags.h>
 
+#include "../utils/Color.h"
 #include "Canvas.h"
 #include "FeatureFlags.h"
 #include "MinikinUtils.h"
@@ -27,6 +29,8 @@
 #include "hwui/PaintFilter.h"
 #include "pipeline/skia/SkiaRecordingCanvas.h"
 
+namespace flags = com::android::graphics::hwui::flags;
+
 namespace android {
 
 static inline void drawStroke(SkScalar left, SkScalar right, SkScalar top, SkScalar thickness,
@@ -73,8 +77,14 @@
         if (CC_UNLIKELY(canvas->isHighContrastText() && paint.getAlpha() != 0)) {
             // high contrast draw path
             int color = paint.getColor();
-            int channelSum = SkColorGetR(color) + SkColorGetG(color) + SkColorGetB(color);
-            bool darken = channelSum < (128 * 3);
+            bool darken;
+            if (flags::high_contrast_text_luminance()) {
+                uirenderer::Lab lab = uirenderer::sRGBToLab(color);
+                darken = lab.L <= 50;
+            } else {
+                int channelSum = SkColorGetR(color) + SkColorGetG(color) + SkColorGetB(color);
+                darken = channelSum < (128 * 3);
+            }
 
             // outline
             gDrawTextBlobMode = DrawTextBlobMode::HctOutline;
diff --git a/libs/hwui/utils/ForceDark.h b/libs/hwui/utils/ForceDark.h
index 28538c4b..ecfe41f 100644
--- a/libs/hwui/utils/ForceDark.h
+++ b/libs/hwui/utils/ForceDark.h
@@ -17,6 +17,8 @@
 #ifndef FORCEDARKUTILS_H
 #define FORCEDARKUTILS_H
 
+#include <stdint.h>
+
 namespace android {
 namespace uirenderer {
 
@@ -26,9 +28,9 @@
  * This should stay in sync with the java @IntDef in
  * frameworks/base/graphics/java/android/graphics/ForceDarkType.java
  */
-enum class ForceDarkType : __uint8_t { NONE = 0, FORCE_DARK = 1, FORCE_INVERT_COLOR_DARK = 2 };
+enum class ForceDarkType : uint8_t { NONE = 0, FORCE_DARK = 1, FORCE_INVERT_COLOR_DARK = 2 };
 
 } /* namespace uirenderer */
 } /* namespace android */
 
-#endif  // FORCEDARKUTILS_H
\ No newline at end of file
+#endif  // FORCEDARKUTILS_H
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
similarity index 96%
rename from packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
index 477f455..0329794 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -12,20 +12,19 @@
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
- *
  */
 
 package com.android.systemui.keyguard.data.quickaffordance
 
 import android.app.Activity
 import androidx.test.filters.SmallTest
-import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.controls.ControlsServiceInfo
 import com.android.systemui.controls.controller.ControlsController
 import com.android.systemui.controls.dagger.ControlsComponent
 import com.android.systemui.controls.management.ControlsListingController
 import com.android.systemui.controls.ui.ControlsUiController
+import com.android.systemui.res.R
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
@@ -37,17 +36,17 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-import org.junit.runners.Parameterized.Parameter
-import org.junit.runners.Parameterized.Parameters
 import org.mockito.ArgumentCaptor
 import org.mockito.Captor
 import org.mockito.Mock
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
+import platform.test.runner.parameterized.Parameter
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 
 @SmallTest
-@RunWith(Parameterized::class)
+@RunWith(ParameterizedAndroidJunit4::class)
 class HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest : SysuiTestCase() {
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt
index 5df26b3..a6b4320 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt
@@ -28,27 +28,26 @@
 /**
  * Factory to create dialogs for consenting to show app panels for specific apps.
  *
- * [internalDialogFactory] is for facilitating testing.
+ * [dialogFactory] is for facilitating testing.
  */
-class PanelConfirmationDialogFactory(
-    private val internalDialogFactory: (Context) -> SystemUIDialog
+class PanelConfirmationDialogFactory @Inject constructor(
+        private val dialogFactory: SystemUIDialog.Factory
 ) {
-    @Inject constructor() : this({ SystemUIDialog(it) })
 
     /**
      * Creates a dialog to show to the user. [response] will be true if an only if the user responds
      * affirmatively.
      */
     fun createConfirmationDialog(
-        context: Context,
-        appName: CharSequence,
-        response: Consumer<Boolean>
+            context: Context,
+            appName: CharSequence,
+            response: Consumer<Boolean>
     ): Dialog {
         val listener =
             DialogInterface.OnClickListener { _, which ->
                 response.accept(which == DialogInterface.BUTTON_POSITIVE)
             }
-        return internalDialogFactory(context).apply {
+        return dialogFactory.create(context).apply {
             setTitle(this.context.getString(R.string.controls_panel_authorization_title, appName))
             setMessage(this.context.getString(R.string.controls_panel_authorization, appName))
             setCanceledOnTouchOutside(true)
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsDialogsFactory.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsDialogsFactory.kt
index 2ad6014..e42a4a6 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsDialogsFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsDialogsFactory.kt
@@ -25,20 +25,21 @@
 import java.util.function.Consumer
 import javax.inject.Inject
 
-class ControlsDialogsFactory(private val internalDialogFactory: (Context) -> SystemUIDialog) {
+class ControlsDialogsFactory @Inject constructor(
+        private val dialogFactory: SystemUIDialog.Factory
+) {
 
-    @Inject constructor() : this({ SystemUIDialog(it) })
 
     fun createRemoveAppDialog(
-        context: Context,
-        appName: CharSequence,
-        response: Consumer<Boolean>
+            context: Context,
+            appName: CharSequence,
+            response: Consumer<Boolean>
     ): Dialog {
         val listener =
             DialogInterface.OnClickListener { _, which ->
                 response.accept(which == DialogInterface.BUTTON_POSITIVE)
             }
-        return internalDialogFactory(context).apply {
+        return dialogFactory.create(context).apply {
             setTitle(context.getString(R.string.controls_panel_remove_app_authorization, appName))
             setCanceledOnTouchOutside(true)
             setOnCancelListener { response.accept(false) }
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt
index 6cb68ba..89bfd96 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.haptics.slider
 
+import android.view.MotionEvent
 import androidx.annotation.FloatRange
 
 /** Configuration parameters of a [SliderHapticFeedbackProvider] */
@@ -38,6 +39,8 @@
     val numberOfLowTicks: Int = 5,
     /** Maximum velocity allowed for vibration scaling. This is not expected to change. */
     val maxVelocityToScale: Float = 2000f, /* In pixels/sec */
+    /** Axis to use when computing velocity. Must be the same as the slider's axis of movement */
+    val velocityAxis: Int = MotionEvent.AXIS_X,
     /** Vibration scale at the upper bookend of the slider */
     @FloatRange(from = 0.0, to = 1.0) val upperBookendScale: Float = 1f,
     /** Vibration scale at the lower bookend of the slider */
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt
index 9e6245a..6f28ab7 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt
@@ -162,27 +162,33 @@
 
     override fun onLowerBookend() {
         if (!hasVibratedAtLowerBookend) {
-            velocityTracker.computeCurrentVelocity(UNITS_SECOND, config.maxVelocityToScale)
-            vibrateOnEdgeCollision(abs(velocityTracker.xVelocity))
+            vibrateOnEdgeCollision(abs(getTrackedVelocity()))
             hasVibratedAtLowerBookend = true
         }
     }
 
     override fun onUpperBookend() {
         if (!hasVibratedAtUpperBookend) {
-            velocityTracker.computeCurrentVelocity(UNITS_SECOND, config.maxVelocityToScale)
-            vibrateOnEdgeCollision(abs(velocityTracker.xVelocity))
+            vibrateOnEdgeCollision(abs(getTrackedVelocity()))
             hasVibratedAtUpperBookend = true
         }
     }
 
     override fun onProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float) {
-        velocityTracker.computeCurrentVelocity(UNITS_SECOND, config.maxVelocityToScale)
-        vibrateDragTexture(abs(velocityTracker.xVelocity), progress)
+        vibrateDragTexture(abs(getTrackedVelocity()), progress)
         hasVibratedAtUpperBookend = false
         hasVibratedAtLowerBookend = false
     }
 
+    private fun getTrackedVelocity(): Float {
+        velocityTracker.computeCurrentVelocity(UNITS_SECOND, config.maxVelocityToScale)
+        return if (velocityTracker.isAxisSupported(config.velocityAxis)) {
+            velocityTracker.getAxisVelocity(config.velocityAxis)
+        } else {
+            0f
+        }
+    }
+
     override fun onProgressJump(@FloatRange(from = 0.0, to = 1.0) progress: Float) {}
 
     override fun onSelectAndArrow(@FloatRange(from = 0.0, to = 1.0) progress: Float) {}
diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
index a6c6233..7e06f5a 100644
--- a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
+++ b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
@@ -87,6 +87,7 @@
 import java.util.Objects;
 
 import javax.inject.Inject;
+import javax.inject.Provider;
 
 /**
  */
@@ -149,6 +150,7 @@
     public static final String EXTRA_CONFIRM_ONLY = "extra_confirm_only";
 
     private final Context mContext;
+    private final SystemUIDialog.Factory mSystemUIDialogFactory;
     private final NotificationManager mNoMan;
     private final PowerManager mPowerMan;
     private final KeyguardManager mKeyguard;
@@ -186,11 +188,17 @@
     /**
      */
     @Inject
-    public PowerNotificationWarnings(Context context, ActivityStarter activityStarter,
-            BroadcastSender broadcastSender, Lazy<BatteryController> batteryControllerLazy,
-            DialogLaunchAnimator dialogLaunchAnimator, UiEventLogger uiEventLogger,
-            GlobalSettings globalSettings, UserTracker userTracker) {
+    public PowerNotificationWarnings(
+            Context context,
+            ActivityStarter activityStarter,
+            BroadcastSender broadcastSender,
+            Lazy<BatteryController> batteryControllerLazy,
+            DialogLaunchAnimator dialogLaunchAnimator,
+            UiEventLogger uiEventLogger,
+            UserTracker userTracker,
+            SystemUIDialog.Factory systemUIDialogFactory) {
         mContext = context;
+        mSystemUIDialogFactory = systemUIDialogFactory;
         mNoMan = mContext.getSystemService(NotificationManager.class);
         mPowerMan = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
         mKeyguard = mContext.getSystemService(KeyguardManager.class);
@@ -444,7 +452,7 @@
 
     private void showHighTemperatureDialog() {
         if (mHighTempDialog != null) return;
-        final SystemUIDialog d = new SystemUIDialog(mContext);
+        final SystemUIDialog d = mSystemUIDialogFactory.create();
         d.setIconAttribute(android.R.attr.alertDialogIcon);
         d.setTitle(R.string.high_temp_title);
         d.setMessage(R.string.high_temp_dialog_message);
@@ -479,7 +487,7 @@
 
     private void showThermalShutdownDialog() {
         if (mThermalShutdownDialog != null) return;
-        final SystemUIDialog d = new SystemUIDialog(mContext);
+        final SystemUIDialog d = mSystemUIDialogFactory.create();
         d.setIconAttribute(android.R.attr.alertDialogIcon);
         d.setTitle(R.string.thermal_shutdown_title);
         d.setMessage(R.string.thermal_shutdown_dialog_message);
@@ -643,7 +651,7 @@
 
     private void showStartSaverConfirmation(Bundle extras) {
         if (mSaverConfirmation != null || mUseExtraSaverConfirmation) return;
-        final SystemUIDialog d = new SystemUIDialog(mContext);
+        final SystemUIDialog d = mSystemUIDialogFactory.create();
         final boolean confirmOnly = extras.getBoolean(BatterySaverUtils.EXTRA_CONFIRM_TEXT_ONLY);
         final int batterySaverTriggerMode =
                 extras.getInt(BatterySaverUtils.EXTRA_POWER_SAVE_MODE_TRIGGER,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
index 6f35cfb..b5def41 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
@@ -148,7 +148,8 @@
     private val deviceConfigProxy: DeviceConfigProxy,
     private val dialogLaunchAnimator: DialogLaunchAnimator,
     private val broadcastDispatcher: BroadcastDispatcher,
-    private val dumpManager: DumpManager
+    private val dumpManager: DumpManager,
+    private val systemUIDialogFactory: SystemUIDialog.Factory,
 ) : Dumpable, FgsManagerController {
 
     companion object {
@@ -375,7 +376,7 @@
     override fun showDialog(expandable: Expandable?) {
         synchronized(lock) {
             if (dialog == null) {
-                val dialog = SystemUIDialog(context)
+                val dialog = systemUIDialogFactory.create()
                 dialog.setTitle(R.string.fgs_manager_dialog_title)
                 dialog.setMessage(R.string.fgs_manager_dialog_message)
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java
index ccf7afb..c9b0022 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java
@@ -55,6 +55,7 @@
 
     private final DataSaverController mDataSaverController;
     private final DialogLaunchAnimator mDialogLaunchAnimator;
+    private final SystemUIDialog.Factory mSystemUIDialogFactory;
 
     @Inject
     public DataSaverTile(
@@ -68,12 +69,14 @@
             ActivityStarter activityStarter,
             QSLogger qsLogger,
             DataSaverController dataSaverController,
-            DialogLaunchAnimator dialogLaunchAnimator
+            DialogLaunchAnimator dialogLaunchAnimator,
+            SystemUIDialog.Factory systemUIDialogFactory
     ) {
         super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger,
                 statusBarStateController, activityStarter, qsLogger);
         mDataSaverController = dataSaverController;
         mDialogLaunchAnimator = dialogLaunchAnimator;
+        mSystemUIDialogFactory = systemUIDialogFactory;
         mDataSaverController.observe(getLifecycle(), this);
     }
 
@@ -98,7 +101,7 @@
         // Show a dialog to confirm first. Dialogs shown by the DialogLaunchAnimator must be created
         // and shown on the main thread, so we post it to the UI handler.
         mUiHandler.post(() -> {
-            SystemUIDialog dialog = new SystemUIDialog(mContext);
+            SystemUIDialog dialog = mSystemUIDialogFactory.create();
             dialog.setTitle(com.android.internal.R.string.data_saver_enable_title);
             dialog.setMessage(com.android.internal.R.string.data_saver_description);
             dialog.setPositiveButton(com.android.internal.R.string.data_saver_enable_button,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
index acd7510..41cd221 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
@@ -23,7 +23,6 @@
 import android.content.Intent
 import android.provider.Settings
 import android.view.LayoutInflater
-import androidx.annotation.VisibleForTesting
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.res.R
@@ -44,31 +43,15 @@
  * Controller for [UserDialog].
  */
 @SysUISingleton
-class UserSwitchDialogController @VisibleForTesting constructor(
-    private val userDetailViewAdapterProvider: Provider<UserDetailView.Adapter>,
-    private val activityStarter: ActivityStarter,
-    private val falsingManager: FalsingManager,
-    private val dialogLaunchAnimator: DialogLaunchAnimator,
-    private val uiEventLogger: UiEventLogger,
-    private val dialogFactory: (Context) -> SystemUIDialog
+class UserSwitchDialogController @Inject constructor(
+        private val userDetailViewAdapterProvider: Provider<UserDetailView.Adapter>,
+        private val activityStarter: ActivityStarter,
+        private val falsingManager: FalsingManager,
+        private val dialogLaunchAnimator: DialogLaunchAnimator,
+        private val uiEventLogger: UiEventLogger,
+        private val dialogFactory: SystemUIDialog.Factory
 ) {
 
-    @Inject
-    constructor(
-        userDetailViewAdapterProvider: Provider<UserDetailView.Adapter>,
-        activityStarter: ActivityStarter,
-        falsingManager: FalsingManager,
-        dialogLaunchAnimator: DialogLaunchAnimator,
-        uiEventLogger: UiEventLogger
-    ) : this(
-        userDetailViewAdapterProvider,
-        activityStarter,
-        falsingManager,
-        dialogLaunchAnimator,
-        uiEventLogger,
-        { SystemUIDialog(it) }
-    )
-
     companion object {
         private const val INTERACTION_JANK_TAG = "switch_user"
         private val USER_SETTINGS_INTENT = Intent(Settings.ACTION_USER_SETTINGS)
@@ -81,7 +64,7 @@
      * [userDetailViewAdapterProvider] and show it as launched from [expandable].
      */
     fun showDialog(context: Context, expandable: Expandable) {
-        with(dialogFactory(context)) {
+        with(dialogFactory.create()) {
             setShowForAllUsers(true)
             setCanceledOnTouchOutside(true)
 
diff --git a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java
index f071623..9076182 100644
--- a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java
+++ b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java
@@ -21,8 +21,10 @@
 import android.annotation.TestApi;
 import android.content.Context;
 import android.content.res.Configuration;
+import android.content.res.Resources;
 import android.hardware.devicestate.DeviceStateManager;
 import android.hardware.devicestate.DeviceStateManagerGlobal;
+import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup.LayoutParams;
 import android.widget.LinearLayout;
@@ -72,20 +74,27 @@
     private DeviceStateManager.DeviceStateCallback mDeviceStateManagerCallback =
             new DeviceStateManagerCallback();
 
-    private final Context mContext;
     private final CommandQueue mCommandQueue;
     private final Executor mExecutor;
+    private final Resources mResources;
+    private final LayoutInflater mLayoutInflater;
+    private final SystemUIDialog.Factory mSystemUIDialogFactory;
 
-    @VisibleForTesting
-    SystemUIDialog mRearDisplayEducationDialog;
+    private SystemUIDialog mRearDisplayEducationDialog;
     @Nullable LinearLayout mDialogViewContainer;
 
     @Inject
-    public RearDisplayDialogController(Context context, CommandQueue commandQueue,
-            @Main Executor executor) {
-        mContext = context;
+    public RearDisplayDialogController(
+            CommandQueue commandQueue,
+            @Main Executor executor,
+            @Main Resources resources,
+            LayoutInflater layoutInflater,
+            SystemUIDialog.Factory systemUIDialogFactory) {
         mCommandQueue = commandQueue;
         mExecutor = executor;
+        mResources = resources;
+        mLayoutInflater = layoutInflater;
+        mSystemUIDialogFactory = systemUIDialogFactory;
     }
 
     @Override
@@ -104,8 +113,7 @@
         if (mRearDisplayEducationDialog != null && mRearDisplayEducationDialog.isShowing()
                 && mDialogViewContainer != null) {
             // Refresh the dialog view when configuration is changed.
-            Context dialogContext = mRearDisplayEducationDialog.getContext();
-            View dialogView = createDialogView(dialogContext);
+            View dialogView = createDialogView(mRearDisplayEducationDialog.getContext());
             mDialogViewContainer.removeAllViews();
             mDialogViewContainer.addView(dialogView);
         }
@@ -114,9 +122,7 @@
     private void createAndShowDialog() {
         mServiceNotified = false;
         Context dialogContext = mRearDisplayEducationDialog.getContext();
-
         View dialogView = createDialogView(dialogContext);
-
         mDialogViewContainer = new LinearLayout(dialogContext);
         mDialogViewContainer.setLayoutParams(
                 new LinearLayout.LayoutParams(
@@ -133,11 +139,11 @@
 
     private View createDialogView(Context context) {
         View dialogView;
+        LayoutInflater inflater = mLayoutInflater.cloneInContext(context);
         if (mStartedFolded) {
-            dialogView = View.inflate(context,
-                    R.layout.activity_rear_display_education, null);
+            dialogView = inflater.inflate(R.layout.activity_rear_display_education, null);
         } else {
-            dialogView = View.inflate(context,
+            dialogView = inflater.inflate(
                     R.layout.activity_rear_display_education_opened, null);
         }
         LottieAnimationView animationView = dialogView.findViewById(
@@ -172,9 +178,9 @@
      * Ensures we're not using old values from when the dialog may have been shown previously.
      */
     private void initializeValues(int startingBaseState) {
-        mRearDisplayEducationDialog = new SystemUIDialog(mContext);
+        mRearDisplayEducationDialog = mSystemUIDialogFactory.create();
         if (mFoldedStates == null) {
-            mFoldedStates = mContext.getResources().getIntArray(
+            mFoldedStates = mResources.getIntArray(
                     com.android.internal.R.array.config_foldedDeviceStates);
         }
         mStartedFolded = isFoldedState(startingBaseState);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
index af6da3f..3394eac 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
@@ -149,6 +149,14 @@
             return create(new DialogDelegate<>(){}, mContext);
         }
 
+        /** Creates a new instance of {@link SystemUIDialog} with no customized behavior.
+         *
+         * When you just need a dialog created with a specific {@link Context}, call this.
+         */
+        public SystemUIDialog create(Context context) {
+            return create(new DialogDelegate<>(){}, context);
+        }
+
         /**
          * Creates a new instance of {@link SystemUIDialog} with {@code delegate} as the {@link
          * Delegate}.
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
index 8087a87..550a65c 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
@@ -48,6 +48,8 @@
 import com.android.systemui.statusbar.phone.SystemUIDialog;
 import com.android.systemui.util.leak.LeakDetector;
 
+import dagger.Lazy;
+
 import java.util.HashSet;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
@@ -87,6 +89,7 @@
     // Set of all tunables, used for leak detection.
     private final HashSet<Tunable> mTunables = LeakDetector.ENABLED ? new HashSet<>() : null;
     private final Context mContext;
+    private final Lazy<SystemUIDialog.Factory> mSystemUIDialogFactoryLazy;
     private final LeakDetector mLeakDetector;
     private final DemoModeController mDemoModeController;
 
@@ -104,9 +107,11 @@
             @Main Handler mainHandler,
             LeakDetector leakDetector,
             DemoModeController demoModeController,
-            UserTracker userTracker) {
+            UserTracker userTracker,
+            Lazy<SystemUIDialog.Factory> systemUIDialogFactoryLazy) {
         super(context);
         mContext = context;
+        mSystemUIDialogFactoryLazy = systemUIDialogFactoryLazy;
         mContentResolver = mContext.getContentResolver();
         mLeakDetector = leakDetector;
         mDemoModeController = demoModeController;
@@ -301,7 +306,7 @@
 
     @Override
     public void showResetRequest(Runnable onDisabled) {
-        SystemUIDialog dialog = new SystemUIDialog(mContext);
+        SystemUIDialog dialog = mSystemUIDialogFactoryLazy.get().create();
         dialog.setShowForAllUsers(true);
         dialog.setMessage(R.string.remove_from_settings_prompt);
         dialog.setButton(DialogInterface.BUTTON_NEGATIVE, mContext.getString(R.string.cancel),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt
index bfb5485..c525711 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt
@@ -120,7 +120,7 @@
             fontScalingDialogDelegate
         )
 
-        whenever(dialogFactory.create(any())).thenReturn(dialog)
+        whenever(dialogFactory.create(any(), any())).thenReturn(dialog)
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java
index 4022d43..3ff43c6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java
@@ -28,8 +28,6 @@
 
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
-import android.view.LayoutInflater;
-import android.view.View;
 import android.widget.Button;
 import android.widget.TextView;
 
@@ -95,7 +93,7 @@
 
         mFeatureFlags.set(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM, true);
         when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState);
-        when(mSystemUIDialogFactory.create(any())).thenReturn(mDialog);
+        when(mSystemUIDialogFactory.create(any(), any())).thenReturn(mDialog);
 
         mBroadcastDialogDelegate = new BroadcastDialogDelegate(
                 mContext,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/contrast/ContrastDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/contrast/ContrastDialogDelegateTest.kt
index 65f68f9..35ac2ae 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/contrast/ContrastDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/contrast/ContrastDialogDelegateTest.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.model.SysUiState
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.phone.DialogDelegate
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
@@ -69,7 +70,8 @@
         mDependency.injectTestDependency(SysUiState::class.java, sysuiState)
         mDependency.injectMockDependency(DialogLaunchAnimator::class.java)
         whenever(sysuiState.setFlag(any(), any())).thenReturn(sysuiState)
-        whenever(sysuiDialogFactory.create(any())).thenReturn(sysuiDialog)
+        whenever(sysuiDialogFactory.create(any(SystemUIDialog.Delegate::class.java)))
+            .thenReturn(sysuiDialog)
         whenever(sysuiDialog.layoutInflater).thenReturn(LayoutInflater.from(mContext))
 
         whenever(mockUserTracker.userId).thenReturn(context.userId)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt
index 4e8f866..7f0ea9a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt
@@ -17,34 +17,48 @@
 
 package com.android.systemui.controls.management
 
+import android.content.Context
 import android.content.DialogInterface
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
-import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.capture
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
 import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+import org.mockito.Mockito.`when` as whenever
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
 class PanelConfirmationDialogFactoryTest : SysuiTestCase() {
 
+    @Mock private lateinit var mockDialog : SystemUIDialog
+    @Mock private lateinit var mockDialogFactory : SystemUIDialog.Factory
+    private lateinit var factory : PanelConfirmationDialogFactory
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+
+        whenever(mockDialogFactory.create(any(Context::class.java))).thenReturn(mockDialog)
+        whenever(mockDialog.context).thenReturn(mContext)
+        factory = PanelConfirmationDialogFactory(mockDialogFactory)
+    }
+
     @Test
     fun testDialogHasCorrectInfo() {
-        val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) }
-        val factory = PanelConfirmationDialogFactory { mockDialog }
         val appName = "appName"
 
-        factory.createConfirmationDialog(context, appName) {}
+        factory.createConfirmationDialog(mContext, appName) {}
 
         verify(mockDialog).setCanceledOnTouchOutside(true)
         verify(mockDialog)
@@ -55,12 +69,9 @@
 
     @Test
     fun testDialogPositiveButton() {
-        val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) }
-        val factory = PanelConfirmationDialogFactory { mockDialog }
-
         var response: Boolean? = null
 
-        factory.createConfirmationDialog(context, "") { response = it }
+        factory.createConfirmationDialog(mContext,"") { response = it }
 
         val captor: ArgumentCaptor<DialogInterface.OnClickListener> = argumentCaptor()
         verify(mockDialog).setPositiveButton(eq(R.string.controls_dialog_ok), capture(captor))
@@ -72,12 +83,9 @@
 
     @Test
     fun testDialogNeutralButton() {
-        val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) }
-        val factory = PanelConfirmationDialogFactory { mockDialog }
-
         var response: Boolean? = null
 
-        factory.createConfirmationDialog(context, "") { response = it }
+        factory.createConfirmationDialog(mContext, "") { response = it }
 
         val captor: ArgumentCaptor<DialogInterface.OnClickListener> = argumentCaptor()
         verify(mockDialog).setNeutralButton(eq(R.string.cancel), capture(captor))
@@ -89,12 +97,9 @@
 
     @Test
     fun testDialogCancel() {
-        val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) }
-        val factory = PanelConfirmationDialogFactory { mockDialog }
-
         var response: Boolean? = null
 
-        factory.createConfirmationDialog(context, "") { response = it }
+        factory.createConfirmationDialog(mContext, "") { response = it }
 
         val captor: ArgumentCaptor<DialogInterface.OnCancelListener> = argumentCaptor()
         verify(mockDialog).setOnCancelListener(capture(captor))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt
index 8eebcee..38c6a0e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt
@@ -17,17 +17,23 @@
 
 package com.android.systemui.controls.ui
 
+import android.content.Context
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.util.FakeSystemUIDialogController
+import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.any
 import org.mockito.Mockito.eq
 import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
@@ -37,18 +43,24 @@
         const val APP_NAME = "Test App"
     }
 
-    private val fakeDialogController = FakeSystemUIDialogController()
+    @Mock
+    private lateinit var mockDialogFactory : SystemUIDialog.Factory
+
+    private val fakeDialogController = FakeSystemUIDialogController(mContext)
 
     private lateinit var underTest: ControlsDialogsFactory
 
     @Before
     fun setup() {
-        underTest = ControlsDialogsFactory { fakeDialogController.dialog }
+        MockitoAnnotations.initMocks(this)
+        whenever(mockDialogFactory.create(any(Context::class.java)))
+            .thenReturn(fakeDialogController.dialog)
+        underTest = ControlsDialogsFactory(mockDialogFactory)
     }
 
     @Test
     fun testCreatesRemoveAppDialog() {
-        val dialog = underTest.createRemoveAppDialog(context, APP_NAME) {}
+        val dialog = underTest.createRemoveAppDialog(mContext, APP_NAME) {}
 
         verify(dialog)
             .setTitle(
@@ -60,7 +72,7 @@
     @Test
     fun testPositiveClickRemoveAppDialogWorks() {
         var dialogResult: Boolean? = null
-        underTest.createRemoveAppDialog(context, APP_NAME) { dialogResult = it }
+        underTest.createRemoveAppDialog(mContext, APP_NAME) { dialogResult = it }
 
         fakeDialogController.clickPositive()
 
@@ -70,7 +82,7 @@
     @Test
     fun testNeutralClickRemoveAppDialogWorks() {
         var dialogResult: Boolean? = null
-        underTest.createRemoveAppDialog(context, APP_NAME) { dialogResult = it }
+        underTest.createRemoveAppDialog(mContext, APP_NAME) { dialogResult = it }
 
         fakeDialogController.clickNeutral()
 
@@ -80,7 +92,7 @@
     @Test
     fun testCancelRemoveAppDialogWorks() {
         var dialogResult: Boolean? = null
-        underTest.createRemoveAppDialog(context, APP_NAME) { dialogResult = it }
+        underTest.createRemoveAppDialog(mContext, APP_NAME) { dialogResult = it }
 
         fakeDialogController.cancel()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
index 11bd9cb..36ae0c7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
@@ -51,6 +51,7 @@
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.res.R
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.FakeSystemUIDialogController
 import com.android.systemui.util.concurrency.FakeExecutor
@@ -97,9 +98,10 @@
     @Mock lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository
     @Mock lateinit var featureFlags: FeatureFlags
     @Mock lateinit var packageManager: PackageManager
+    @Mock lateinit var systemUIDialogFactory: SystemUIDialog.Factory
 
     private val preferredPanelRepository = FakeSelectedComponentRepository()
-    private val fakeDialogController = FakeSystemUIDialogController()
+    private lateinit var fakeDialogController: FakeSystemUIDialogController
     private val uiExecutor = FakeExecutor(FakeSystemClock())
     private val bgExecutor = FakeExecutor(FakeSystemClock())
 
@@ -114,6 +116,9 @@
     fun setup() {
         MockitoAnnotations.initMocks(this)
 
+        fakeDialogController = FakeSystemUIDialogController(mContext)
+        whenever(systemUIDialogFactory.create(any(Context::class.java)))
+            .thenReturn(fakeDialogController.dialog)
         controlsSettingsRepository = FakeControlsSettingsRepository()
 
         // This way, it won't be cloned every time `LayoutInflater.fromContext` is called, but we
@@ -146,10 +151,7 @@
                 authorizedPanelsRepository,
                 preferredPanelRepository,
                 featureFlags,
-                ControlsDialogsFactory {
-                    isRemoveAppDialogCreated = true
-                    fakeDialogController.dialog
-                },
+                ControlsDialogsFactory(systemUIDialogFactory),
                 dumpManager,
             )
         `when`(userTracker.userId).thenReturn(0)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt
index ab6bc2c..66fdf53 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt
@@ -19,7 +19,6 @@
 import android.os.VibrationAttributes
 import android.os.VibrationEffect
 import android.view.VelocityTracker
-import android.view.animation.AccelerateInterpolator
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -51,8 +50,6 @@
     private val lowTickDuration = 12 // Mocked duration of a low tick
     private val dragTextureThresholdMillis =
         lowTickDuration * config.numberOfLowTicks + config.deltaMillisForDragInterval
-    private val progressInterpolator = AccelerateInterpolator(config.progressInterpolatorFactor)
-    private val velocityInterpolator = AccelerateInterpolator(config.velocityInterpolatorFactor)
     private lateinit var sliderHapticFeedbackProvider: SliderHapticFeedbackProvider
 
     @Before
@@ -60,7 +57,9 @@
         MockitoAnnotations.initMocks(this)
         whenever(vibratorHelper.getPrimitiveDurations(any()))
             .thenReturn(intArrayOf(lowTickDuration))
-        whenever(velocityTracker.xVelocity).thenReturn(config.maxVelocityToScale)
+        whenever(velocityTracker.isAxisSupported(config.velocityAxis)).thenReturn(true)
+        whenever(velocityTracker.getAxisVelocity(config.velocityAxis))
+            .thenReturn(config.maxVelocityToScale)
         sliderHapticFeedbackProvider =
             SliderHapticFeedbackProvider(vibratorHelper, velocityTracker, config, clock)
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
index 6248bb1..1a303b0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
@@ -55,6 +55,7 @@
 import com.android.systemui.broadcast.BroadcastSender;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.settings.UserTracker;
+import com.android.systemui.statusbar.phone.SystemUIDialog;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.util.NotificationChannels;
 import com.android.systemui.util.settings.FakeGlobalSettings;
@@ -77,7 +78,6 @@
     public static final String FORMATTED_45M = "0h 45m";
     public static final String FORMATTED_HOUR = "1h 0m";
     private final NotificationManager mMockNotificationManager = mock(NotificationManager.class);
-    private final GlobalSettings mGlobalSettings = new FakeGlobalSettings();
     private PowerNotificationWarnings mPowerNotificationWarnings;
 
     @Mock
@@ -90,6 +90,10 @@
     private UserTracker mUserTracker;
     @Mock
     private View mView;
+    @Mock
+    private SystemUIDialog.Factory mSystemUIDialogFactory;
+    @Mock
+    private SystemUIDialog mSystemUIDialog;
 
     private BroadcastReceiver mReceiver;
 
@@ -113,9 +117,16 @@
         when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
         when(mUserTracker.getUserHandle()).thenReturn(
                 UserHandle.of(ActivityManager.getCurrentUser()));
-        mPowerNotificationWarnings = new PowerNotificationWarnings(wrapper, starter,
-                broadcastSender, () -> mBatteryController, mDialogLaunchAnimator, mUiEventLogger,
-                mGlobalSettings, mUserTracker);
+        when(mSystemUIDialogFactory.create()).thenReturn(mSystemUIDialog);
+        mPowerNotificationWarnings = new PowerNotificationWarnings(
+                wrapper,
+                starter,
+                broadcastSender,
+                () -> mBatteryController,
+                mDialogLaunchAnimator,
+                mUiEventLogger,
+                mUserTracker,
+                mSystemUIDialogFactory);
         BatteryStateSnapshot snapshot = new BatteryStateSnapshot(100, false, false, 1,
                 BatteryManager.BATTERY_HEALTH_GOOD, 5, 15);
         mPowerNotificationWarnings.updateSnapshot(snapshot);
@@ -251,7 +262,7 @@
 
         verify(mDialogLaunchAnimator, never()).showFromView(any(), any());
 
-        assertThat(mPowerNotificationWarnings.getSaverConfirmationDialog().isShowing()).isTrue();
+        verify(mPowerNotificationWarnings.getSaverConfirmationDialog()).show();
         mPowerNotificationWarnings.getSaverConfirmationDialog().dismiss();
     }
 
@@ -266,7 +277,7 @@
 
         verify(mDialogLaunchAnimator, never()).showFromView(any(), any());
 
-        assertThat(mPowerNotificationWarnings.getSaverConfirmationDialog().isShowing()).isTrue();
+        verify(mPowerNotificationWarnings.getSaverConfirmationDialog()).show();
         mPowerNotificationWarnings.getSaverConfirmationDialog().dismiss();
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java
index f5a3bec..698868d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java
@@ -27,6 +27,7 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.app.IActivityManager;
 import android.app.IForegroundServiceObserver;
@@ -53,6 +54,7 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.settings.UserTracker;
+import com.android.systemui.statusbar.phone.SystemUIDialog;
 import com.android.systemui.util.DeviceConfigProxyFake;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.time.FakeSystemClock;
@@ -95,6 +97,10 @@
     BroadcastDispatcher mBroadcastDispatcher;
     @Mock
     DumpManager mDumpManager;
+    @Mock
+    SystemUIDialog.Factory mSystemUIDialogFactory;
+    @Mock
+    SystemUIDialog mSystemUIDialog;
 
     private FgsManagerController mFmc;
 
@@ -114,6 +120,7 @@
         mSystemClock = new FakeSystemClock();
         mMainExecutor = new FakeExecutor(mSystemClock);
         mBackgroundExecutor = new FakeExecutor(mSystemClock);
+        when(mSystemUIDialogFactory.create()).thenReturn(mSystemUIDialog);
 
         mUserProfiles = new ArrayList<>();
         Mockito.doReturn(mUserProfiles).when(mUserTracker).getUserProfiles();
@@ -325,7 +332,8 @@
                 mDeviceConfigProxyFake,
                 mDialogLaunchAnimator,
                 mBroadcastDispatcher,
-                mDumpManager
+                mDumpManager,
+                mSystemUIDialogFactory
         );
         fmc.init();
         Assert.assertTrue(fmc.getIncludesUserVisibleJobs());
@@ -351,7 +359,8 @@
                 mDeviceConfigProxyFake,
                 mDialogLaunchAnimator,
                 mBroadcastDispatcher,
-                mDumpManager
+                mDumpManager,
+                mSystemUIDialogFactory
         );
         fmc.init();
         Assert.assertFalse(fmc.getIncludesUserVisibleJobs());
@@ -457,7 +466,8 @@
                 mDeviceConfigProxyFake,
                 mDialogLaunchAnimator,
                 mBroadcastDispatcher,
-                mDumpManager
+                mDumpManager,
+                mSystemUIDialogFactory
         );
         result.init();
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt
index 51e95be..c109a1e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt
@@ -32,7 +32,9 @@
 import com.android.systemui.qs.QsEventLogger
 import com.android.systemui.qs.logging.QSLogger
 import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.statusbar.policy.DataSaverController
+import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Before
@@ -49,8 +51,6 @@
 
     @Mock private lateinit var mHost: QSHost
     @Mock private lateinit var mMetricsLogger: MetricsLogger
-    @Mock private lateinit var mStatusBarStateController: StatusBarStateController
-    @Mock private lateinit var mActivityStarter: ActivityStarter
     @Mock private lateinit var mQsLogger: QSLogger
     private val falsingManager = FalsingManagerFake()
     @Mock private lateinit var statusBarStateController: StatusBarStateController
@@ -58,6 +58,8 @@
     @Mock private lateinit var dataSaverController: DataSaverController
     @Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
     @Mock private lateinit var uiEventLogger: QsEventLogger
+    @Mock private lateinit var systemUIDialogFactory: SystemUIDialog.Factory
+    @Mock private lateinit var systemUIDialog: SystemUIDialog
 
     private lateinit var testableLooper: TestableLooper
     private lateinit var tile: DataSaverTile
@@ -67,7 +69,8 @@
         MockitoAnnotations.initMocks(this)
         testableLooper = TestableLooper.get(this)
 
-        Mockito.`when`(mHost.context).thenReturn(mContext)
+        whenever(mHost.context).thenReturn(mContext)
+        whenever(systemUIDialogFactory.create()).thenReturn(systemUIDialog)
 
         tile =
             DataSaverTile(
@@ -81,7 +84,8 @@
                 activityStarter,
                 mQsLogger,
                 dataSaverController,
-                dialogLaunchAnimator
+                dialogLaunchAnimator,
+                systemUIDialogFactory
             )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
index 0a34810..945490f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
@@ -36,6 +36,7 @@
 import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -56,6 +57,8 @@
 class UserSwitchDialogControllerTest : SysuiTestCase() {
 
     @Mock
+    private lateinit var dialogFactory: SystemUIDialog.Factory
+    @Mock
     private lateinit var dialog: SystemUIDialog
     @Mock
     private lateinit var falsingManager: FalsingManager
@@ -80,7 +83,8 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
-        `when`(dialog.context).thenReturn(mContext)
+        whenever(dialog.context).thenReturn(mContext)
+        whenever(dialogFactory.create()).thenReturn(dialog)
 
         controller = UserSwitchDialogController(
             { userDetailViewAdapter },
@@ -88,7 +92,7 @@
             falsingManager,
             dialogLaunchAnimator,
             uiEventLogger,
-            { dialog }
+            dialogFactory
         )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java
index 273ce85..35bf775 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java
@@ -18,25 +18,42 @@
 
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertNotSame;
-import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.content.res.Configuration;
+import android.content.res.Resources;
 import android.hardware.devicestate.DeviceStateManager;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
+import android.view.LayoutInflater;
+import android.view.View;
 import android.widget.TextView;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.res.R;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.model.SysUiState;
+import com.android.systemui.res.R;
 import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.phone.SystemUIDialog;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.time.FakeSystemClock;
 
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
@@ -45,24 +62,49 @@
 
     @Mock
     private CommandQueue mCommandQueue;
+    @Mock
+    private SystemUIDialog.Factory mSystemUIDialogFactory;
+    @Mock
+    private SystemUIDialog mSystemUIDialog;
+    private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
+    @Mock
+    private SysUiState mSysUiState;
+    @Mock
+    private Resources mResources;
 
-    private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
+    LayoutInflater mLayoutInflater = LayoutInflater.from(mContext);
 
+    private final FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
 
     private static final int CLOSED_BASE_STATE = 0;
     private static final int OPEN_BASE_STATE = 1;
 
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+
+        mFeatureFlags.set(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM, true);
+        when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState);
+        when(mSystemUIDialogFactory.create()).thenReturn(mSystemUIDialog);
+        when(mSystemUIDialog.getContext()).thenReturn(mContext);
+    }
     @Test
     public void testClosedDialogIsShown() {
-        RearDisplayDialogController controller = new RearDisplayDialogController(mContext,
-                mCommandQueue, mFakeExecutor);
+        RearDisplayDialogController controller = new RearDisplayDialogController(
+                mCommandQueue,
+                mFakeExecutor,
+                mResources,
+                mLayoutInflater,
+                mSystemUIDialogFactory);
         controller.setDeviceStateManagerCallback(new TestDeviceStateManagerCallback());
         controller.setFoldedStates(new int[]{0});
         controller.setAnimationRepeatCount(0);
 
         controller.showRearDisplayDialog(CLOSED_BASE_STATE);
-        assertTrue(controller.mRearDisplayEducationDialog.isShowing());
-        TextView deviceClosedTitleTextView = controller.mRearDisplayEducationDialog.findViewById(
+        verify(mSystemUIDialog).show();
+
+        View container = getDialogViewContainer();
+        TextView deviceClosedTitleTextView = container.findViewById(
                 R.id.rear_display_title_text_view);
         assertEquals(deviceClosedTitleTextView.getText().toString(),
                 getContext().getResources().getString(
@@ -71,20 +113,28 @@
 
     @Test
     public void testClosedDialogIsRefreshedOnConfigurationChange() {
-        RearDisplayDialogController controller = new RearDisplayDialogController(mContext,
-                mCommandQueue, mFakeExecutor);
+        RearDisplayDialogController controller = new RearDisplayDialogController(
+                mCommandQueue,
+                mFakeExecutor,
+                mResources,
+                mLayoutInflater,
+                mSystemUIDialogFactory);
         controller.setDeviceStateManagerCallback(new TestDeviceStateManagerCallback());
         controller.setFoldedStates(new int[]{0});
         controller.setAnimationRepeatCount(0);
 
         controller.showRearDisplayDialog(CLOSED_BASE_STATE);
-        assertTrue(controller.mRearDisplayEducationDialog.isShowing());
-        TextView deviceClosedTitleTextView = controller.mRearDisplayEducationDialog.findViewById(
+        verify(mSystemUIDialog).show();
+        View container = getDialogViewContainer();
+        TextView deviceClosedTitleTextView = container.findViewById(
                 R.id.rear_display_title_text_view);
 
+        reset(mSystemUIDialog);
+        when(mSystemUIDialog.isShowing()).thenReturn(true);
+        when(mSystemUIDialog.getContext()).thenReturn(mContext);
+
         controller.onConfigChanged(new Configuration());
-        assertTrue(controller.mRearDisplayEducationDialog.isShowing());
-        TextView deviceClosedTitleTextView2 = controller.mRearDisplayEducationDialog.findViewById(
+        TextView deviceClosedTitleTextView2 = container.findViewById(
                 R.id.rear_display_title_text_view);
 
         assertNotSame(deviceClosedTitleTextView, deviceClosedTitleTextView2);
@@ -92,22 +142,33 @@
 
     @Test
     public void testOpenDialogIsShown() {
-        RearDisplayDialogController controller = new RearDisplayDialogController(mContext,
-                mCommandQueue, mFakeExecutor);
+        RearDisplayDialogController controller = new RearDisplayDialogController(
+                mCommandQueue,
+                mFakeExecutor,
+                mResources,
+                mLayoutInflater,
+                mSystemUIDialogFactory);
         controller.setDeviceStateManagerCallback(new TestDeviceStateManagerCallback());
         controller.setFoldedStates(new int[]{0});
         controller.setAnimationRepeatCount(0);
 
         controller.showRearDisplayDialog(OPEN_BASE_STATE);
 
-        assertTrue(controller.mRearDisplayEducationDialog.isShowing());
-        TextView deviceClosedTitleTextView = controller.mRearDisplayEducationDialog.findViewById(
+        verify(mSystemUIDialog).show();
+        View container = getDialogViewContainer();
+        TextView deviceClosedTitleTextView = container.findViewById(
                 R.id.rear_display_title_text_view);
         assertEquals(deviceClosedTitleTextView.getText().toString(),
                 getContext().getResources().getString(
                         R.string.rear_display_unfolded_bottom_sheet_title));
     }
 
+    private View getDialogViewContainer() {
+        ArgumentCaptor<View> viewCaptor = ArgumentCaptor.forClass(View.class);
+        verify(mSystemUIDialog).setView(viewCaptor.capture());
+
+        return viewCaptor.getValue();
+    }
     /**
      * Empty device state manager callbacks, so we can verify that the correct
      * dialogs are being created regardless of device state of the test device.
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSystemUIDialogController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSystemUIDialogController.kt
index 0c9ce0f..697b508 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSystemUIDialogController.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSystemUIDialogController.kt
@@ -17,6 +17,7 @@
 
 package com.android.systemui.util
 
+import android.content.Context
 import android.content.DialogInterface
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.util.mockito.any
@@ -27,13 +28,15 @@
 import org.mockito.Mockito.verify
 import org.mockito.stubbing.Stubber
 
-class FakeSystemUIDialogController {
+class FakeSystemUIDialogController(context: Context) {
 
     val dialog: SystemUIDialog = mock()
 
+
     private val clickListeners: MutableMap<Int, DialogInterface.OnClickListener> = mutableMapOf()
 
     init {
+        whenever(dialog.context).thenReturn(context)
         saveListener(DialogInterface.BUTTON_POSITIVE)
             .whenever(dialog)
             .setPositiveButton(any(), any())
diff --git a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
index 52a8f9e..a6ed846 100644
--- a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
+++ b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
@@ -286,32 +286,33 @@
                 selfManaged, /* notifyOnDeviceNearby */ false, /* revoked */ false,
                 timestamp, Long.MAX_VALUE, /* systemDataSyncFlags */ 0);
 
-        if (deviceProfile != null) {
-            // If the "Device Profile" is specified, make the companion application a holder of the
-            // corresponding role.
-            addRoleHolderForAssociation(mService.getContext(), association, success -> {
-                if (success) {
-                    addAssociationToStore(association, deviceProfile);
-
-                    sendCallbackAndFinish(association, callback, resultReceiver);
-                } else {
-                    Slog.e(TAG, "Failed to add u" + userId + "\\" + packageName
-                            + " to the list of " + deviceProfile + " holders.");
-
-                    sendCallbackAndFinish(null, callback, resultReceiver);
-                }
-            });
-        } else {
-            addAssociationToStore(association, null);
-
-            sendCallbackAndFinish(association, callback, resultReceiver);
-        }
+        // Add role holder for association (if specified) and add new association to store.
+        maybeGrantRoleAndStoreAssociation(association, callback, resultReceiver);
 
         // Don't need to update the mRevokedAssociationsPendingRoleHolderRemoval since
         // maybeRemoveRoleHolderForAssociation in PackageInactivityListener will handle the case
         // that there are other devices with the same profile, so the role holder won't be removed.
     }
 
+    public void maybeGrantRoleAndStoreAssociation(@NonNull AssociationInfo association,
+            @Nullable IAssociationRequestCallback callback,
+            @Nullable ResultReceiver resultReceiver) {
+        // If the "Device Profile" is specified, make the companion application a holder of the
+        // corresponding role.
+        // If it is null, then the operation will succeed without granting any role.
+        addRoleHolderForAssociation(mService.getContext(), association, success -> {
+            if (success) {
+                addAssociationToStore(association);
+                sendCallbackAndFinish(association, callback, resultReceiver);
+            } else {
+                Slog.e(TAG, "Failed to add u" + association.getUserId()
+                        + "\\" + association.getPackageName()
+                        + " to the list of " + association.getDeviceProfile() + " holders.");
+                sendCallbackAndFinish(null, callback, resultReceiver);
+            }
+        });
+    }
+
     public void enableSystemDataSync(int associationId, int flags) {
         AssociationInfo association = mAssociationStore.getAssociationById(associationId);
         AssociationInfo updated = (new AssociationInfo.Builder(association))
@@ -326,15 +327,14 @@
         mAssociationStore.updateAssociation(updated);
     }
 
-    private void addAssociationToStore(@NonNull AssociationInfo association,
-            @Nullable String deviceProfile) {
+    private void addAssociationToStore(@NonNull AssociationInfo association) {
         Slog.i(TAG, "New CDM association created=" + association);
 
         mAssociationStore.addAssociation(association);
 
         mService.updateSpecialAccessPermissionForAssociatedPackage(association);
 
-        logCreateAssociation(deviceProfile);
+        logCreateAssociation(association.getDeviceProfile());
     }
 
     private void sendCallbackAndFinish(@Nullable AssociationInfo association,
diff --git a/services/companion/java/com/android/server/companion/RolesUtils.java b/services/companion/java/com/android/server/companion/RolesUtils.java
index 163f614..af9d2d7 100644
--- a/services/companion/java/com/android/server/companion/RolesUtils.java
+++ b/services/companion/java/com/android/server/companion/RolesUtils.java
@@ -47,6 +47,17 @@
         return roleHolders.contains(packageName);
     }
 
+    /**
+     * Attempt to add the association's companion app as the role holder for the device profile
+     * specified in the association. If the association does not have any device profile specified,
+     * then the operation will always be successful as a no-op.
+     *
+     * @param context
+     * @param associationInfo the association for which the role should be granted to the app
+     * @param roleGrantResult the result callback for adding role holder. True if successful, and
+     *                        false if failed. If the association does not have any device profile
+     *                        specified, then the operation will always be successful as a no-op.
+     */
     static void addRoleHolderForAssociation(
             @NonNull Context context, @NonNull AssociationInfo associationInfo,
             @NonNull Consumer<Boolean> roleGrantResult) {
@@ -55,7 +66,11 @@
         }
 
         final String deviceProfile = associationInfo.getDeviceProfile();
-        if (deviceProfile == null) return;
+        if (deviceProfile == null) {
+            // If no device profile is specified, then no-op and resolve callback with success.
+            roleGrantResult.accept(true);
+            return;
+        }
 
         final RoleManager roleManager = context.getSystemService(RoleManager.class);
 
diff --git a/services/core/java/com/android/server/am/TEST_MAPPING b/services/core/java/com/android/server/am/TEST_MAPPING
index 575db01..e90910a 100644
--- a/services/core/java/com/android/server/am/TEST_MAPPING
+++ b/services/core/java/com/android/server/am/TEST_MAPPING
@@ -146,6 +146,15 @@
         { "include-filter": "android.app.cts.ServiceTest" },
         { "include-filter": "android.app.cts.ActivityManagerFgsBgStartTest" }
       ]
+    },
+    {
+      "name": "CtsStatsdAtomHostTestCases",
+      "options": [
+        { "include-filter": "android.cts.statsdatom.appexit.AppExitHostTest" },
+        { "exclude-annotation": "androidx.test.filters.LargeTest" },
+        { "exclude-annotation": "androidx.test.filters.FlakyTest" },
+        { "exclude-annotation": "org.junit.Ignore" }
+      ]
     }
   ]
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java
index 0bb61415..90da74c 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java
@@ -147,7 +147,7 @@
                 gestureAvailabilityDispatcher, () -> mCurrentUserId, getUserSwitchCallback()));
         mLockoutTracker = new LockoutFrameworkImpl(getContext(),
                 userId -> mLockoutResetDispatcher.notifyLockoutResetCallbacks(
-                        getSensorProperties().sensorId));
+                        getSensorProperties().sensorId), getHandler());
     }
 
     @Override
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java
index 2f77275..0e05a79 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java
@@ -19,6 +19,7 @@
 import static android.Manifest.permission.RESET_FINGERPRINT_LOCKOUT;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.AlarmManager;
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
@@ -81,19 +82,30 @@
             @NonNull LockoutResetCallback lockoutResetCallback) {
         this(context, lockoutResetCallback, (userId) -> PendingIntent.getBroadcast(context, userId,
                 new Intent(ACTION_LOCKOUT_RESET).putExtra(KEY_LOCKOUT_RESET_USER, userId),
-                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE));
+                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE),
+                null /* handler */);
+    }
+
+    public LockoutFrameworkImpl(@NonNull Context context,
+            @NonNull LockoutResetCallback lockoutResetCallback,
+            @NonNull Handler handler) {
+        this(context, lockoutResetCallback, (userId) -> PendingIntent.getBroadcast(context, userId,
+                new Intent(ACTION_LOCKOUT_RESET).putExtra(KEY_LOCKOUT_RESET_USER, userId),
+                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE),
+                handler);
     }
 
     @VisibleForTesting
     LockoutFrameworkImpl(@NonNull Context context,
             @NonNull LockoutResetCallback lockoutResetCallback,
-            @NonNull Function<Integer, PendingIntent> lockoutResetIntent) {
+            @NonNull Function<Integer, PendingIntent> lockoutResetIntent,
+            @Nullable Handler handler) {
         mLockoutResetCallback = lockoutResetCallback;
         mTimedLockoutCleared = new SparseBooleanArray();
         mFailedAttempts = new SparseIntArray();
         mAlarmManager = context.getSystemService(AlarmManager.class);
         mLockoutReceiver = new LockoutReceiver();
-        mHandler = new Handler(Looper.getMainLooper());
+        mHandler = handler == null ? new Handler(Looper.getMainLooper()) : handler;
         mLockoutResetIntent = lockoutResetIntent;
 
         context.registerReceiver(mLockoutReceiver, new IntentFilter(ACTION_LOCKOUT_RESET),
diff --git a/services/core/java/com/android/server/clipboard/ClipboardService.java b/services/core/java/com/android/server/clipboard/ClipboardService.java
index 56a94ec0..49f6070 100644
--- a/services/core/java/com/android/server/clipboard/ClipboardService.java
+++ b/services/core/java/com/android/server/clipboard/ClipboardService.java
@@ -1424,7 +1424,11 @@
         String defaultIme = Settings.Secure.getStringForUser(getContext().getContentResolver(),
                 Settings.Secure.DEFAULT_INPUT_METHOD, userId);
         if (!TextUtils.isEmpty(defaultIme)) {
-            final String imePkg = ComponentName.unflattenFromString(defaultIme).getPackageName();
+            final ComponentName imeComponent = ComponentName.unflattenFromString(defaultIme);
+            if (imeComponent == null) {
+                return false;
+            }
+            final String imePkg = imeComponent.getPackageName();
             return imePkg.equals(packageName);
         }
         return false;
diff --git a/services/core/java/com/android/server/pdb/TEST_MAPPING b/services/core/java/com/android/server/pdb/TEST_MAPPING
index 1aa8601..9e98023 100644
--- a/services/core/java/com/android/server/pdb/TEST_MAPPING
+++ b/services/core/java/com/android/server/pdb/TEST_MAPPING
@@ -1,5 +1,5 @@
 {
-    "postsubmit": [
+    "presubmit": [
         {
             "name": "FrameworksServicesTests",
             "options": [
diff --git a/services/core/java/com/android/server/utils/AnrTimer.java b/services/core/java/com/android/server/utils/AnrTimer.java
index 7b5192c..e3aba0f 100644
--- a/services/core/java/com/android/server/utils/AnrTimer.java
+++ b/services/core/java/com/android/server/utils/AnrTimer.java
@@ -16,21 +16,30 @@
 
 package com.android.server.utils;
 
+import static android.text.TextUtils.formatSimple;
+
 import android.annotation.NonNull;
 import android.os.Handler;
 import android.os.Message;
 import android.os.SystemClock;
 import android.os.Trace;
+import android.text.TextUtils;
 import android.text.format.TimeMigrationUtils;
+import android.util.ArrayMap;
 import android.util.IndentingPrintWriter;
 import android.util.Log;
+import android.util.LongSparseArray;
+import android.util.SparseArray;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.Keep;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.RingBuffer;
 
+import java.lang.ref.WeakReference;
 import java.io.PrintWriter;
 import java.util.Arrays;
+import java.util.ArrayList;
 import java.util.Objects;
 
 /**
@@ -60,9 +69,14 @@
  * is restarted with the extension timeout.  If extensions are disabled or if the extension is zero,
  * the client process is notified of the expiration.
  *
+ * <p>Instances use native resources but not system resources when the feature is enabled.
+ * Instances should be explicitly closed unless they are being closed as part of process
+ * exit. (So, instances in system server generally need not be explicitly closed since they are
+ * created during process start and will last until process exit.)
+ *
  * @hide
  */
-public class AnrTimer<V> {
+public class AnrTimer<V> implements AutoCloseable {
 
     /**
      * The log tag.
@@ -87,6 +101,12 @@
     private static final long TRACE_TAG = Trace.TRACE_TAG_ACTIVITY_MANAGER;
 
     /**
+     * Enable tracing from the time a timer expires until it is accepted or discarded.  This is
+     * used to diagnose long latencies in the client.
+     */
+    private static final boolean ENABLE_TRACING = false;
+
+    /**
      * Return true if the feature is enabled.  By default, the value is take from the Flags class
      * but it can be changed for local testing.
      */
@@ -103,6 +123,9 @@
         }
     }
 
+    /** The default injector. */
+    private static final Injector sDefaultInjector = new Injector();
+
     /**
      * An error is defined by its issue, the operation that detected the error, the tag of the
      * affected service, a short stack of the bad call, and the stringified arg associated with
@@ -160,41 +183,46 @@
     /** A lock for the AnrTimer instance. */
     private final Object mLock = new Object();
 
-    /**
-     * The total number of timers started.
-     */
+    /** The map from client argument to the associated timer ID. */
+    @GuardedBy("mLock")
+    private final ArrayMap<V, Integer> mTimerIdMap = new ArrayMap<>();
+
+    /** Reverse map from timer ID to client argument. */
+    @GuardedBy("mLock")
+    private final SparseArray<V> mTimerArgMap = new SparseArray<>();
+
+    /** The highwater mark of started, but not closed, timers. */
+    @GuardedBy("mLock")
+    private int mMaxStarted = 0;
+
+    /** The total number of timers started. */
     @GuardedBy("mLock")
     private int mTotalStarted = 0;
 
-    /**
-     * The total number of errors detected.
-     */
+    /** The total number of errors detected. */
     @GuardedBy("mLock")
     private int mTotalErrors = 0;
 
-    /**
-     * The handler for messages sent from this instance.
-     */
+    /** The total number of timers that have expired. */
+    @GuardedBy("mLock")
+    private int mTotalExpired = 0;
+
+    /** The handler for messages sent from this instance. */
     private final Handler mHandler;
 
-    /**
-     * The message type for messages sent from this interface.
-     */
+    /** The message type for messages sent from this interface. */
     private final int mWhat;
 
-    /**
-     * A label that identifies the AnrTimer associated with a Timer in log messages.
-     */
+    /** A label that identifies the AnrTimer associated with a Timer in log messages. */
     private final String mLabel;
 
-    /**
-     * Whether this timer instance supports extending timeouts.
-     */
+    /** Whether this timer instance supports extending timeouts. */
     private final boolean mExtend;
 
-    /**
-     * The top-level switch for the feature enabled or disabled.
-     */
+    /** The injector used to create this instance.  This is only used for testing. */
+    private final Injector mInjector;
+
+    /** The top-level switch for the feature enabled or disabled. */
     private final FeatureSwitch mFeature;
 
     /**
@@ -223,7 +251,27 @@
         mWhat = what;
         mLabel = label;
         mExtend = extend;
-        mFeature = new FeatureDisabled();
+        mInjector = injector;
+        boolean enabled = mInjector.anrTimerServiceEnabled() && nativeTimersSupported();
+        mFeature = createFeatureSwitch(enabled);
+    }
+
+    // Return the correct feature.  FeatureEnabled is returned if and only if the feature is
+    // flag-enabled and if the native shadow was successfully created.  Otherwise, FeatureDisabled
+    // is returned.
+    private FeatureSwitch createFeatureSwitch(boolean enabled) {
+        if (!enabled) {
+            return new FeatureDisabled();
+        } else {
+            try {
+                return new FeatureEnabled();
+            } catch (RuntimeException e) {
+                // Something went wrong in the native layer.  Log the error and fall back on the
+                // feature-disabled logic.
+                Log.e(TAG, e.toString());
+                return new FeatureDisabled();
+            }
+        }
     }
 
     /**
@@ -245,7 +293,7 @@
      * @param extend A flag to indicate if expired timers can be granted extensions.
      */
     public AnrTimer(@NonNull Handler handler, int what, @NonNull String label, boolean extend) {
-        this(handler, what, label, extend, new Injector());
+        this(handler, what, label, extend, sDefaultInjector);
     }
 
     /**
@@ -272,19 +320,44 @@
     }
 
     /**
+     * Start a trace on the timer.  The trace is laid down in the AnrTimerTrack.
+     */
+    private void traceBegin(int timerId, int pid, int uid, String what) {
+        if (ENABLE_TRACING) {
+            final String label = formatSimple("%s(%d,%d,%s)", what, pid, uid, mLabel);
+            final int cookie = timerId;
+            Trace.asyncTraceForTrackBegin(TRACE_TAG, TRACK, label, cookie);
+        }
+    }
+
+    /**
+     * End a trace on the timer.
+     */
+    private void traceEnd(int timerId) {
+        if (ENABLE_TRACING) {
+            final int cookie = timerId;
+            Trace.asyncTraceForTrackEnd(TRACE_TAG, TRACK, cookie);
+        }
+    }
+
+    /**
      * The FeatureSwitch class provides a quick switch between feature-enabled behavior and
      * feature-disabled behavior.
      */
     private abstract class FeatureSwitch {
         abstract void start(@NonNull V arg, int pid, int uid, long timeoutMs);
 
-        abstract void cancel(@NonNull V arg);
+        abstract boolean cancel(@NonNull V arg);
 
-        abstract void accept(@NonNull V arg);
+        abstract boolean accept(@NonNull V arg);
 
-        abstract void discard(@NonNull V arg);
+        abstract boolean discard(@NonNull V arg);
 
         abstract boolean enabled();
+
+        abstract void dump(PrintWriter pw, boolean verbose);
+
+        abstract void close();
     }
 
     /**
@@ -301,18 +374,21 @@
 
         /** Cancel a timer by removing the message from the client's handler. */
         @Override
-        void cancel(@NonNull V arg) {
+        boolean cancel(@NonNull V arg) {
             mHandler.removeMessages(mWhat, arg);
+            return true;
         }
 
         /** accept() is a no-op when the feature is disabled. */
         @Override
-        void accept(@NonNull V arg) {
+        boolean accept(@NonNull V arg) {
+            return true;
         }
 
         /** discard() is a no-op when the feature is disabled. */
         @Override
-        void discard(@NonNull V arg) {
+        boolean discard(@NonNull V arg) {
+            return true;
         }
 
         /** The feature is not enabled. */
@@ -320,12 +396,179 @@
         boolean enabled() {
             return false;
         }
+
+        /** dump() is a no-op when the feature is disabled. */
+        @Override
+        void dump(PrintWriter pw, boolean verbose) {
+        }
+
+        /** close() is a no-op when the feature is disabled. */
+        @Override
+        void close() {
+        }
+    }
+
+    /**
+     * A static list of AnrTimer instances.  The list is traversed by dumpsys.  Only instances
+     * using native resources are included.
+     */
+    @GuardedBy("sAnrTimerList")
+    private static final LongSparseArray<WeakReference<AnrTimer>> sAnrTimerList =
+        new LongSparseArray<>();
+
+    /**
+     * The FeatureEnabled class enables the AnrTimer logic.  It is used when the AnrTimer service
+     * is enabled via Flags.anrTimerServiceEnabled.
+     */
+    private class FeatureEnabled extends FeatureSwitch {
+
+        /**
+         * The native timer that supports this instance. The value is set to non-zero when the
+         * native timer is created and it is set back to zero when the native timer is freed.
+         */
+        private long mNative = 0;
+
+        /** Fetch the native tag (an integer) for the given label. */
+        FeatureEnabled() {
+            mNative = nativeAnrTimerCreate(mLabel);
+            if (mNative == 0) throw new IllegalArgumentException("unable to create native timer");
+            synchronized (sAnrTimerList) {
+                sAnrTimerList.put(mNative, new WeakReference(AnrTimer.this));
+            }
+        }
+
+        /**
+         * Start a timer.
+         */
+        @Override
+        void start(@NonNull V arg, int pid, int uid, long timeoutMs) {
+            synchronized (mLock) {
+                if (mTimerIdMap.containsKey(arg)) {
+                    // There is an existing timer.  Cancel it.
+                    cancel(arg);
+                }
+                int timerId = nativeAnrTimerStart(mNative, pid, uid, timeoutMs, mExtend);
+                if (timerId > 0) {
+                    mTimerIdMap.put(arg, timerId);
+                    mTimerArgMap.put(timerId, arg);
+                    mTotalStarted++;
+                    mMaxStarted = Math.max(mMaxStarted, mTimerIdMap.size());
+                } else {
+                    throw new RuntimeException("unable to start timer");
+                }
+            }
+        }
+
+        /**
+         * Cancel a timer.  No error is reported if the timer is not found because some clients
+         * cancel timers from common code that runs even if a timer was never started.
+         */
+        @Override
+        boolean cancel(@NonNull V arg) {
+            synchronized (mLock) {
+                Integer timer = removeLocked(arg);
+                if (timer == null) {
+                    return false;
+                }
+                if (!nativeAnrTimerCancel(mNative, timer)) {
+                    // There may be an expiration message in flight.  Cancel it.
+                    mHandler.removeMessages(mWhat, arg);
+                    return false;
+                }
+                return true;
+            }
+        }
+
+        /**
+         * Accept a timer in the framework-level handler.  The timeout has been accepted and the
+         * timeout handler is executing.
+         */
+        @Override
+        boolean accept(@NonNull V arg) {
+            synchronized (mLock) {
+                Integer timer = removeLocked(arg);
+                if (timer == null) {
+                    notFoundLocked("accept", arg);
+                    return false;
+                }
+                nativeAnrTimerAccept(mNative, timer);
+                traceEnd(timer);
+                return true;
+            }
+        }
+
+        /**
+         * Discard a timer in the framework-level handler.  For whatever reason, the timer is no
+         * longer interesting.  No statistics are collected.  Return false if the time was not
+         * found.
+         */
+        @Override
+        boolean discard(@NonNull V arg) {
+            synchronized (mLock) {
+                Integer timer = removeLocked(arg);
+                if (timer == null) {
+                    notFoundLocked("discard", arg);
+                    return false;
+                }
+                nativeAnrTimerDiscard(mNative, timer);
+                traceEnd(timer);
+                return true;
+            }
+        }
+
+        /** The feature is enabled. */
+        @Override
+        boolean enabled() {
+            return true;
+        }
+
+        /** Dump statistics from the native layer. */
+        @Override
+        void dump(PrintWriter pw, boolean verbose) {
+            synchronized (mLock) {
+                if (mNative != 0) {
+                    nativeAnrTimerDump(mNative, verbose);
+                } else {
+                    pw.println("closed");
+                }
+            }
+        }
+
+        /** Free native resources. */
+        @Override
+        void close() {
+            // Remove self from the list of active timers.
+            synchronized (sAnrTimerList) {
+                sAnrTimerList.remove(mNative);
+            }
+            synchronized (mLock) {
+                if (mNative != 0) nativeAnrTimerClose(mNative);
+                mNative = 0;
+            }
+        }
+
+        /**
+         * Delete the entries associated with arg from the maps and return the ID of the timer, if
+         * any.
+         */
+        @GuardedBy("mLock")
+        private Integer removeLocked(V arg) {
+            Integer r = mTimerIdMap.remove(arg);
+            if (r != null) {
+                synchronized (mTimerArgMap) {
+                    mTimerArgMap.remove(r);
+                }
+            }
+            return r;
+        }
     }
 
     /**
      * Start a timer associated with arg.  The same object must be used to cancel, accept, or
      * discard a timer later.  If a timer already exists with the same arg, then the existing timer
-     * is canceled and a new timer is created.
+     * is canceled and a new timer is created.  The timeout is signed but negative delays are
+     * nonsensical.  Rather than throw an exception, timeouts less than 0ms are forced to 0ms.  This
+     * allows a client to deliver an immediate timeout via the AnrTimer.
      *
      * @param arg The key by which the timer is known.  This is never examined or modified.
      * @param pid The Linux process ID of the target being timed.
@@ -333,25 +576,39 @@
      * @param timeoutMs The timer timeout, in milliseconds.
      */
     public void start(@NonNull V arg, int pid, int uid, long timeoutMs) {
+        if (timeoutMs < 0) timeoutMs = 0;
         mFeature.start(arg, pid, uid, timeoutMs);
     }
 
     /**
      * Cancel the running timer associated with arg.  The timer is forgotten.  If the timer has
-     * expired, the call is treated as a discard.  No errors are reported if the timer does not
-     * exist or if the timer has expired.
+     * expired, the call is treated as a discard.  The function returns true if a running timer was
+     * found, and false if an expired timer was found or if no timer was found.  After this call,
+     * the timer does not exist.
+     *
+     * Note: the return value is always true if the feature is not enabled.
+     *
+     * @param arg The key by which the timer is known.  This is never examined or modified.
+     * @return True if a running timer was canceled.
      */
-    public void cancel(@NonNull V arg) {
-        mFeature.cancel(arg);
+    public boolean cancel(@NonNull V arg) {
+        return mFeature.cancel(arg);
     }
 
     /**
      * Accept the expired timer associated with arg.  This indicates that the caller considers the
-     * timer expiration to be a true ANR.  (See {@link #discard} for an alternate response.)  It is
-     * an error to accept a running timer, however the running timer will be canceled.
+     * timer expiration to be a true ANR.  (See {@link #discard} for an alternate response.)  The
+     * function returns true if an expired timer was found and false if a running timer was found or
+     * if no timer was found.  After this call, the timer does not exist.  It is an error to accept
+     * a running timer, however, the running timer will be canceled.
+     *
+     * Note: the return value is always true if the feature is not enabled.
+     *
+     * @param arg The key by which the timer is known.  This is never examined or modified.
+     * @return True if an expired timer was accepted.
      */
-    public void accept(@NonNull V arg) {
-        mFeature.accept(arg);
+    public boolean accept(@NonNull V arg) {
+        return mFeature.accept(arg);
     }
 
     /**
@@ -359,11 +616,57 @@
      * timer expiration to be a false ANR.  ((See {@link #accept} for an alternate response.)  One
      * reason to discard an expired timer is if the process being timed was also being debugged:
      * such a process could be stopped at a breakpoint and its failure to respond would not be an
-     * error.  It is an error to discard a running timer, however the running timer will be
-     * canceled.
+     * error.  After this call thie timer does not exist. It is an error to discard a running timer,
+     * however the running timer will be canceled.
+     *
+     * Note: the return value is always true if the feature is not enabled.
+     *
+     * @param arg The key by which the timer is known.  This is never examined or modified.
+     * @return True if an expired timer was discarded.
      */
-    public void discard(@NonNull V arg) {
-        mFeature.discard(arg);
+    public boolean discard(@NonNull V arg) {
+        return mFeature.discard(arg);
+    }
+
+    /**
+     * The notifier that a timer has fired.  The timerId and original pid/uid are supplied.  This
+     * method is called from native code.  This method takes mLock so that a timer cannot expire
+     * in the middle of another operation (like start or cancel).
+     */
+    @Keep
+    private boolean expire(int timerId, int pid, int uid) {
+        traceBegin(timerId, pid, uid, "expired");
+        V arg = null;
+        synchronized (mLock) {
+            arg = mTimerArgMap.get(timerId);
+            if (arg == null) {
+                Log.e(TAG, formatSimple("failed to expire timer %s:%d : arg not found",
+                                mLabel, timerId));
+                mTotalErrors++;
+                return false;
+            }
+            mTotalExpired++;
+        }
+        mHandler.sendMessage(Message.obtain(mHandler, mWhat, arg));
+        return true;
+    }
+
+    /**
+     * Close the object and free any native resources.
+     */
+    public void close() {
+        mFeature.close();
+    }
+
+    /**
+     * Ensure any native resources are freed when the object is GC'ed.  Best practice is to close
+     * the object explicitly, but overriding finalize() avoids accidental leaks.
+     */
+    @SuppressWarnings("Finalize")
+    @Override
+    protected void finalize() throws Throwable {
+        close();
+        super.finalize();
     }
 
     /**
@@ -373,8 +676,11 @@
         synchronized (mLock) {
             pw.format("timer: %s\n", mLabel);
             pw.increaseIndent();
-            pw.format("started=%d errors=%d\n", mTotalStarted, mTotalErrors);
+            pw.format("started=%d maxStarted=%d running=%d expired=%d errors=%d\n",
+                    mTotalStarted, mMaxStarted, mTimerIdMap.size(),
+                    mTotalExpired, mTotalErrors);
             pw.decreaseIndent();
+            mFeature.dump(pw, false);
         }
     }
 
@@ -386,6 +692,13 @@
     }
 
     /**
+     * The current time in milliseconds.
+     */
+    private static long now() {
+        return SystemClock.uptimeMillis();
+    }
+
+    /**
      * Dump all errors to the output stream.
      */
     private static void dumpErrors(IndentingPrintWriter ipw) {
@@ -422,23 +735,89 @@
         mTotalErrors++;
     }
 
-    /**
-     * Log an error about  a timer not found.
-     */
+    /** Record an error about a timer not found. */
     @GuardedBy("mLock")
     private void notFoundLocked(String operation, Object arg) {
         recordErrorLocked(operation, "notFound", arg);
     }
 
-    /**
-     * Dumpsys output.
-     */
-    public static void dump(@NonNull PrintWriter pw, boolean verbose) {
+    /** Dumpsys output, allowing for overrides. */
+    @VisibleForTesting
+    static void dump(@NonNull PrintWriter pw, boolean verbose, @NonNull Injector injector) {
+        if (!injector.anrTimerServiceEnabled()) return;
+
         final IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
         ipw.println("AnrTimer statistics");
         ipw.increaseIndent();
+        synchronized (sAnrTimerList) {
+            final int size = sAnrTimerList.size();
+            ipw.println("reporting " + size + " timers");
+            for (int i = 0; i < size; i++) {
+                AnrTimer a = sAnrTimerList.valueAt(i).get();
+                if (a != null) a.dump(ipw);
+            }
+        }
         if (verbose) dumpErrors(ipw);
         ipw.format("AnrTimerEnd\n");
         ipw.decreaseIndent();
     }
+
+    /** Dumpsys output.  There is no output if the feature is not enabled. */
+    public static void dump(@NonNull PrintWriter pw, boolean verbose) {
+        dump(pw, verbose, sDefaultInjector);
+    }
+
+    /**
+     * Return true if the native timers are supported.  Native timers are supported if the method
+     * nativeAnrTimerSupported() can be executed and it returns true.
+     */
+    private static boolean nativeTimersSupported() {
+        try {
+            return nativeAnrTimerSupported();
+        } catch (java.lang.UnsatisfiedLinkError e) {
+            return false;
+        }
+    }
+
+    /**
+     * Native methods
+     */
+
+    /** Return true if the native AnrTimer code is operational. */
+    private static native boolean nativeAnrTimerSupported();
+
+    /**
+     * Create a new native timer with the given key and name.  The key is not used by the native
+     * code but it is returned to the Java layer in the expiration handler.  The name is only for
+     * logging.  Unlike the other methods, this is an instance method: the "this" parameter is
+     * passed into the native layer.
+     */
+    private native long nativeAnrTimerCreate(String name);
+
+    /** Release the native resources.  No further operations are premitted. */
+    private static native int nativeAnrTimerClose(long service);
+
+    /** Start a timer and return its ID.  Zero is returned on error. */
+    private static native int nativeAnrTimerStart(long service, int pid, int uid, long timeoutMs,
+            boolean extend);
+
+    /**
+     * Cancel a timer by ID.  Return true if the timer was running and canceled.  Return false if
+     * the timer was not found or if the timer had already expired.
+     */
+    private static native boolean nativeAnrTimerCancel(long service, int timerId);
+
+    /** Accept an expired timer by ID.  Return true if the timer was found. */
+    private static native boolean nativeAnrTimerAccept(long service, int timerId);
+
+    /** Discard an expired timer by ID.  Return true if the timer was found.  */
+    private static native boolean nativeAnrTimerDiscard(long service, int timerId);
+
+    /** Prod the native library to log a few statistics. */
+    private static native void nativeAnrTimerDump(long service, boolean verbose);
+
+    // This is not a native method but it is a native interface, in the sense that it is called from
+    // the native layer to report timer expiration.  The function must return true if the expiration
+    // message is delivered to the upper layers and false if it could not be delivered.
+    // private boolean expire(int timerId, int pid, int uid);
 }
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index b19f3d8..dfa9dce 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -79,6 +79,7 @@
         ":lib_cachedAppOptimizer_native",
         ":lib_gameManagerService_native",
         ":lib_oomConnection_native",
+        ":lib_anrTimer_native",
     ],
 
     include_dirs: [
@@ -246,3 +247,10 @@
     name: "lib_oomConnection_native",
     srcs: ["com_android_server_am_OomConnection.cpp"],
 }
+
+filegroup {
+    name: "lib_anrTimer_native",
+    srcs: [
+        "com_android_server_utils_AnrTimer.cpp",
+    ],
+}
diff --git a/services/core/jni/com_android_server_utils_AnrTimer.cpp b/services/core/jni/com_android_server_utils_AnrTimer.cpp
new file mode 100644
index 0000000..97b18fa
--- /dev/null
+++ b/services/core/jni/com_android_server_utils_AnrTimer.cpp
@@ -0,0 +1,918 @@
+/*
+ * 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.
+ */
+
+#include <time.h>
+#include <pthread.h>
+#include <sys/timerfd.h>
+#include <inttypes.h>
+
+#include <algorithm>
+#include <list>
+#include <memory>
+#include <set>
+#include <string>
+#include <vector>
+
+#define LOG_TAG "AnrTimerService"
+
+#include <jni.h>
+#include <nativehelper/JNIHelp.h>
+#include "android_runtime/AndroidRuntime.h"
+#include "core_jni_helpers.h"
+
+#include <utils/Mutex.h>
+#include <utils/Timers.h>
+
+#include <utils/Log.h>
+#include <utils/Timers.h>
+#include <android-base/logging.h>
+#include <android-base/stringprintf.h>
+#include <android-base/unique_fd.h>
+
+using ::android::base::StringPrintf;
+
+
+// Native support is unavailable on WIN32 platforms.  This macro preemptively disables it.
+#ifdef _WIN32
+#define NATIVE_SUPPORT 0
+#else
+#define NATIVE_SUPPORT 1
+#endif
+
+namespace android {
+
+// using namespace android;
+
+// Almost nothing in this module needs to be in the android namespace.
+namespace {
+
+// If not on a Posix system, create stub timerfd methods.  These are defined to allow
+// compilation.  They are not functional.  Also, they do not leak outside this compilation unit.
+#ifdef _WIN32
+int timer_create() {
+  return -1;
+}
+int timer_settime(int, int, void const *, void *) {
+  return -1;
+}
+#else
+int timer_create() {
+  return timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC);
+}
+int timer_settime(int fd, int flags, const struct itimerspec *new_value,
+                  struct itimerspec *_Nullable old_value) {
+  return timerfd_settime(fd, flags, new_value, old_value);
+}
+#endif
+
+// A local debug flag that gates a set of log messages for debug only.  This is normally const
+// false so the debug statements are not included in the image.  The flag can be set true in a
+// unit test image to debug test failures.
+const bool DEBUG = false;
+
+// Return the current time in nanoseconds.  This time is relative to system boot.
+nsecs_t now() {
+    return systemTime(SYSTEM_TIME_MONOTONIC);
+}
+
+/**
+ * This class encapsulates the anr timer service.  The service manages a list of individual
+ * timers.  A timer is either Running or Expired.  Once started, a timer may be canceled or
+ * accepted.  Both actions collect statistics about the timer and then delete it.  An expired
+ * timer may also be discarded, which deletes the timer without collecting any statistics.
+ *
+ * All public methods in this class are thread-safe.
+ */
+class AnrTimerService {
+  private:
+    class ProcessStats;
+    class Timer;
+
+  public:
+
+    // The class that actually runs the clock.
+    class Ticker;
+
+    // A timer is identified by a timer_id_t.  Timer IDs are unique in the moment.
+    using timer_id_t = uint32_t;
+
+    // A manifest constant.  No timer is ever created with this ID.
+    static const timer_id_t NOTIMER = 0;
+
+    // A notifier is called with a timer ID, the timer's tag, and the client's cookie.  The pid
+    // and uid that were originally assigned to the timer are passed as well.
+    using notifier_t = bool (*)(timer_id_t, int pid, int uid, void* cookie, jweak object);
+
+    enum Status {
+        Invalid,
+        Running,
+        Expired,
+        Canceled
+    };
+
+    /**
+     * Create a timer service.  The service is initialized with a name used for logging.  The
+     * constructor is also given the notifier callback, and two cookies for the callback: the
+     * traditional void* and an int.
+     */
+    AnrTimerService(char const* label, notifier_t notifier, void* cookie, jweak jtimer, Ticker*);
+
+    // Delete the service and clean up memory.
+    ~AnrTimerService();
+
+    // Start a timer and return the associated timer ID.  It does not matter if the same pid/uid
+    // are already in the running list.  Once start() is called, one of cancel(), accept(), or
+    // discard() must be called to clean up the internal data structures.
+    timer_id_t start(int pid, int uid, nsecs_t timeout, bool extend);
+
+    // Cancel a timer and remove it from all lists.  This is called when the event being timed
+    // has occurred.  If the timer was Running, the function returns true.  The other
+    // possibilities are that the timer was Expired or non-existent; in both cases, the function
+    // returns false.
+    bool cancel(timer_id_t timerId);
+
+    // Accept a timer and remove it from all lists.  This is called when the upper layers accept
+    // that a timer has expired.  If the timer was Expired, the function returns true.  The
+    // other possibilities are tha the timer was Running or non-existing; in both cases, the
+    // function returns false.
+    bool accept(timer_id_t timerId);
+
+    // Discard a timer without collecting any statistics.  This is called when the upper layers
+    // recognize that a timer expired but decide the expiration is not significant.  If the
+    // timer was Expired, the function returns true.  The other possibilities are tha the timer
+    // was Running or non-existing; in both cases, the function returns false.
+    bool discard(timer_id_t timerId);
+
+    // A timer has expired.
+    void expire(timer_id_t);
+
+    // Dump a small amount of state to the log file.
+    void dump(bool verbose) const;
+
+    // Return the Java object associated with this instance.
+    jweak jtimer() const {
+        return notifierObject_;
+    }
+
+  private:
+    // The service cannot be copied.
+    AnrTimerService(AnrTimerService const &) = delete;
+
+    // Insert a timer into the running list.  The lock must be held by the caller.
+    void insert(const Timer&);
+
+    // Remove a timer from the lists and return it. The lock must be held by the caller.
+    Timer remove(timer_id_t timerId);
+
+    // Return a string representation of a status value.
+    static char const *statusString(Status);
+
+    // The name of this service, for logging.
+    std::string const label_;
+
+    // The callback that is invoked when a timer expires.
+    notifier_t const notifier_;
+
+    // The two cookies passed to the notifier.
+    void* notifierCookie_;
+    jweak notifierObject_;
+
+    // The global lock
+    mutable Mutex lock_;
+
+    // The list of all timers that are still running.  This is sorted by ID for fast lookup.
+    std::set<Timer> running_;
+
+    // The maximum number of active timers.
+    size_t maxActive_;
+
+    // Simple counters
+    struct Counters {
+        // The number of timers started, canceled, accepted, discarded, and expired.
+        size_t started;
+        size_t canceled;
+        size_t accepted;
+        size_t discarded;
+        size_t expired;
+
+        // The number of times there were zero active timers.
+        size_t drained;
+
+        // The number of times a protocol error was seen.
+        size_t error;
+    };
+
+    Counters counters_;
+
+    // The clock used by this AnrTimerService.
+    Ticker *ticker_;
+};
+
+class AnrTimerService::ProcessStats {
+  public:
+    nsecs_t cpu_time;
+    nsecs_t cpu_delay;
+
+    ProcessStats() :
+            cpu_time(0),
+            cpu_delay(0) {
+    }
+
+    // Collect all statistics for a process.  Return true if the fill succeeded and false if it
+    // did not.  If there is any problem, the statistics are zeroed.
+    bool fill(int pid) {
+        cpu_time = 0;
+        cpu_delay = 0;
+
+        char path[PATH_MAX];
+        snprintf(path, sizeof(path), "/proc/%u/schedstat", pid);
+        ::android::base::unique_fd fd(open(path, O_RDONLY | O_CLOEXEC));
+        if (!fd.ok()) {
+            return false;
+        }
+        char buffer[128];
+        ssize_t len = read(fd, buffer, sizeof(buffer));
+        if (len <= 0) {
+            return false;
+        }
+        if (len >= sizeof(buffer)) {
+            ALOGE("proc file too big: %s", path);
+            return false;
+        }
+        buffer[len] = 0;
+        unsigned long t1;
+        unsigned long t2;
+        if (sscanf(buffer, "%lu %lu", &t1, &t2) != 2) {
+            return false;
+        }
+        cpu_time = t1;
+        cpu_delay = t2;
+        return true;
+    }
+};
+
+class AnrTimerService::Timer {
+  public:
+    // A unique ID assigned when the Timer is created.
+    timer_id_t const id;
+
+    // The creation parameters.  The timeout is the original, relative timeout.
+    int const pid;
+    int const uid;
+    nsecs_t const timeout;
+    bool const extend;
+
+    // The state of this timer.
+    Status status;
+
+    // The scheduled timeout.  This is an absolute time.  It may be extended.
+    nsecs_t scheduled;
+
+    // True if this timer has been extended.
+    bool extended;
+
+    // Bookkeeping for extensions.  The initial state of the process.  This is collected only if
+    // the timer is extensible.
+    ProcessStats initial;
+
+    // The default constructor is used to create timers that are Invalid, representing the "not
+    // found" condition when a collection is searched.
+    Timer() :
+            id(NOTIMER),
+            pid(0),
+            uid(0),
+            timeout(0),
+            extend(false),
+            status(Invalid),
+            scheduled(0),
+            extended(false) {
+    }
+
+    // This constructor creates a timer with the specified id.  This can be used as the argument
+    // to find().
+    Timer(timer_id_t id) :
+            id(id),
+            pid(0),
+            uid(0),
+            timeout(0),
+            extend(false),
+            status(Invalid),
+            scheduled(0),
+            extended(false) {
+    }
+
+    // Create a new timer.  This starts the timer.
+    Timer(int pid, int uid, nsecs_t timeout, bool extend) :
+            id(nextId()),
+            pid(pid),
+            uid(uid),
+            timeout(timeout),
+            extend(extend),
+            status(Running),
+            scheduled(now() + timeout),
+            extended(false) {
+        if (extend && pid != 0) {
+            initial.fill(pid);
+        }
+    }
+
+    // Cancel a timer.  Return the headroom (which may be negative).  This does not, as yet,
+    // account for extensions.
+    void cancel() {
+        ALOGW_IF(DEBUG && status != Running, "cancel %s", toString().c_str());
+        status = Canceled;
+    }
+
+    // Expire a timer. Return true if the timer is expired and false otherwise.  The function
+    // returns false if the timer is eligible for extension.  If the function returns false, the
+    // scheduled time is updated.
+    bool expire() {
+        ALOGI_IF(DEBUG, "expire %s", toString().c_str());
+        nsecs_t extension = 0;
+        if (extend && !extended) {
+            // Only one extension is permitted.
+            extended = true;
+            ProcessStats current;
+            current.fill(pid);
+            extension = current.cpu_delay - initial.cpu_delay;
+            if (extension < 0) extension = 0;
+            if (extension > timeout) extension = timeout;
+        }
+        if (extension == 0) {
+            status = Expired;
+        } else {
+            scheduled += extension;
+        }
+        return status == Expired;
+    }
+
+    // Accept a timeout.
+    void accept() {
+    }
+
+    // Discard a timeout.
+    void discard() {
+    }
+
+    // Timers are sorted by id, which is unique.  This provides fast lookups.
+    bool operator<(Timer const &r) const {
+        return id < r.id;
+    }
+
+    bool operator==(timer_id_t r) const {
+        return id == r;
+    }
+
+    std::string toString() const {
+        return StringPrintf("timer id=%d pid=%d status=%s", id, pid, statusString(status));
+    }
+
+    std::string toString(nsecs_t now) const {
+        uint32_t ms = nanoseconds_to_milliseconds(now - scheduled);
+        return StringPrintf("timer id=%d pid=%d status=%s scheduled=%ums",
+                            id, pid, statusString(status), -ms);
+    }
+
+    static int maxId() {
+        return idGen;
+    }
+
+  private:
+    // Get the next free ID.  NOTIMER is never returned.
+    static timer_id_t nextId() {
+        timer_id_t id = idGen.fetch_add(1);
+        while (id == NOTIMER) {
+            id = idGen.fetch_add(1);
+        }
+        return id;
+    }
+
+    // IDs start at 1.  A zero ID is invalid.
+    static std::atomic<timer_id_t> idGen;
+};
+
+// IDs start at 1.
+std::atomic<AnrTimerService::timer_id_t> AnrTimerService::Timer::idGen(1);
+
+/**
+ * Manage a set of timers and notify clients when there is a timeout.
+ */
+class AnrTimerService::Ticker {
+  private:
+    struct Entry {
+        const nsecs_t scheduled;
+        const timer_id_t id;
+        AnrTimerService* const service;
+
+        Entry(nsecs_t scheduled, timer_id_t id, AnrTimerService* service) :
+                scheduled(scheduled), id(id), service(service) {};
+
+        bool operator<(const Entry &r) const {
+            return scheduled == r.scheduled ? id < r.id : scheduled < r.scheduled;
+        }
+    };
+
+  public:
+
+    // Construct the ticker.  This creates the timerfd file descriptor and starts the monitor
+    // thread.  The monitor thread is given a unique name.
+    Ticker() {
+        timerFd_ = timer_create();
+        if (timerFd_ < 0) {
+            ALOGE("failed to create timerFd: %s", strerror(errno));
+            return;
+        }
+
+        if (pthread_create(&watcher_, 0, run, this) != 0) {
+            ALOGE("failed to start thread: %s", strerror(errno));
+            watcher_ = 0;
+            ::close(timerFd_);
+            return;
+        }
+
+        // 16 is a magic number from the kernel.  Thread names may not be longer than this many
+        // bytes, including the terminating null.  The snprintf() method will truncate properly.
+        char name[16];
+        snprintf(name, sizeof(name), "AnrTimerService");
+        pthread_setname_np(watcher_, name);
+
+        ready_ = true;
+    }
+
+    ~Ticker() {
+        // Closing the file descriptor will close the monitor process, if any.
+        if (timerFd_ >= 0) ::close(timerFd_);
+        timerFd_ = -1;
+        watcher_ = 0;
+    }
+
+    // Insert a timer.  Unless canceled, the timer will expire at the scheduled time.  If it
+    // expires, the service will be notified with the id.
+    void insert(nsecs_t scheduled, timer_id_t id, AnrTimerService *service) {
+        Entry e(scheduled, id, service);
+        AutoMutex _l(lock_);
+        timer_id_t front = headTimerId();
+        running_.insert(e);
+        if (front != headTimerId()) restartLocked();
+        maxRunning_ = std::max(maxRunning_, running_.size());
+    }
+
+    // Remove a timer.  The timer is identified by its scheduled timeout and id.  Technically,
+    // the id is sufficient (because timer IDs are unique) but using the timeout is more
+    // efficient.
+    void remove(nsecs_t scheduled, timer_id_t id) {
+        Entry key(scheduled, id, 0);
+        AutoMutex _l(lock_);
+        timer_id_t front = headTimerId();
+        auto found = running_.find(key);
+        if (found != running_.end()) running_.erase(found);
+        if (front != headTimerId()) restartLocked();
+    }
+
+    // Remove every timer associated with the service.
+    void remove(AnrTimerService const* service) {
+        AutoMutex _l(lock_);
+        timer_id_t front = headTimerId();
+        for (auto i = running_.begin(); i != running_.end(); i++) {
+            if (i->service == service) {
+                running_.erase(i);
+            }
+        }
+        if (front != headTimerId()) restartLocked();
+    }
+
+    // Return the number of timers still running.
+    size_t running() const {
+        AutoMutex _l(lock_);
+        return running_.size();
+    }
+
+    // Return the high-water mark of timers running.
+    size_t maxRunning() const {
+        AutoMutex _l(lock_);
+        return maxRunning_;
+    }
+
+  private:
+
+    // Return the head of the running list.  The lock must be held by the caller.
+    timer_id_t headTimerId() {
+        return running_.empty() ? NOTIMER : running_.cbegin()->id;
+    }
+
+    // A simple wrapper that meets the requirements of pthread_create.
+    static void* run(void* arg) {
+        reinterpret_cast<Ticker*>(arg)->monitor();
+        ALOGI("monitor exited");
+        return 0;
+    }
+
+    // Loop (almost) forever.  Whenever the timerfd expires, expire as many entries as
+    // possible.  The loop terminates when the read fails; this generally indicates that the
+    // file descriptor has been closed and the thread can exit.
+    void monitor() {
+        uint64_t token = 0;
+        while (read(timerFd_, &token, sizeof(token)) == sizeof(token)) {
+            // Move expired timers into the local ready list.  This is done inside
+            // the lock.  Then, outside the lock, expire them.
+            nsecs_t current = now();
+            std::vector<Entry> ready;
+            {
+                AutoMutex _l(lock_);
+                while (!running_.empty()) {
+                    Entry timer = *(running_.begin());
+                    if (timer.scheduled <= current) {
+                        ready.push_back(timer);
+                        running_.erase(running_.cbegin());
+                    } else {
+                        break;
+                    }
+                }
+                restartLocked();
+            }
+            // Call the notifiers outside the lock.  Calling the notifiers with the lock held
+            // can lead to deadlock, if the Java-side handler also takes a lock.  Note that the
+            // timerfd is already running.
+            for (auto i = ready.begin(); i != ready.end(); i++) {
+                Entry e = *i;
+                e.service->expire(e.id);
+            }
+        }
+    }
+
+    // Restart the ticker.  The caller must be holding the lock.  This method updates the
+    // timerFd_ to expire at the time of the first Entry in the running list.  This method does
+    // not check to see if the currently programmed expiration time is different from the
+    // scheduled expiration time of the first entry.
+    void restartLocked() {
+        if (!running_.empty()) {
+            Entry const x = *(running_.cbegin());
+            nsecs_t delay = x.scheduled - now();
+            // Force a minimum timeout of 10ns.
+            if (delay < 10) delay = 10;
+            time_t sec = nanoseconds_to_seconds(delay);
+            time_t ns = delay - seconds_to_nanoseconds(sec);
+            struct itimerspec setting = {
+                .it_interval = { 0, 0 },
+                .it_value = { sec, ns },
+            };
+            timer_settime(timerFd_, 0, &setting, nullptr);
+            restarted_++;
+            ALOGI_IF(DEBUG, "restarted timerfd for %ld.%09ld", sec, ns);
+        } else {
+            const struct itimerspec setting = {
+                .it_interval = { 0, 0 },
+                .it_value = { 0, 0 },
+            };
+            timer_settime(timerFd_, 0, &setting, nullptr);
+            drained_++;
+            ALOGI_IF(DEBUG, "drained timer list");
+        }
+    }
+
+    // The usual lock.
+    mutable Mutex lock_;
+
+    // True if the object was initialized properly.  Android does not support throwing C++
+    // exceptions, so clients should check this flag after constructing the object.  This is
+    // effectively const after the instance has been created.
+    bool ready_ = false;
+
+    // The file descriptor of the timer.
+    int timerFd_ = -1;
+
+    // The thread that monitors the timer.
+    pthread_t watcher_ = 0;
+
+    // The number of times the timer was restarted.
+    size_t restarted_ = 0;
+
+    // The number of times the timer list was exhausted.
+    size_t drained_ = 0;
+
+    // The highwater mark of timers that are running.
+    size_t maxRunning_ = 0;
+
+    // The list of timers that are scheduled.  This set is sorted by timeout and then by timer
+    // ID.  A set is sufficient (as opposed to a multiset) because timer IDs are unique.
+    std::set<Entry> running_;
+};
+
+
+AnrTimerService::AnrTimerService(char const* label,
+            notifier_t notifier, void* cookie, jweak jtimer, Ticker* ticker) :
+        label_(label),
+        notifier_(notifier),
+        notifierCookie_(cookie),
+        notifierObject_(jtimer),
+        ticker_(ticker) {
+
+    // Zero the statistics
+    maxActive_ = 0;
+    memset(&counters_, 0, sizeof(counters_));
+
+    ALOGI_IF(DEBUG, "initialized %s", label);
+}
+
+AnrTimerService::~AnrTimerService() {
+    AutoMutex _l(lock_);
+    ticker_->remove(this);
+}
+
+char const *AnrTimerService::statusString(Status s) {
+    switch (s) {
+        case Invalid: return "invalid";
+        case Running: return "running";
+        case Expired: return "expired";
+        case Canceled: return "canceled";
+    }
+    return "unknown";
+}
+
+AnrTimerService::timer_id_t AnrTimerService::start(int pid, int uid,
+        nsecs_t timeout, bool extend) {
+    ALOGI_IF(DEBUG, "starting");
+    AutoMutex _l(lock_);
+    Timer t(pid, uid, timeout, extend);
+    insert(t);
+    counters_.started++;
+
+    ALOGI_IF(DEBUG, "started timer %u timeout=%zu", t.id, static_cast<size_t>(timeout));
+    return t.id;
+}
+
+bool AnrTimerService::cancel(timer_id_t timerId) {
+    ALOGI_IF(DEBUG, "canceling %u", timerId);
+    if (timerId == NOTIMER) return false;
+    AutoMutex _l(lock_);
+    Timer timer = remove(timerId);
+
+    bool result = timer.status == Running;
+    if (timer.status != Invalid) {
+        timer.cancel();
+    } else {
+        counters_.error++;
+    }
+    counters_.canceled++;
+    ALOGI_IF(DEBUG, "canceled timer %u", timerId);
+    return result;
+}
+
+bool AnrTimerService::accept(timer_id_t timerId) {
+    ALOGI_IF(DEBUG, "accepting %u", timerId);
+    if (timerId == NOTIMER) return false;
+    AutoMutex _l(lock_);
+    Timer timer = remove(timerId);
+
+    bool result = timer.status == Expired;
+    if (timer.status == Expired) {
+        timer.accept();
+    } else {
+        counters_.error++;
+    }
+    counters_.accepted++;
+    ALOGI_IF(DEBUG, "accepted timer %u", timerId);
+    return result;
+}
+
+bool AnrTimerService::discard(timer_id_t timerId) {
+    ALOGI_IF(DEBUG, "discarding %u", timerId);
+    if (timerId == NOTIMER) return false;
+    AutoMutex _l(lock_);
+    Timer timer = remove(timerId);
+
+    bool result = timer.status == Expired;
+    if (timer.status == Expired) {
+        timer.discard();
+    } else {
+        counters_.error++;
+    }
+    counters_.discarded++;
+    ALOGI_IF(DEBUG, "discarded timer %u", timerId);
+    return result;
+}
+
+// Hold the lock in order to manage the running list.
+// the listener.
+void AnrTimerService::expire(timer_id_t timerId) {
+    ALOGI_IF(DEBUG, "expiring %u", timerId);
+    // Save the timer attributes for the notification
+    int pid = 0;
+    int uid = 0;
+    bool expired = false;
+    {
+        AutoMutex _l(lock_);
+        Timer t = remove(timerId);
+        expired = t.expire();
+        if (t.status == Invalid) {
+            ALOGW_IF(DEBUG, "error: expired invalid timer %u", timerId);
+            return;
+        } else {
+            // The timer is either Running (because it was extended) or expired (and is awaiting an
+            // accept or discard).
+            insert(t);
+        }
+    }
+
+    // Deliver the notification outside of the lock.
+    if (expired) {
+        if (!notifier_(timerId, pid, uid, notifierCookie_, notifierObject_)) {
+            AutoMutex _l(lock_);
+            // Notification failed, which means the listener will never call accept() or
+            // discard().  Do not reinsert the timer.
+            remove(timerId);
+        }
+    }
+    ALOGI_IF(DEBUG, "expired timer %u", timerId);
+}
+
+void AnrTimerService::insert(const Timer& t) {
+    running_.insert(t);
+    if (t.status == Running) {
+        // Only forward running timers to the ticker.  Expired timers are handled separately.
+        ticker_->insert(t.scheduled, t.id, this);
+        maxActive_ = std::max(maxActive_, running_.size());
+    }
+}
+
+AnrTimerService::Timer AnrTimerService::remove(timer_id_t timerId) {
+    Timer key(timerId);
+    auto found = running_.find(key);
+    if (found != running_.end()) {
+        Timer result = *found;
+        running_.erase(found);
+        ticker_->remove(result.scheduled, result.id);
+        return result;
+    }
+    return Timer();
+}
+
+void AnrTimerService::dump(bool verbose) const {
+    AutoMutex _l(lock_);
+    ALOGI("timer %s ops started=%zu canceled=%zu accepted=%zu discarded=%zu expired=%zu",
+          label_.c_str(),
+          counters_.started, counters_.canceled, counters_.accepted,
+          counters_.discarded, counters_.expired);
+    ALOGI("timer %s stats max-active=%zu/%zu running=%zu/%zu errors=%zu",
+          label_.c_str(),
+          maxActive_, ticker_->maxRunning(), running_.size(), ticker_->running(),
+          counters_.error);
+
+    if (verbose) {
+        nsecs_t time = now();
+        for (auto i = running_.begin(); i != running_.end(); i++) {
+            Timer t = *i;
+            ALOGI("   running %s", t.toString(time).c_str());
+        }
+    }
+}
+
+/**
+ * True if the native methods are supported in this process.  Native methods are supported only
+ * if the initialization succeeds.
+ */
+bool nativeSupportEnabled = false;
+
+/**
+ * Singleton/globals for the anr timer.  Among other things, this includes a Ticker* and a use
+ * count.  The JNI layer creates a single Ticker for all operational AnrTimers.  The Ticker is
+ * created when the first AnrTimer is created, and is deleted when the last AnrTimer is closed.
+ */
+static Mutex gAnrLock;
+struct AnrArgs {
+    jclass clazz = NULL;
+    jmethodID func = NULL;
+    JavaVM* vm = NULL;
+    AnrTimerService::Ticker* ticker = nullptr;
+    int tickerUseCount = 0;;
+};
+static AnrArgs gAnrArgs;
+
+// The cookie is the address of the AnrArgs object to which the notification should be sent.
+static bool anrNotify(AnrTimerService::timer_id_t timerId, int pid, int uid,
+                      void* cookie, jweak jtimer) {
+    AutoMutex _l(gAnrLock);
+    AnrArgs* target = reinterpret_cast<AnrArgs* >(cookie);
+    JNIEnv *env;
+    if (target->vm->AttachCurrentThread(&env, 0) != JNI_OK) {
+        ALOGE("failed to attach thread to JavaVM");
+        return false;
+    }
+    jboolean r = false;
+    jobject timer = env->NewGlobalRef(jtimer);
+    if (timer != nullptr) {
+        r = env->CallBooleanMethod(timer, target->func, timerId, pid, uid);
+        env->DeleteGlobalRef(timer);
+    }
+    target->vm->DetachCurrentThread();
+    return r;
+}
+
+jboolean anrTimerSupported(JNIEnv* env, jclass) {
+    return nativeSupportEnabled;
+}
+
+jlong anrTimerCreate(JNIEnv* env, jobject jtimer, jstring jname) {
+    if (!nativeSupportEnabled) return 0;
+    AutoMutex _l(gAnrLock);
+    if (!gAnrArgs.ticker) {
+        gAnrArgs.ticker = new AnrTimerService::Ticker();
+    }
+    gAnrArgs.tickerUseCount++;
+
+    ScopedUtfChars name(env, jname);
+    jobject timer = env->NewWeakGlobalRef(jtimer);
+    AnrTimerService* service =
+            new AnrTimerService(name.c_str(), anrNotify, &gAnrArgs, timer, gAnrArgs.ticker);
+    return reinterpret_cast<jlong>(service);
+}
+
+AnrTimerService *toService(jlong pointer) {
+    return reinterpret_cast<AnrTimerService*>(pointer);
+}
+
+jint anrTimerClose(JNIEnv* env, jclass, jlong ptr) {
+    if (!nativeSupportEnabled) return -1;
+    if (ptr == 0) return -1;
+    AutoMutex _l(gAnrLock);
+    AnrTimerService *s = toService(ptr);
+    env->DeleteWeakGlobalRef(s->jtimer());
+    delete s;
+    if (--gAnrArgs.tickerUseCount <= 0) {
+        delete gAnrArgs.ticker;
+        gAnrArgs.ticker = nullptr;
+    }
+    return 0;
+}
+
+jint anrTimerStart(JNIEnv* env, jclass, jlong ptr,
+        jint pid, jint uid, jlong timeout, jboolean extend) {
+    if (!nativeSupportEnabled) return 0;
+    // On the Java side, timeouts are expressed in milliseconds and must be converted to
+    // nanoseconds before being passed to the library code.
+    return toService(ptr)->start(pid, uid, milliseconds_to_nanoseconds(timeout), extend);
+}
+
+jboolean anrTimerCancel(JNIEnv* env, jclass, jlong ptr, jint timerId) {
+    if (!nativeSupportEnabled) return false;
+    return toService(ptr)->cancel(timerId);
+}
+
+jboolean anrTimerAccept(JNIEnv* env, jclass, jlong ptr, jint timerId) {
+    if (!nativeSupportEnabled) return false;
+    return toService(ptr)->accept(timerId);
+}
+
+jboolean anrTimerDiscard(JNIEnv* env, jclass, jlong ptr, jint timerId) {
+    if (!nativeSupportEnabled) return false;
+    return toService(ptr)->discard(timerId);
+}
+
+jint anrTimerDump(JNIEnv *env, jclass, jlong ptr, jboolean verbose) {
+    if (!nativeSupportEnabled) return -1;
+    toService(ptr)->dump(verbose);
+    return 0;
+}
+
+static const JNINativeMethod methods[] = {
+    {"nativeAnrTimerSupported", "()Z",  (void*) anrTimerSupported},
+    {"nativeAnrTimerCreate", "(Ljava/lang/String;)J", (void*) anrTimerCreate},
+    {"nativeAnrTimerClose", "(J)I",     (void*) anrTimerClose},
+    {"nativeAnrTimerStart", "(JIIJZ)I", (void*) anrTimerStart},
+    {"nativeAnrTimerCancel", "(JI)Z",   (void*) anrTimerCancel},
+    {"nativeAnrTimerAccept", "(JI)Z",   (void*) anrTimerAccept},
+    {"nativeAnrTimerDiscard", "(JI)Z",  (void*) anrTimerDiscard},
+    {"nativeAnrTimerDump", "(JZ)V",     (void*) anrTimerDump},
+};
+
+} // anonymous namespace
+
+int register_android_server_utils_AnrTimer(JNIEnv* env)
+{
+    static const char *className = "com/android/server/utils/AnrTimer";
+    jniRegisterNativeMethods(env, className, methods, NELEM(methods));
+
+    jclass service = FindClassOrDie(env, className);
+    gAnrArgs.clazz = MakeGlobalRefOrDie(env, service);
+    gAnrArgs.func = env->GetMethodID(gAnrArgs.clazz, "expire", "(III)Z");
+    env->GetJavaVM(&gAnrArgs.vm);
+
+    nativeSupportEnabled = NATIVE_SUPPORT;
+
+    return 0;
+}
+
+} // namespace android
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index 11734da..f3158d1 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -52,6 +52,7 @@
 int register_android_server_HardwarePropertiesManagerService(JNIEnv* env);
 int register_android_server_SyntheticPasswordManager(JNIEnv* env);
 int register_android_hardware_display_DisplayViewport(JNIEnv* env);
+int register_android_server_utils_AnrTimer(JNIEnv *env);
 int register_android_server_am_OomConnection(JNIEnv* env);
 int register_android_server_am_CachedAppOptimizer(JNIEnv* env);
 int register_android_server_am_LowMemDetector(JNIEnv* env);
@@ -113,6 +114,7 @@
     register_android_server_storage_AppFuse(env);
     register_android_server_SyntheticPasswordManager(env);
     register_android_hardware_display_DisplayViewport(env);
+    register_android_server_utils_AnrTimer(env);
     register_android_server_am_OomConnection(env);
     register_android_server_am_CachedAppOptimizer(env);
     register_android_server_am_LowMemDetector(env);
diff --git a/services/tests/servicestests/jni/Android.bp b/services/tests/servicestests/jni/Android.bp
index 174beb8..c30e4eb 100644
--- a/services/tests/servicestests/jni/Android.bp
+++ b/services/tests/servicestests/jni/Android.bp
@@ -23,6 +23,7 @@
         ":lib_cachedAppOptimizer_native",
         ":lib_gameManagerService_native",
         ":lib_oomConnection_native",
+        ":lib_anrTimer_native",
         "onload.cpp",
     ],
 
@@ -55,4 +56,4 @@
         "android.hardware.graphics.mapper@4.0",
         "android.hidl.token@1.0-utils",
     ],
-}
\ No newline at end of file
+}
diff --git a/services/tests/servicestests/jni/onload.cpp b/services/tests/servicestests/jni/onload.cpp
index f160b3d..25487c5 100644
--- a/services/tests/servicestests/jni/onload.cpp
+++ b/services/tests/servicestests/jni/onload.cpp
@@ -27,6 +27,7 @@
 int register_android_server_am_CachedAppOptimizer(JNIEnv* env);
 int register_android_server_app_GameManagerService(JNIEnv* env);
 int register_android_server_am_OomConnection(JNIEnv* env);
+int register_android_server_utils_AnrTimer(JNIEnv *env);
 };
 
 using namespace android;
@@ -44,5 +45,6 @@
     register_android_server_am_CachedAppOptimizer(env);
     register_android_server_app_GameManagerService(env);
     register_android_server_am_OomConnection(env);
+    register_android_server_utils_AnrTimer(env);
     return JNI_VERSION_1_4;
 }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java
index 59c94dc..89a4961 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java
@@ -65,7 +65,6 @@
 import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintResetLockoutClient;
 
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.Mock;
@@ -248,7 +247,6 @@
     }
 
     @Test
-    @Ignore("b/317403648")
     public void lockoutPermanentResetViaClient() {
         setLockoutPermanent();
 
diff --git a/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java b/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java
index 861d14a..6c085e0 100644
--- a/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java
+++ b/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java
@@ -23,17 +23,21 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
+import android.util.Log;
 
 import android.platform.test.annotations.Presubmit;
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.annotations.GuardedBy;
 
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameters;
 
+import java.io.PrintWriter;
+import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -45,6 +49,9 @@
 @RunWith(Parameterized.class)
 public class AnrTimerTest {
 
+    // A log tag.
+    private static final String TAG = "AnrTimerTest";
+
     // The commonly used message timeout key.
     private static final int MSG_TIMEOUT = 1;
 
@@ -63,9 +70,7 @@
         }
     }
 
-    /**
-     * The test handler is a self-contained object for a single test.
-     */
+    /** The test helper is a self-contained object for a single test. */
     private static class Helper {
         final Object mLock = new Object();
 
@@ -114,7 +119,7 @@
     /**
      * Force AnrTimer to use the test parameter for the feature flag.
      */
-    class TestInjector extends AnrTimer.Injector {
+    private class TestInjector extends AnrTimer.Injector {
         @Override
         boolean anrTimerServiceEnabled() {
             return mEnabled;
@@ -124,9 +129,9 @@
     /**
      * An instrumented AnrTimer.
      */
-    private static class TestAnrTimer extends AnrTimer<TestArg> {
+    private class TestAnrTimer extends AnrTimer<TestArg> {
         private TestAnrTimer(Handler h, int key, String tag) {
-            super(h, key, tag);
+            super(h, key, tag, false, new TestInjector());
         }
 
         TestAnrTimer(Helper helper) {
@@ -173,35 +178,103 @@
     @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]);
+        try (TestAnrTimer timer = new TestAnrTimer(helper)) {
+            // One-time check that the injector is working as expected.
+            assertEquals(mEnabled, timer.serviceEnabled());
+            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.
+     * Verify that a restarted timer is delivered exactly once.  The initial timer value is very
+     * large, to ensure it does not expire before the timer can be restarted.
+     */
+    @Test
+    public void testTimerRestart() throws Exception {
+        Helper helper = new Helper(1);
+        try (TestAnrTimer timer = new TestAnrTimer(helper)) {
+            TestArg t = new TestArg(1, 1);
+            timer.start(t, 10000);
+            // Briefly pause.
+            assertFalse(helper.await(10));
+            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 a restarted timer is delivered exactly once.  The initial timer value is very
+     * large, to ensure it does not expire before the timer can be restarted.
+     */
+    @Test
+    public void testTimerZero() throws Exception {
+        Helper helper = new Helper(1);
+        try (TestAnrTimer timer = new TestAnrTimer(helper)) {
+            TestArg t = new TestArg(1, 1);
+            timer.start(t, 0);
+            // 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 on a single AnrTimer, 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]);
+        try (TestAnrTimer timer = new TestAnrTimer(helper)) {
+            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 if three timers are scheduled on three separate AnrTimers, they are delivered
+     * in time order.
+     */
+    @Test
+    public void testMultipleServices() throws Exception {
+        // Expect three messages.
+        Helper helper = new Helper(3);
+        TestArg t1 = new TestArg(1, 1);
+        TestArg t2 = new TestArg(1, 2);
+        TestArg t3 = new TestArg(1, 3);
+        try (TestAnrTimer x1 = new TestAnrTimer(helper);
+             TestAnrTimer x2 = new TestAnrTimer(helper);
+             TestAnrTimer x3 = new TestAnrTimer(helper)) {
+            x1.start(t1, 50);
+            x2.start(t2, 60);
+            x3.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]);
+        }
     }
 
     /**
@@ -211,20 +284,109 @@
     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]);
+        try (TestAnrTimer timer = new TestAnrTimer(helper)) {
+            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]);
+        }
+    }
+
+    /**
+     * Return the dump string.
+     */
+    private String getDumpOutput() {
+        StringWriter sw = new StringWriter();
+        PrintWriter pw = new PrintWriter(sw);
+        AnrTimer.dump(pw, true, new TestInjector());
+        pw.close();
+        return sw.getBuffer().toString();
+    }
+
+    /**
+     * Verify the dump output.
+     */
+    @Test
+    public void testDumpOutput() throws Exception {
+        String r1 = getDumpOutput();
+        assertEquals(false, r1.contains("timer:"));
+
+        Helper helper = new Helper(2);
+        TestArg t1 = new TestArg(1, 1);
+        TestArg t2 = new TestArg(1, 2);
+        TestArg t3 = new TestArg(1, 3);
+        try (TestAnrTimer timer = new TestAnrTimer(helper)) {
+            timer.start(t1, 5000);
+            timer.start(t2, 5000);
+            timer.start(t3, 5000);
+
+            String r2 = getDumpOutput();
+            // There are timers in the list if and only if the feature is enabled.
+            final boolean expected = mEnabled;
+            assertEquals(expected, r2.contains("timer:"));
+        }
+
+        String r3 = getDumpOutput();
+        assertEquals(false, r3.contains("timer:"));
+    }
+
+    /**
+     * Verify that GC works as expected.  This test will almost certainly be flaky, since it
+     * relies on the finalizers running, which is a best-effort on the part of the JVM.
+     * Therefore, the test is marked @Ignore.  Remove that annotation to run the test locally.
+     */
+    @Ignore
+    @Test
+    public void testGarbageCollection() throws Exception {
+        if (!mEnabled) return;
+
+        String r1 = getDumpOutput();
+        assertEquals(false, r1.contains("timer:"));
+
+        Helper helper = new Helper(2);
+        TestArg t1 = new TestArg(1, 1);
+        TestArg t2 = new TestArg(1, 2);
+        TestArg t3 = new TestArg(1, 3);
+        // The timer is explicitly not closed.  It is, however, scoped to the next block.
+        {
+            TestAnrTimer timer = new TestAnrTimer(helper);
+            timer.start(t1, 5000);
+            timer.start(t2, 5000);
+            timer.start(t3, 5000);
+
+            String r2 = getDumpOutput();
+            // There are timers in the list if and only if the feature is enabled.
+            final boolean expected = mEnabled;
+            assertEquals(expected, r2.contains("timer:"));
+        }
+
+        // Try to make finalizers run.  The timer object above should be a candidate.  Finalizers
+        // are run on their own thread, so pause this thread to give that thread some time.
+        String r3 = getDumpOutput();
+        for (int i = 0; i < 10 && r3.contains("timer:"); i++) {
+            Log.i(TAG, "requesting finalization " + i);
+            System.gc();
+            System.runFinalization();
+            Thread.sleep(4 * 1000);
+            r3 = getDumpOutput();
+        }
+
+        // The timer was not explicitly closed but it should have been implicitly closed by GC.
+        assertEquals(false, r3.contains("timer:"));
+    }
+
+    // TODO: [b/302724778] Remove manual JNI load
+    static {
+        System.loadLibrary("servicestestjni");
     }
 }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 39779b0..f1edd9a 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -303,7 +303,6 @@
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.ClassRule;
-import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TestRule;
@@ -14061,7 +14060,6 @@
     }
 
     @Test
-    @Ignore("b/316989461")
     public void cancelNotificationsFromListener_rapidClear_oldNew_cancelOne()
             throws RemoteException {
         mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
@@ -14073,7 +14071,8 @@
         mService.addNotification(nr1);
 
         // Create old notification.
-        final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0);
+        final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel,
+                System.currentTimeMillis() - 60000);
         mService.addNotification(nr2);
 
         // Cancel specific notifications via listener.
@@ -14091,16 +14090,17 @@
     }
 
     @Test
-    @Ignore("b/316989461")
     public void cancelNotificationsFromListener_rapidClear_old_cancelOne() throws RemoteException {
         mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
                 .FLAG_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER_APP_OP_ENABLED);
 
         // Create old notifications.
-        final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel, 0);
+        final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel,
+                System.currentTimeMillis() - 60000);
         mService.addNotification(nr1);
 
-        final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0);
+        final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel,
+                System.currentTimeMillis() - 60000);
         mService.addNotification(nr2);
 
         // Cancel specific notifications via listener.
@@ -14119,7 +14119,6 @@
     }
 
     @Test
-    @Ignore("b/316989461")
     public void cancelNotificationsFromListener_rapidClear_oldNew_cancelOne_flagDisabled()
             throws RemoteException {
         mSetFlagsRule.disableFlags(android.view.contentprotection.flags.Flags
@@ -14131,7 +14130,8 @@
         mService.addNotification(nr1);
 
         // Create old notification.
-        final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0);
+        final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel,
+                System.currentTimeMillis() - 60000);
         mService.addNotification(nr2);
 
         // Cancel specific notifications via listener.
@@ -14150,7 +14150,6 @@
     }
 
     @Test
-    @Ignore("b/316989461")
     public void cancelNotificationsFromListener_rapidClear_oldNew_cancelAll()
             throws RemoteException {
         mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
@@ -14162,7 +14161,8 @@
         mService.addNotification(nr1);
 
         // Create old notification.
-        final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0);
+        final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel,
+                System.currentTimeMillis() - 60000);
         mService.addNotification(nr2);
 
         // Cancel all notifications via listener.
@@ -14179,16 +14179,17 @@
     }
 
     @Test
-    @Ignore("b/316989461")
     public void cancelNotificationsFromListener_rapidClear_old_cancelAll() throws RemoteException {
         mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
                 .FLAG_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER_APP_OP_ENABLED);
 
         // Create old notifications.
-        final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel, 0);
+        final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel,
+                System.currentTimeMillis() - 60000);
         mService.addNotification(nr1);
 
-        final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0);
+        final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel,
+                System.currentTimeMillis() - 60000);
         mService.addNotification(nr2);
 
         // Cancel all notifications via listener.
@@ -14206,7 +14207,6 @@
     }
 
     @Test
-    @Ignore("b/316989461")
     public void cancelNotificationsFromListener_rapidClear_oldNew_cancelAll_flagDisabled()
             throws RemoteException {
         mSetFlagsRule.disableFlags(android.view.contentprotection.flags.Flags
@@ -14218,7 +14218,8 @@
         mService.addNotification(nr1);
 
         // Create old notification.
-        final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0);
+        final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel,
+                System.currentTimeMillis() - 60000);
         mService.addNotification(nr2);
 
         // Cancel all notifications via listener.
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 6497ee9..782d89c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -115,6 +115,9 @@
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.util.ArraySet;
 import android.util.DisplayMetrics;
 import android.view.Display;
@@ -146,6 +149,7 @@
 import com.android.server.policy.WindowManagerPolicy;
 import com.android.server.wm.utils.WmDisplayCutout;
 
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -172,6 +176,10 @@
 @RunWith(WindowTestRunner.class)
 public class DisplayContentTests extends WindowTestsBase {
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @SetupWindows(addAllCommonWindows = true)
     @Test
     public void testForAllWindows() {
@@ -508,6 +516,7 @@
      * Tests tapping on a root task in different display results in window gaining focus.
      */
     @Test
+    @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_REMOVE_POINTER_EVENT_TRACKING_IN_WM)
     public void testInputEventBringsCorrectDisplayInFocus() {
         DisplayContent dc0 = mWm.getDefaultDisplayContentLocked();
         // Create a second display